Contents
This article is organized as follows:
- Introduction
- The Solution
- Implementing the Functionality
- Putting it all together: Using the ServiceInstallerEx class
- Conclusion
Part I: Introduction
I'd like to begin by providing a brief background of services and their capabilities to put the forthcoming solution into context. Windows services are a feature of NT operating systems such as NT 4, Windows 2000 and XP. Services are not available on legacy "desktop" operating systems such as Windows 95, 98, and the short lived Me. They are analogous to the daemon processes that run on UNIX servers and workstations.
Simply put, a service is just another process, except that it conforms to the service API which enables it to run in the background without a user having to explicitly start it. It should be noted, however, that it is possible for a single service process to run multiple services, thereby sharing the same process space. Services can be configured to start up automatically at boot time before the user logs on, which is critical in situations such as "headless" machines that host mission critical applications in isolated server closets. Another useful feature of services is that they can be stopped, paused and resumed locally as well as remotely using the Windows computer management console. This enables system administrators to, for example, shutdown and restart a SQL server database engine.
Prior to the release of Windows 2000, services did not have fault tolerance built into the interface. In other words, if a service generated a fault and died, there was no way to restart the service. What happened was that good developers ended up writing "keep-alive" code using the Win32 Process or PSAPI.dll interfaces to insure that their services were always available. Microsoft duly noted the significance of this deficiency and introduced extensions to the service configuration API that enabled fine grain recovery options. These new extensions, now permitted a service to configure actions to take, should a service fail. These actions include restarting the service, rebooting the machine, running a user specified command, or taking no action at all (the default). Though the computer management console user interface only allows configuration and display of up to three actions, it is a little known fact that a service can actually be configured for any number of recovery actions.
Service Interfaces in .NET
The task of creating services has been greatly eased with the classes in the System.ServiceProcess
namespace in the .NET Framework. Of these, the following classes are of particular interest:
System.ServiceProcess.ServiceController
This class provides a rich and easy to use interface to control and obtain information from a local or remote service. Callers can start, stop, or pause services as well as execute custom commands on the service.
System.ServiceProcess.ServiceProcessInstaller
This class provides a simple mechanism to install one or more services when called from a Windows installer utility such as installutil.exe. The service process installer is easily added to any Windows service project in Visual Studio .NET by right clicking in design view of the service and selecting "Add Installer".
System.ServiceProcess.ServiceInstaller
This class provides a mechanism for installing a single service. Multiple services are installed by having multiple instances of this class in a ServiceProcessInstaller
instance. The ServiceInstaller
class provides properties and configuration information for the particular service.
Deficiencies in the .NET class libraries
Though useful and simple, Microsoft did not provide interfaces to configure the "advanced" service options such as the recovery options. They even failed to provide an interface to set the description for the service. I found this to be a little disappointing since fault tolerance in services is nothing to joke about. Another deficiency with service installations is that there is no way to configure them to start right after installation. In order to start our new service, we would have to:
- Create a setup project and custom action to start the service
- Create a startup process to start the service and instruct the user to run this after installation.
- Instruct the user to restart the machine or tell them to use the management console (NO!). This is not desirable since it is quite unnecessary to restart the entire machine (that could be running mission critical applications) simply to start one itty bitty service.
It certainly would be more pragmatic to be able to configure your service to start right after installation � hands free.
PART II: The Solution
Microsoft is quite aware of folks like me, and did the correct thing by not sealing the ServiceInstaller
class. This allows us to easily extend it by simply deriving from it and adding our own capabilities. For this solution, I created a new class called ServiceInstallerEx
(ala Win32 extensions) and derived all the base class functionality that already exists such as setting dependencies, events etc.
public class ServiceInstallerEx : System.ServiceProcess.ServiceInstaller
In the constructor for the class, I simply register two event handlers for the Committed
event of the base class. When a service is installed, there are many events thrown, however the ones we are mostly concerned with are the AfterInstall
and Committed
events. The AfterInstall
event is fired when the service is initially installed. The Committed
event is fired when the service has been installed and fully committed. Since our steps are explicitly modifying the service configuration, it is best to do our dirty work after the service is fully committed, instead of doing it after the AfterInstall
event. The constructor for our ServiceInstallerEx
class registers two delegates to perform the work after the service has been committed.
public ServiceInstallerEx() : base()
{
FailureActions = new ArrayList();
base.Committed += new InstallEventHandler( this.UpdateServiceConfig );
base.Committed += new InstallEventHandler( this.StartIfNeeded );
logMsgBase = "ServiceInstallerEx : " + base.ServiceName + " : ";
}
The UpdateServiceConfig()
method handles the configuration of the service description and recovery actions, whereas the StartIfNeeded()
method starts the service if so configured through the class properties. Our ServiceInstallerEx
class exposes some properties for the caller as follows:
-
FailureActions
This is a System.Collections.ArrayList
object that holds instances of FailureAction
objects. Each FailureAction
instance represents a successive action to take on service failure. For example, item at slot 0 (first item) would indicate the first failure action and so on. A FailureAction
object has two properties that must be set: the RecoverAction
and the Delay
. The RecoverAction
is an enumerated value that can be set to None
, Restart
, RunCommand
, or Reboot
. The Delay
is a period of time in seconds to wait after the failure before taking the configured action.
-
Description
A string that describes the service.
-
FailCountResetTime
Time specified in seconds. If a failure occurs, this is the time that must elapse with no subsequent failures before the fail count for the service is reset to 0.
-
FailRebootMsg
This is a string to broadcast on the subnet when rebooting the machine. If a recovery action specifies that the system should reboot, then this message will be broadcasted.
-
FailRunCommand
This is a command line string including all arguments as would be passed to a CreateProcess()
call, when a Run Command action is specified.
-
StartOnInstall
This is a Boolean flag (default false
), that indicates we want the service to start right after installation. This should be sparingly used if services are dependent upon other services.
-
StartTimeout
This is a wait timeout value in seconds (default 15), that we will wait for the service to start when the StartOnInstall
flag is set to true
.
PART III: Implementing the functionality
The ChangeServiceConfig2()
method in advapi.dll is used to set the description and failure actions. This method takes essentially a (void *
) that points to a structure or structure array that holds the SERVICE_DESCRIPTION
or SERVICE_FAILURE_ACTIONS
structure. To resolve the two versions, we abstract the calls to the ChangeServiceConfig2()
using the DllImportAttribute
class EntryPoint
property as follows:
[DllImport("advapi32.dll", EntryPoint="ChangeServiceConfig2")]
public static extern bool
ChangeServiceFailureActions( IntPtr hService, int dwInfoLevel,
[ MarshalAs( UnmanagedType.Struct ) ] ref SERVICE_FAILURE_ACTIONS lpInfo );
[DllImport("advapi32.dll", EntryPoint="ChangeServiceConfig2")]
public static extern bool
ChangeServiceDescription( IntPtr hService, int dwInfoLevel,
[ MarshalAs( UnmanagedType.Struct ) ] ref SERVICE_DESCRIPTION lpInfo );
The UpdateServiceConfig()
method of the ServiceInstallerEx
class conforms to the InstallEventHandler
delegate and is registered to run after the service has been committed. There are two issues to take note of in this method:
Issue 1
First is that the Marshal
class in System.Runtime.InteropServices
does not provide a clean way to marshal arrays of structures, but does provide methods to marshal arrays of value types in the .NET common type system. Since the SC_ACTION
Win32 structure is merely two integers that are packed sequentially, we, in essence, cheat here by transposing the array of structures into an array of integers twice the size.
int[] actions = new int[numActions*2];
int currInd = 0;
foreach( FailureAction fa in FailureActions ){
actions[currInd] = (int)fa.Type;
actions[++currInd] = fa.Delay;
currInd++;
}
tmpBuf = Marshal.AllocHGlobal( numActions*8 );
Marshal.Copy( actions, 0, tmpBuf, numActions*2 );
SERVICE_FAILURE_ACTIONS sfa = new SERVICE_FAILURE_ACTIONS();
sfa.lpsaActions=tmpBuf.ToInt32();
. . .
bool rslt = ChangeServiceFailureActions( svcHndl,
SERVICE_CONFIG_FAILURE_ACTIONS,ref sfa );
Issue 2
The second thing to note is the issue of a reboot action. Microsoft has implemented security features that require code to obtain special privileges to perform major system functions such as shutdowns and reboots. The ChangeServiceConfig2()
function we use for our service configuration, requires the caller to have Shutdown Privileges when setting a service failure action to Reboot. To do this, we follow Microsoft's guidance on doing this but implement it using interop.
if( needShutdownPrivilege ){
rslt = this.GrandShutdownPrivilege();
if( !rslt ) return;
}
The GrantShutdownPrivilege
method is privately scoped and not accessible to the caller. It does the interop work to grant shutdown privileges to the user. This method is called assuming the calling process will terminate soon after installation and therefore will not need to explicitly revoke the privilege.
I placed all the meaningful code in a try
-catch
-finally
block to clean up handles, release locks, and free memory. To facilitate debugging any problems, I added a primitive message logging method to log to console and the system Application event log.
Starting the service upon installation
This is the easiest part of it all. We added a method StartIfNeeded()
to our ServiceInstallerEx
class that conforms to the InstallEventHandler
delegate signature that is responsible for starting the service upon installation. If the StartOnInstall
property is set to true
, this method starts the service using the ServiceController
class in the System.ServiceProcess
namespace. The timeout to wait for the service start is also configurable by the StartTimeout
property that has a default value of 15 seconds.
ServiceController sc = new ServiceController( base.ServiceName );
sc.Start();
sc.WaitForStatus( ServiceControllerStatus.Running, waitTo );
sc.Close();
PART IV: Putting it all together
The ServiceInstallerEx
class is provided by the Verifide.ServiceUtils.dll library. Once this has been built, it is a trivial task to use the extension features as follows:
Step 1: Add an installer to the service project
After creating a Windows Service application in Visual Studio .NET, open the service in design view, right click, and select "Add Installer". This adds an installer to your project and places fully functional template code for the service installation. You will notice that there are two objects created for you. A ServiceProcessInstaller
, and a ServiceInstaller
. The ServiceProcessInstaller
class controls the process properties such as the logon account (I suggest setting the account to LocalSystem unless otherwise required). The ServiceInstaller
instance, on the other hand, controls properties such as the startup type (Automatic, Manual, Disabled) and dependencies on other services.
Step 2: Use ServiceInstallerEx instead of the ServiceInstaller instance
To use the extension class, you must first add a reference to the Verifide.ServiceUtils.dll through Solution Explorer. Then you should include the namespace with the using
statement to use the un-elongated names for classes such as FailureAction
and RecoverAction
.
Edit the ProjectInstaller.cs file that was created in step 1. Change the code to use the Verifide.ServiceUtils.ServiceInstallerEx
class instead of the System.ServiceProcess.ServiceInstaller
class. This needs to be changed in two places. First in the declaration of the ServiceInstaller
variable and second when the installer is instantiated in the "Component Designer generated code" region. Worth mentioning here is that all code in the "Component Designer generated code" region is dynamically generated. If you open the service installer in design mode and then view the code, you will notice that all your changes are lost. Therefore, do not do anything more in this region other than changing the type as required. (The design view generated code does not change the type.)
public class ProjectInstaller : System.Configuration.Install.Installer
{
private System.ServiceProcess.ServiceProcessInstaller
serviceProcessInstaller1;
private Verifide.ServiceUtils.ServiceInstallerEx serviceInstaller1;
.
#region Component Designer generated code
private void InitializeComponent()
{
this.serviceProcessInstaller1 =
new System.ServiceProcess.ServiceProcessInstaller();
this.serviceInstaller1 = new Verifide.ServiceUtils.ServiceInstallerEx();
Step 3: There is no step three
Well, I lied, there is but it's easy. Add code to the constructor for the ProjectInstaller
class to configure the options and you're done. Following is all that's left for the user. Of course, if you choose not to set any of the extension properties, you need not do anything else; since we inherited from the ServiceInstaller
class, we're pretty transparent.
public ProjectInstaller()
{
InitializeComponent();
serviceInstaller1.Description = "Look Smeagol! Its the Precious!";
serviceInstaller1.FailCountResetTime = 60*60*24*4;
serviceInstaller1.FailRebootMsg = "Whitney Houston! We have a problem" ;
serviceInstaller1.FailureActions.Add(
new FailureAction( RecoverAction.Restart,60000) );
serviceInstaller1.FailureActions.Add(
new FailureAction( RecoverAction.RunCommand, 2000 ) );
serviceInstaller1.FailureActions.Add(
new FailureAction( RecoverAction.None, 3000 ) );
serviceInstaller1.StartOnInstall = true;
}
Step 4: Install the service
From the bin directory of the service application you generate, install the service using the installutil.exe command. Run the following commands from a Visual Studio .NET command prompt (Start->Programs->Visual Studio->Tools->Command Prompt) to ensure the installutil program is in your path.
To install a service: installutil servicename.exe
To uninstall: installutil /u servicename.exe
Note that you must either specify the full path of the service exe, or you have to be in the directory where the service resides, to run the installutil program. Go to the Windows management console (Right click on My Computer->Manage->Services and Applications->Services). To view the recovery options, right click on the service in the list, select Properties, and then select the Recovery tab.
Part V: Conclusion
After having used .NET for over two years now, I have come to the conclusion that the platform surely makes a lot of tasks very easy. However, there is a lot of fine-grain functionality that is not exposed by the framework. For those of us that have programmed at, shall I dare to say, orthodox levels, these "advanced" features are nothing to fear. We've done them and most importantly are aware of them and the purpose they serve.
Services are an integral part of distributed and business platform development. Microsoft, in my opinion, has done a great job in simplifying the creation, yet more importantly the management of services. However, from sheer experience, the advances in Windows 2000 with the recovery options have personally blessed me with many nights of good sleep. I was quite disappointed that fault tolerance in services was not a priority in the development of the framework class libraries.
In implementing the solution, I chose to provide the simplest interface that would allow the developer to maintain the consistency with the FCL names and usages. It relies on the Win32 API to fill the void in the FCL. Aaah Win32, the ole high-performance, low-carb, low-fat, bread and butter that got me out of many a jam. Win32 interop is a great mechanism to get the fine grain control over the "advanced" features, however, must be used with caution. Microsoft will eventually obsolete this API (though they'll probably use it internally) and with it, the wealth of features that didn't make it to the .NET FCL will forever be gone.