Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / C#

Automating Windows Applications Using the WCF Equipped Injected Component

Rate me:
Please Sign up or sign in to vote.
4.96/5 (47 votes)
15 Nov 2010CPOL14 min read 102.3K   2K   122   16
A .NET WCF equipped component injected into automated process allows both local and remote clients to control the process and get its asynchronous events.
Image 1
  • Introduction
  • Background
  • Injection Steps
  • Components
  • Client Notification
  • Threading
  • How Does Sample Work?
  • Running the Sample
  • Discussion
  • Conclusion

Introduction

This article discusses a way to convert a Windows application that does not export any program interface to an automation server. Many very good publications have presented and discussed in depth various aspects of this problem. Most of these works dealt with techniques for injection of code into process to be automated (target process) and API hooking. This article demonstrates the injection to target process using remote thread and focuses on communication between injected code and outside world. Usage of Windows Communication Foundation (WCF) services for such communication is presented in this article.

Background

Actually this article is a continuation of my article Automating Windows Application describing injection of a COM object into target process. A well-known Notepad text editor was taken as an example target application for automation. The following approach was suggested. NotepadPlugin.dll was injected into Notepad.exe target process with the remote thread technique. The plug-in code subclasses both outer (frame) and inner (view) windows of Notepad application, installing custom handlers for several Windows messages (e. g., WM_CHAR). Prior to quit, the remote thread procedure posts a user-registered message WM_CREATE_OBJECT to, say, outer window. This message is handled by a subclass window procedure. The handler creates COM object which registers itself with Running Object Table (ROT) effectively exposing its direct interfaces outside target process, machine-wide. Clients bind to the object, use its direct interfaces to manipulate target application and implement its outgoing (sink) interfaces to subscribe to target applications events firing via connection point mechanism.

This technique works fine, but has a limitation: clients and target application should run on the same machine since ROT is available only locally. One possible way to overcome this limitation is suggested in this article.

To achieve this, the above technique is undergone a remarkable modification: instead of embedding of unmanaged true COM object into target process, this time a managed .NET object capable to communicate with outside world via WCF services is embedded into target process. This managed object is enveloped in COM Callable Wrapper (CCW) referred below to also as simply COM wrapper. The suggested technique is explained based on a code sample.

Injection Steps

The injection itself is remained almost unchanged with regard to the previous article. For the sake of simplicity we assume a single-threaded target application having an outer (frame) and an inner (view) windows. The following steps are taken to automate target application.

  • NotepadPlugin.dll in injected into the target process with remote thread technique by Injector COM object. This is a worker thread, and its function is DllMain() of NotepadPlugin.dll. Remote thread ends after DllMain() returns. Injector COM object is instantiated by AutomationClientNET application running as a local client. Its purpose is to find a running target Notepad process (or to start a new one if there is no already running Notepads) and to inject plug-in into the target process. After NotepadPlugin.dll injection, the Injector is no longer used and may be released. Injector is used by only one local client, below referred to as Client-Injector.
  • NotepadPlugin.dll subclasses outer and inner windows of target application with FrameWndProc() and ViewWndProc() window procedures respectively. These window procedures contain message handlers with new functionality for the target application. Function PluginProc() (called by DllMain()) of the NotepadPlugin.dll actually performs the subclassing. Both NotepadPlugin.dll and Injector use helper WindowFinder COM object to find Notepad windows.
  • Function PluginProc() of NotepadPlugin.dll running in remote thread, posts a user-registered WM_CREATE_OBJECT Windows message to the Notepad frame window. FrameWndProc() window procedure handles WM_CREATE_OBJECT message. The handler creates a managed NotepadHandlerNET object using its COM wrapper.

Components

Main components embedded into Notepad target process and AutomationClientNET application are depicted in the figure below.

Image 2

After injection, two pieces of "foreign" code are placed into target process, namely, unmanaged NotepadPlugin.dll and managed NotepadHandlerNET object (this can be easily seen with a famous Process Explorer utility by Mark Russinovich as it is shown in the figure below).

Image 3

