Yet Another Service Wrapper - The Ultimate Service

by Jon Wojtowicz

I recently had a requirement to wrap executables and libraries so they would run as a service. My specific requirements were the following, in this context a task is defined as any method set to run within the service.

  • Allows for impersonation so each code set can run under a different user account.
  • Has the ability to run multiple separate tasks under a single instance of the service. Each task must run under a separate thread to allow multiple threads of the same task to be running concurrently.
  • Supports two types of tasks, polling (fired after a short interval) and scheduled (fired once per day at a fixed time).
  • Tasks are specified in a configuration file.
  • Must support class creation and method parameters. This includes support for arrays as parameters.
  • Must support static and instance methods.
  • Must support executing methods in executables as well as libraries with minimal to no change to existing code.
  • Each task must be able to use its own configuration file.
  • Changes to a task's files will cause the task to reload.

After searching the web for such a wrapper I could not find one that would satisfy all of these requirements. I did find many examples of service wrappers but while each had various pieces of the functionality I needed none had all the requirements. The biggest drawback I found in previous design was forcing the user to use an interface. Unfortunately I could not use this pattern due to the requirement of minimizing any changes to existing code. What I needed was a service that could execute any arbitrary code.



Overview

The Universal Service provides a service wrapper for executing methods within .Net assemblies. The assemblies can be either a library (dll) or executable (exe). The methods can be either static or instance. Static methods do not create an instance of the containing class while the instance methods require an instance of the class be created. Class creation and method parameters are supported. This should be limited to primitive data types and strings to prevent casting errors. Arrays of primitives are supported for creation and method parameters. The return value from the task method is ignored and output parameters are not supported.

The service creates each task in a separate application domain. This allows for each task to have a separate application configuration file. The also allows the task to be reloaded if any of the task files are modified. The task assemblies are shadow copied and are not locked during execution.

The service requires that each task's files be set up in a sub directory of the service executable location. This allows for easy path resolution when loading assemblies. The files are monitored by comparing file modified timestamps. All the files in the task's subdirectory are monitored and a change to any file will cause the task to be stopped and reloaded. Multiple tasks can share the same subdirectory and yet have differing application configuration files. A change to any of the configuration files in this shared scenario will cause all the tasks sharing the directory to stop and reload.

The service stops a task by setting a stop flag inside the task. This allows the task to complete its current processing prior to shutting down. This process is used when reloading a task due to file changes and when the service is stopped. This may cause an unusually long shutdown time for the service if a task is long running.

Launching a task is based on an internal timer. A polling task will launch if the current time span from the last run to the current time is greater than or equal to the polling interval set for the task. A scheduled task will execute if the current time is equal to the scheduled time and at least 23 hours have elapsed since the last execution. This allows the task to be run once per day. Future modifications may allow the running on specific days of the week only but the current version does not support this.

Each task is executed on a separate background thread within the service. This allows the tasks to run using a separate thread identity from the main process thread as well running the tasks concurrently. The identity for a task is specified providing credentials in the configuration.

The following is an internal representation of the service with running tasks.

Configuration

The Universal service retrieves tasks information from its application configuration file. A custom configuration section handler, TaskSectionHandler, is used to determine the tasks. A task is specified using the schema as follows.

Credential Type

<xs:complexType name="Credential">
<xs:attribute name="UserName" type="xs:string" use="required"/>
<xs:attribute name="Password" type="xs:string" use="required"/>
</xs:complexType>

The Credential type specifies the secondary credential name to extract and use for executing the task. The KeyName attribute specified the secondary credential name as specified in the Credential Utility.

ParameterValue Type

<xs:complexType name="ParameterValue">
<xs:attribute name="Value" use="required" type="xs:string"/>
</xs:complexType>

The ParameterValue type contains the value for a specified parameter. The Value attribute contains the actual value in a string representation.

ParameterType Type

<xs:complexType name="ParameterType">
<xs:attribute name="TypeName" use="required" type="xs:string"/>
</xs:complexType>

The ParameterType type specifies the actual type of the parameter. The TypeName attribute contains the actual runtime type of the parameter.

Char Type

<xs:simpleType name="Char">
<xs:restriction base="xs:string">
<xs:length value="1" />
</xs:restriction>
</xs:simpleType>

The Char type specifies a string composed of a single character.

Parameter Type

<xs:complexType name="Parameter">
<xs:sequence>
<xs:element name="Type" type="ParameterType"/>
<xs:element name="ParameterValue" type="ParameterValue"/>
</xs:sequence>
<xs:attribute name="IsArray" type="xs:boolean" default="false" use="optional"/>
<xs:attribute name="Separator" type="Char" default="|" use="optional"/>
</xs:complexType>

The Parameter type specifies a parameter for use as either a method or instance creation parameter. This type contains a Type element which is a ParameterType and a ParameterValue element which is a ParameterValue type. These two elements specify the parameter's runtime type and the corresponding value. The IsArray attribute is used to specify if the type is an array type. It is optional and defaults to false. If the IsArray attribute is true then the Separator attribute is significant. The Separator attribute is optional and defaults to the pipe ( | ) symbol. This attribute is ignored if the IsArray attribute is false.

Parameters Type

<xs:complexType name="Parameters">
<xs:sequence>
<xs:element name="Parameter" type="Parameter" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>

The Parameters type is a collection of Parameter elements. It may be empty.

TaskData Type

<xs:complexType name="TaskData">
<xs:sequence>
<xs:element name="TaskCredential" type="Credential" minOccurs="0" maxOccurs="1"/>
<xs:element name="MethodParameters" type="Parameters"/>
<xs:element name="CreationParameters" type="Parameters" />
</xs:sequence>
<xs:attribute name="TaskName" type="xs:string" use="required" />
<xs:attribute name="BinPath" type="xs:string" use="required"/>
<xs:attribute name="AssemblyName" type="xs:string" use="required"/>
<xs:attribute name="ClassName" type="xs:string" use="required"/>
<xs:attribute name="MethodName" type="xs:string" use="required"</>
<xs:attribute name="ConfigFileName" type="xs:string" use="required"/>
<xs:attribute name="Hour" type="xs:string" use="required"/>
<xs:attribute name="Minute" type="xs:string" use="required"/>
<xs:attribute name="Second" type="xs:string" use="required"/>
<xs:attribute name="IsStaticMethod" type="xs:boolean" use="required"/>
</xs:complexType>

The TaskData type contains the information for a single task. It is comprised of the following:

An optional TaskCredential element of type Credential. This is used when task thread impersonation is required. If this element is not present the task will run under the service account.

A required MethodParameters element of type Parameters. This is a collection of the parameters required to run the task.

A required CreationParameters element of type Parameters. This is a collection of the parameters to use for creating an instance of the class containing the method to execute. For static methods, this element is ignored but the values are still parsed.

A TaskName attribute which provides a friendly name for identifying the task. This will also be used for the name of the application domain containing the task. If the TaskCredential element is specified the TaskName value will be used as the application name when retrieving the credentials.

A BinPath attribute which specifies the name of the subdirectory to the files and assemblies required by the tasks to execute.

An AssemblyName attribute which specifies the name of the assembly containing the method to execute. The assembly must be placed in the subdirectory specified in the BinPath attribute.

A ClassName attribute is used to specify the fully qualified name of the class containing the method to execute.

A MethodName attribute which is used to specify the name of the method to execute.

A ConfigFileName attribute which can be used to specify the name of the configuration file to load in the tasks application domain as its application configuration file. The specified file must be placed in the subdirectory specified by the BinPath.

The following are used to specify the time interval between task executions (polling) or the time of day for execution (scheduled).

Hour attribute which specifies the hour.

Minute attribute which specifies the minutes.

Second attribute which specifies the seconds.

An IsStaticMethod attribute which indicates whether the method specified is static.

All attributes are required.

TaskDataCollection Type

<xs:complexType name="TaskDataCollection">
<xs:choice< minOccurs="0" maxOccurs="unbounded">
<xs:element name="Polling" type="TaskData"/>
<xs:element name="Scheduled" type="TaskData"/>
</xs:choice>
</xs:complexType>

The TaskDataCollection type is a collection of TaskData types. It can contain any combination of Polling elements (tasks) and Scheduled elements (tasks). This type specifies the final format that must be placed in the application configuration file of the service.

Wrapper Configuration