Managed NotepadHandlerNET object is enveloped in COM wrapper. The wrapper is prepared with RegAsm.exe utility. The following command called on after build of NotepadHandlerNET as a Post-Build Event:

RegAsm NotepadHandlerNET.dll /tlb

File NotepadHandlerNET.tlb, output of this call, is used in #import directive in NotepadPlugin.dll (file NotepadPlugin.cpp). Unmanaged NotepadPlugin.dll subclasses outer and inner windows of target application with Windows procedures FrameWndProc() and ViewWndProc(). These procedures intercept and pre-process some Windows messages for the target application windows. Message handlers of these functions call managed NotepadHandlerNET object using CCW of IHandler interface implemented by Handler class. IHandler interface has the only method Do(). The method's first parameter contains type of processing required (e. g., "SetText"), while the second parameter provides data for processing, and, after processing, contains result data. This method provides uniform interface for any call. There is no need to rebuild COM wrapper with appearance of new calls. NotepadHandlerNET object calls NotepadPlugin.dll functions with P/Invoke technique. Functions to be called should be global and exported (functions ActivateFindDialog() and AppendMainMenu() in our sample).

As it was stated above, the reason to embed managed component into unmanaged target application was an ability for WCF service communication. Duplex WCF communication is implemented with two unilateral services. Common classes to construct service host and client proxy are located in SvcHost.dll. NotepadHandlerNET object hosts ICommander ServiceContract (interface):

C#
[ServiceContract]
public interface ICommander
{
    [OperationContract]
    object Command(string sender, string commandName, object commandData);
}

Client application ApplicationClientNET.exe hosts Notification service implementing INotification ServiceContract:

C#
[ServiceContract]
public interface INotification
{
    [OperationContract]
    object Notify(string sender, string eventName, object eventData);
}

The service contracts are very similar. Both have just one operation contract (method) for uniform command / notification with parameters describing sender, command / event name and appropriate data to transfer. Generally the operation contracts may return any object. Such service contracts are flexible and do not require to change their client proxies every time when new command or event would appear.

In NotepadHandlerNET embedded object class Commander implements ICommander ServiceContract. On every call its method Command() constructs a new instance of a class with name identical to commandName parameter (in lower case letters) using Activator.CreateInstance() static method:

C#
// Command method is called by client. It creates actual command
// class by the command name and calls its DoCommand method.
public object Command(string sender, string commandName, object commandData)
{
    object ret = null;
    try
    {
        ICommandImpl comImpl = 
            Activator.CreateInstance(GetCommandTypeByName(commandName))
            as ICommandImpl;
        if (null != comImpl)
            ret = comImpl.DoCommand(sender, commandData);
    }
    catch (Exception e)
    {
        ret = e;
    }

    return ret;
}

These command classes implement ICommandImpl interface with its only DoCommand() method. Such design provides a generic mechanism for commands invocation. In our sample NotepadHandlerNET contains command classes in file Commands.cs. In real world applications commands may be placed in separate assemblies loaded at runtime thus making them truly independent and easily replaceable. It is also possible to pool instances of command classes for performance reasons. Methods DoCommand() of the command classes actually execute clients commands within target application. This is achieved particularly with call functions of unmanaged NotepadPlugin.dll using P/Invoke technique.

Client Notification

Important task of DoCommand() method is a clients notification. The notification mechanism is designed as follows. To get notified by NotepadHandlerNET each client hosts WCF Notification service implementing INotification ServiceContract. Based on this service, special WCF utility SvcUtil.exe generates (see NotificationProxyGenerator.cmd file) correspondent classes for NotepadHandlerNET to create appropriate proxy. Client starts its collaboration with embedded NotepadHandlerNET component with the register command. Its method DoCommand() instantiates AddNotificationSubscriber class for a client specified with the sender constructor parameter, and calls method AddNotificationSubscriber.Do() creating notification proxy for the client (with help of SvcHost.Client.ConnectTo<>() static method). The proxy is placed in special dictionary object in NotificationBase class to be used for the client notification.

Threading

There is a threading issue in notification mechanism. By default WCF service operates synchronously in one thread. It means that all commands in NotepadHandlerNET and all notifications in AutomationClientNET.Notification class are executed sequentially. Each command and notification call blocks a caller thread until its return. So, attempt to notify client directly from DoCommand() method causes deadlock (similar effect would produce attempt of client to send a command to NotepadHandlerNET directly from notification receiving AutomationClientNET.Notification.Notify() method. Of course multi-threaded behavior of WCF service may be achieved by appropriate service attributes. But in our code sample this problem is solved with ThreadPool class. Method DoCommand() does not notify client directly. Instead, when notification is required, it instantiates helper class NotifyAll and calls its method NotifyAll.Do(). It calls ThreadPool.QueueUserWorkItem() static method for each notification proxy providing sending notification from different threads. The deadlock is avoided. And client simply does not send command from within notification handler.

Another threads-related problem may arise in target process when DoCommand() methods of commands classes call functions of Notepad windows. DoCommand() methods are executed in the Command service thread that may not be a Notepad main thread in which Notepad windows was created. Since most window functions should be called from the same thread where the window was created, a call from different thread may cause a crash. In our code sample functions ActivateFindDialog() and AppendMainMenu() are not thread sensitive, and therefore no special precaution is required to call them. But in general special care should be taken to call window functions from the window native thread. This may be achieved, for example, by creating a hidden form in the main thread (that is in NotepadHandlerNET.Handler class constructor), checking thread with InvokeRequired property of the hidden form and, if thread in not a native one then Invoke() window function with appropriate delegate.

How Does Sample Work?

After main components have been described we can discuss the working sample. To begin with, we have tune configuration file of client application AutomationClientNET.exe. This is App.config file in AutomationClientNET project of Visual Studio transformed to AutomationClientNET.exe.config file after build. The file contains the following application settings:

XML
<appSettings>
    <!-- Replace "localhost" with Notepad Machine IP Address -->
    <add key ="EndpointAddressUri" value="http://localhost:29700" />
    <add key ="Client-Injector" value="true" />
</appSettings>

Parameter EndpointAddressUri representing the IP address of Command service (embedded to Notepad) should be changed if we are using particular client remotely: word "localhost" has to be replaced with an IP address of Notepad machine. Parameter Client-Injector set to "true", indicates that this client runs locally and injects code to target Notepad process (Client-Injector). Only one local client may be a Client-Injector. All other clients should have parameter Client-Injector set to "false", no matter whether they are local or remote. Large button of Client-Injector bears "AUTOMATE NOTEPAD" caption, whereas other clients have the button with "Bind to Notepad" written on it.

Of course, binding is possible only to already automated target application. COM wrapped (non-GAC-registered) .NET component called by unmanaged caller application should locate in the same directory with the caller. In our case it means that NotepadHandlerNET assembly (dll) and Notepad.exe target application are locate in the same directory. Normally Notepad.exe locates in <SystemRoot>\system32 directory.

Having no intention to place NotepadHandlerNET.dll to system32, I decided to perform the opposite operation; after its large button is pressed, Client-Injector copies Notepad.exe to the directory containing NotepadHandlerNET.dll and starts it (of course, if Notepad.exe has not yet been copied and run from there). So, Client-Injector AutomationClientNET.exe, Notepad.exe and NotepadHandlerNET.dll should share the same directory.

Attempts to automate Notepad.exe started from a different directory lead to an error. Method AutomationClientNET.NotepadClientForm.Inject() of client's main form carries out this Notepad coping task. The method is called only if the client is a Client-Injector. The same Inject() method manipulates NotepadHandlerNET configuration file. To allow NotepadHandlerNET embedded component to use its configuration file without additional code, the configuration file is renamed to Notepad.exe.config to match target application. Then method Inject() instantiates Injector COM in-proc object and calls its the only InjHelperClass.Run() method effectively injecting NotepadPlugin.dll into target Notepad.exe application. Injection has been described above in the Injection Steps paragraph.

AutomationClientNet.exe client applications transfer commands to NotepadHandlerNET embedded component using WCF Commander service. Its ServiceContract has been already discussed. Client application uses Commander service proxy generated with WCF SvcUtil.exe utility (see CommanderProxyGenerator.cmd file) to perform command calls. The service is configured by NotepadHandlerNET configuration file renamed to Notepad.exe.config. Content of the service XML tag of Notepad.exe.config is shown below:

XML
<service behaviorConfiguration="CommanderBehavior"
        name="NotepadHandlerNET.Commander">
    <endpoint address="Commander" binding="basicHttpBinding" 
        contract="NotepadHandlerNET.ICommander" />
    <endpoint address="Mex"       binding="mexHttpBinding"   
        contract="IMetadataExchange" />
    <host>
    <baseAddresses>
        <!-- Replace "localhost" with Notepad Machine IP Address -->
        <add baseAddress="http://localhost:29700" />
    </baseAddresses>
    </host>
</service>

The service is configured for HTTP access with metadata exchange enabled. To allow remote clients access, "localhost" in the baseAddress parameter should be replaced with Notepad machine IP address. Commander.Command() method instantiates class of command received and runs method DoCommand() of newly created instance of the command class. According to a command task its DoCommand() method may or may not call unmanaged functions exported by NotepadPlugin.dll. In case NotepadPlugin.dll is involved, after the job is completed its methods call Handler component Do() method using CCW. If client(s) notification is required then notification classes AddNotificationSubscriber and NotifyAll derived from NotificationBase class create and activate notification proxy using the ThreadPool-based mechanism described in the Threading paragraph.

AutomationClientNET.exe client applications host Notification service configured by AutomationClientNET.exe.config file. Content of the service XML tag of AutomationClientNET.exe.config is shown below:

XML
<service behaviorConfiguration="NotificationBehavior"
        name="AutomationClientNET.Notification">
    <endpoint address="Commander" binding="basicHttpBinding" 
        contract="AutomationClientNET.INotification" />
    <endpoint address="Mex"       binding="mexHttpBinding"   
        contract="IMetadataExchange" />
    <host>
    <baseAddresses>
        <!-- Replace "localhost" with Client Machine IP Address -->
        <add baseAddress="http://localhost:29701" />
    </baseAddresses>
    </host>
</service>

The service is configured for HTTP access with metadata exchange enabled. To allow remote clients access, "localhost" in the baseAddress parameter should be replaced with the IP address of this client machine. When several clients are simultaneously running on the same machine, each client should have its port unique on this machine.

Running the Sample

Please make sure that WCF (.NET 3.0) is installed on all machines where you are planning to run the service. In your ClientInjector directory run COMRegistration.cmd command file to register WindowFinder.dll and Injector.dll COM in-proc servers and to prepare CCW for managed NotepadHandlerNET.dll. Then start AutomationClientNET.exe Client-Injector application and press its "AUTOMATE NOTEPAD" (large button). It causes Notepad.exe to be copied to the ClientInjector directory and appearance of Notepad.exe.config file. Notepad.exe is started and got automated. Its caption is changed accordingly, and large button of AutomationClientNET.exe becomes disabled, while other buttons get enabled. After successful automation of Notepad you may open its Find dialog, added custom menu item by pressing appropriate buttons of the client application. You may type some text in Notepad and copy it to all clients by pressing client's "Copy Text" button. To test asynchronous action, type some text in Notepad followed by a sentence conclusion sign (".", "?", or "!"). Text typed will be reproduced in an edit box of client application.

Now you may test the sample for several clients. Start a second AutomationClientNET.exe application from your OrdinaryClient directory and press its "Bind to Notepad" large button. After successful binding each client may operate Notepad independently. Both clients get asynchronous events when text typed in Notepad is concluded with ".", "?", or "!".

Finally, test the sample for remote access. For this copy your OrdinaryClient directory to another machine. You will be required to make changes in all configuration files: all AutomationClientNET.exe.config and one Notepad.exe.config. You need to replace "localhost" word with appropriate IP addresses (see How Does Sample Work? paragraph and comments in configuration files themselves). Then start Client-Injector, automate Notepad, run ordinary client on remote machine and bind it to the automated Notepad. Initial binding to remote machine may take some time (could be around 30 sec.). But after the binding was done communication is fast. You will get result similar to one depicted in the beginning of this article (the picture was produced using Remote Desktop Connection to access remote machine).

Discussion

This article is just illustration of WCF services used in process automation. Clearly, the main scope of such an approach is support for local and remote clients with the same code. For enterprise distributed applications usage of WCF services with their variety of transport channels and high configurability without changes in code looks very promising. Suggested mechanism is applicable to different types of automated applications and ways of injection. E. g., managed Browser Helper Objects (BHO) used for browser automation may serve as a host for WCF services to communicate with browser automation clients.

Conclusion

A technique for automation of Windows application with injected objects is presented. A .NET WCF equipped component inside target process allows both local and remote client applications to establish duplex communication with injected objects effectively controlling target process and getting its asynchronous events. WCF service enables usage of the same code for local and remote clients and provides a range of transport channels (HTTP, TCP, Named Pipes and MSMQ) for communication between the clients and component embedded into automated process. The presented technique is particularly useful when automated application is operated by several clients running on local and remote machines.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Israel Israel


  • Nov 2010: Code Project Contests - Windows Azure Apps - Winner
  • Feb 2011: Code Project Contests - Windows Azure Apps - Grand Prize Winner



Comments and Discussions

 
GeneralNot working properly (Throwing message :"Error HRESULT E_FAIL has been returned from a call to a COM component." ) Pin
glitteringsound20-Mar-11 7:37
glitteringsound20-Mar-11 7:37 
GeneralRe: Not working properly (Throwing message :"Error HRESULT E_FAIL has been returned from a call to a COM component." ) Pin
Igor Ladnik21-Mar-11 2:37
professionalIgor Ladnik21-Mar-11 2:37 
GeneralRe: Not working properly (Throwing message :"Error HRESULT E_FAIL has been returned from a call to a COM component." ) [modified] Pin
Igor Ladnik21-Mar-11 5:19
professionalIgor Ladnik21-Mar-11 5:19 
GeneralMy vote of 5 Pin
sam.hill15-Nov-10 18:53
sam.hill15-Nov-10 18:53 
GeneralRe: My vote of 5 Pin
Igor Ladnik29-Nov-10 23:08
professionalIgor Ladnik29-Nov-10 23:08 
GeneralRe: My vote of 5 Pin
dharmik.c3-Dec-12 19:30
dharmik.c3-Dec-12 19:30 
GeneralRe: My vote of 5 Pin
dharmik.c3-Dec-12 19:31
dharmik.c3-Dec-12 19:31 
QuestionGreat article! May I suggest a little fix? Pin
Member 16391315-Dec-08 2:33
Member 16391315-Dec-08 2:33 
AnswerRe: Great article! May I suggest a little fix? Pin
Igor Ladnik15-Dec-08 18:34
professionalIgor Ladnik15-Dec-08 18:34 
GeneralAutomate .Net application Pin
hulinning18-Oct-07 2:15
hulinning18-Oct-07 2:15 
Is that possible to automate and inject into .net applications without using COM?

Thanks
GeneralReally nice article and demo, and I have a question Pin
singtaa7-Jul-07 17:55
singtaa7-Jul-07 17:55 
GeneralRe: Really nice article and demo, and I have a question Pin
Igor Ladnik18-Oct-07 3:52
professionalIgor Ladnik18-Oct-07 3:52 
GeneralExcellent article Pin
George__Washington11-Jun-07 22:15
George__Washington11-Jun-07 22:15 
GeneralRe: Excellent article Pin
Igor Ladnik12-Jun-07 0:13
professionalIgor Ladnik12-Jun-07 0:13 
Generalvery cool Pin
Asghar Panahy15-Mar-07 2:08
Asghar Panahy15-Mar-07 2:08 
GeneralRe: very cool Pin
Igor Ladnik15-Mar-07 3:20
professionalIgor Ladnik15-Mar-07 3:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.