The service wrapper itself has several configurable properties as listed below.

PulseInterval - This is the time in milliseconds between sending run signals to the tasks. The default is 1000 (1 sec)

SuppressFileCheck - This is a Boolean value indicating whether changes to the task files should force the task to reload. The default is false.

FileCheckInterval - This is the time in milliseconds between checks to see if the task files have changed. The default is 60000 (1 minute).

TaskThreadSleepInterval - This is the time in milliseconds that the background task thread sleeps between run checks while in a wait state (in between run times). The default is 250 ms.

These values allow tuning of the CPU utilization as the service is running. All of these activities require some CPU time which can be minimized by setting the values appropriately. As an example if the polling is set for every 10 minutes we can set the PulseInterval to 60000 ms with a TaskThreadSleepInterval of 30000 ms. This would provide a maximum time span between runs of 11.5 minutes which would not be unreasonable on a 10 minute poll. As a contrast if the polling is set to 30 sec then we could set the PulseInterval to 1000 ms and the TaskThreadSleepInterval to 500 ms. This would provide a maximum of 31.5 sec between runs.

The Wrapper service also needs a subdirectory called CopyBin. This is the location for the shadow copied assemblies. The directory will be created by the service installer. For debugging it must be created manually. The service account must be granted full control of this directory for the shadow copying.

Usage Scenarios

Static Polling Task

Description:

A user wants to create a polling task that will run the sample assembly under the service. This will run under the current service account. This will require launching the DoSomethingStatic method in the SampleAssembly assembly which takes a string as its single parameter.  The requirements are as follows:

Task Name: Task1
Assembly Name: SampleAssembly
Class Name: SampleAssembly.ParameterInvokeTest
Configuration File Name: App.Config
Method Name: (static) DoSomethingStatic (string)
Polling Interval: 5 seconds
Method Parameters: "Method Data"
Creation Parameters: none
All of the supporting assemblies will be placed in a subdirectory called Bin1.

The configuration for the described method is the following:

<Polling TaskName="Task1" BinPath="Bin1" AssemblyName="SampleAssembly" ClassName="SampleAssembly.ParameterInvokeTest" MethodName="DoSomethingStatic" IsStaticMethod="true" ConfigFileName="App.Config" Hour="0" Minute="0" Second="5">
<MethodParameters >
<Parameter>
<Type TypeName="System.String" />
<ParameterValue Value="Creation Data" />
</Parameter>
</MethodParameters>
<CreationParameters>
</CreationParameters>
</Polling>

Shared Task Sub Directory

The user wants to extend the previous scenario to include an additional polling task using the same assemblies and method with different method parameters. The new task must also run under a different account.

Two tasks must be created, the second task will specify the same information but will have a different task name and method parameters. The second task will also include an account location for the impersonation. This will result in the following configuration entries:

<Polling TaskName="Task1" BinPath="Bin1" AssemblyName="SampleAssembly" ClassName="SampleAssembly.ParameterInvokeTest" MethodName="DoSomethingStatic" IsStaticMethod="true" ConfigFileName="App.Config" Hour="0" Minute="0" Second="5">
<MethodParameters >
<Parameter>
<Type TypeName="System.String" />
<ParameterValue Value="Creation Data" />
</Parameter>
</MethodParameters>
<CreationParameters>
</CreationParameters>
</Polling>
<Polling TaskName="Task2" BinPath="Bin1" AssemblyName="SampleAssembly" ClassName="SampleAssembly.ParameterInvokeTest" MethodName="DoSomethingStatic" IsStaticMethod="true" ConfigFileName="App.Config" Hour="0" Minute="0" Second="5">
<TaskCredential UserName="Persephone\TestUser" Password="testuser" />
<MethodParameters >
<Parameter>
<Type TypeName="System.String" />
<ParameterValue Value="Some Other Creation Data" />
</Parameter>
</MethodParameters>
<CreationParameters>
</CreationParameters>
</Polling>

Instance Polling Task

A user wants to create a polling task that will run the SampleAssembly under the service. This will run under the current service account. This will require launching the DoSomething method in the SampleAssembly.dll assembly which takes a string array as its single parameter.  The requirements are as follows:

Task Name: Task3
Assembly Name: SampleAssembly
class="clsBodyText">Class Name: SampleAssembly.InvokeTest
Configuration File Name: App.config
Method Name: DoSomething (string[])
Polling Interval: 5 seconds
Method Parameters: "New Data", "Some Data"
Creation Parameters: none
All of the supporting assemblies will be placed in a subdirectory called Bin2.

The configuration for the described method is the following:

<Polling TaskName="Task3" BinPath="Bin2" AssemblyName="SampleAssembly" ClassName="SampleAssembly.InvokeTest" MethodName="DoSomething" IsStaticMethod="false" ConfigFileName="App.Config"
Hour="0" Minute="0" Second="5">
<MethodParameters >
<Parameter IsArray="true" Separator="|" >
<Type TypeName="System.String[]" />
<ParameterValue Value="New Data|Some Data" />
</Parameter>
</MethodParameters>
<CreationParameters>
</CreationParameters>
</Polling>

Working with the Solution

The application was written in VS 2003/.Net 1.1. The solution file provided is broken into multiple projects. The ServiceWrapper project is the main executable for the code. When built in Debug it will launch as a Windows application and continue to run until the Close button is pressed. When built in Release mode it will compile into a service and must be installed prior to execution. The TaskManagment is the library that contains the code that actually constructs and runs the tasks. These two libraries comprise the actual service itself.

When the application starts up it opens a remoting channel to view inside the service to allow limited access to the tasks. The tasks can be viewed, stopped and started via remoting. The RemoteTest application is a Windows application that allows connecting to an instance of the ServiceWrapper and peek inside at the tasks. This will work in either Debug or Release as both compilations of the ServiceWrapper open the same remoting channel. The default port is 12321 and must be unique for each instance of the service running on a machine.

The solution file also contains two sample libraries to test the service, SampleAssembly and SampleLibrary2. These libraries simply write data to the console while they are running so you can see some of the data such as the assembly base paths, method and creational data and the current user.

When testing the code be sure to modify the TaskCredential element in the configuration file to an actual user the test system. As an alternative it can be commented out for testing.

I have successfully run the service on Windows 2000, Windows XP and Windows 2003. On Windows 2000 the service must run under the Local System account if impersonation is used. The Impersonation class uses the LogonUser API call which require elevated permission on Windows 2000. On the other versions of Windows no special account is required. The account the service runs under requires full control of the CopyBin folder under the location of the service. This folder is the shadow copy folder and is created by the service installer if it does not exist. Of course any impersonated user must have read permissions on the sub folders containing the task assemblies.

A configuration file is required for the installation with the InstallUtil utility. This configuration file provides the name that will be used when installing the service. The appSettings in the service's configuration file must have the same values for the service to run properly. This allows multiple installations of the service to run simultaneously on a single server. The remoting port must be unique to each instance on a server as well.

The service itself writes to the Application event log and outputs quite a bit of data. After starting the service you can generally see any errors in the event log with the Source being the name of the service instance. This is very helpful when trying to debug issues with the service.

Security Consideration

Care must be taken in granting permissions to where the service and executing code is located. Also the account the service is running under should have the least amount of privileges necessary to run the service. This service can run just about any arbitrary .Net code specified in the configuration file. Granting the service account too many permissions and/or opening up the location can cause serious security issues. It is better to use the impersonation and grant the secondary account the permissions that the actual task code will need to execute.

The current configuration for the credentials is not secure! This is for testing purposes only. The password needs to be at a minimum encrypted to prevent it from being compromised. This does require modifying the code in the TaskData class. My approach was to store the credentials encrypted in the registry and use my encryption library to read them back out. Your approach will vary depending on your particular requirements.

Have fun with the code!

Download the Solution File that accompanies this article

Jon Wojtowicz is a C# MVP and a Systems Analyst at a large insurance company in Chattanooga, TN where he currently provides developer support and internal training. He has worked as a consultant working with Microsoft Technologies. This includes ASP, COM, VB6 and .Net, both C# and VB.Net since Beta 1. He has been an MCSD since 1999 and an MCT since 2000. Prior to getting a degree in Computer science he worked as a process engineer focusing on process automation, programmable controllers and equipment installations. In his spare time he likes woodworking and gardening.
Article Discussion: