Windows Services Made Simpler, Part 3

Published on Author Michael

In this series of articles I have been demonstrating a simple approach to making Windows services.  In the previous article I discussed the different components generally involved in writing a service.  I also provided the code for the service host and started working on the service instance.  In this article I’m going to finish up the implementation of the service and demonstrate how all this comes together.

Implementing the Service Instance – OnStop

Implementing OnStop is straightforward. Because the worker thread is looping waiting for the terminate event to be signaled the method simply needs to set the event. When the method returns the SCM assumes the service can be terminated so it is important to ensure that the worker thread is done before returning. This can be done using a simple call to Join. A timeout should probably be provided to ensure the method doesn’t wait too long though.

protected override void OnStop ()
{
    if (m_evtTerminate != null)
    {
        m_evtTerminate.Set();

        //Wait for the thread to terminate
        m_thread.Join(StopTimeout);
    };
}

One thing to keep in mind is that the service worker itself won’t actually know it is being terminated since all the logic is being handled by the service. For some service workers this is fine but many workers will make blocking calls to database or external services. It is often useful to be able to tell the service worker to cancel what it is doing as well. One simple approach to this is to create a CancellationTokenSource that is passed to the service worker. As part of the stop method cancel the token. The service worker is still responsible for checking the cancellation token but it does allow the worker to cancel out of any work when the service is stopped.

Implementing the Service Worker

The service worker provides the business logic of the service.  Therefore the implementation is completely dependent upon the requirements.  In this article the DoWork method simply sleeps for 5 seconds but the actual logic could be anything.  Again, if the worker does any lengthy work then consider implementing cancellation support.  Beyond cancellation, the worker need not know anything about being run as a service.

public class WorkerService
{
    public void DoWork ()
    {
        //TODO: Put real work here
        Thread.Sleep(5000);
    }
}

Final Implementation Details

At this point we have a fully functioning service that interacts with the SCM properly.  The actual work being done is separated from the service host making it easier to test and even be reused outside the service.  But there are a couple of final details that need to be resolved.

The first issue goes back to the service host code that starts the service when running as a console application.  That code calls the Start method but the method is not public.  Instead we have to implement the method ourselves but it is very straightforward.

public void Start ( string[] args )
{
    OnStart(args);
}

The second issue is with the cancellation handler for the console.  The cancellation handler will set the termination event but upon return the console application is terminated.  To cause the console application to remain running we need to set the Cancel property to true like so.

Console.CancelKeyPress += (o,e) => 
{ e.Cancel = true;
    m_evtTerminate.Set();                    
};

The final issue is with the underlying service code.  By default a service writes to the event log using a source that is configured at installation time.  If the source is not configured then an exception will occur when writing to the log.  For default log events this is not an issue but if the service code explicitly tries to write something to the log then it will fail.  To check whether the application is running as a service or not you can use the Environment.IsInteractive property.  Any calls to the event log should be wrapped in a check to ensure the service is running (IsInteractive will be false) before trying to write to the log.

Installing a Service

Once you have a working service you’re going to want to install it.  There are quite a few different approaches to installing a service:

Sc.exe can be used on any service but it just installs the service.  Any event logs or other installation tasks will not be performed.  InstallUtil is the best solution for .NET services as it will install any Installer-based component.  One of these happens to be ServiceInstaller

As an aside, InstallUtil is part of the framework and can be used on any machine with the framework installed.  The easiest way to get to it is to start a Developer Command Prompt with administrative privileges.  The DCP is available from the Visual Studio Start Menu folder. 

Creating an Installer

To create a service installer you need only open the service in Visual Studio (the designer), right click the design surface and select the option to add an installer.  There are actually 4 installers: one for the project, one for the service process (ServiceProcessInstaller) and one for the service (ServiceInstaller).  The project installer is simply a container for the other installers.  Installers can have child installers. This is a useful feature when installing things like event logs or services.  The project installer is the parent of the SPI.

The ServiceProcessInstaller is responsible for installing the service process (the host).  As mentioned in the previous article a host can have any number of services.  The SPI defines the process level properties such as the account to use and any user name or password.  Out of the box it is configured to prompt for a user name and password at installation time but you can change it.

The ServiceInstaller is more interesting as it contains the properties for the service.  Each service instance in a process will have its own installer.  Amongst other things the installer determines when the service starts, the description to show and (most importantly) the service name.  The service name is critical to the installer and, if not done correctly, can cause problems.  The ServiceName used by the installer must exactly match the ServiceName property of the associated service instance.  If they do not then the SCM will not be able to start the service as this is the key used by the SCM to find the appropriate service host and instance. 

Keeping the two values in sync is not that hard but, unfortunately, does not play well with the designer.  My recommendation is to define the service name as a named constant in the service instance class.  Then use the named constant in the installer.  Since the designer will not let you reference named constants this has to be done by editing the project installer’s initialization code.  But by using a named constant you avoid maintenance issues.

//Inside service
public Service1 ()
{
    InitializeComponent();
    ServiceName = Name;
}

public const string Name = "MyService";

//Inside project installer
public ProjectInstaller ()
{
    InitializeComponent();

    serviceInstaller1.ServiceName = Service1.Name;
}

A few final notes about the service installer.  By default a service runs in its own process.  If you are hosting multiple services in the same process then the service type is modified to use a shared process instead. This is to be expected and is fine.  However if you switch from/to a single service to/from multiple services you need to reinstall the service.  If you modify anything related to the service name, process name or any installer properties then you also need to reinstall the service.  Beyond that, changes to the service code do not require that you uninstall and reinstall the service.

The other note on service installer is actually an “issue” with Windows.  Whenever you uninstall a service be sure to stop the service first.  If a service is running when it is uninstalled then it is marked for deletion but not actually removed.  The service won’t be removed until the next reboot.  As such you cannot reinstall or work with the service without rebooting.

Attached is the service code discussed in this article.  Feel free to play around with it by running it as a normal application and as a service.

ServicesMadeSimpler.zip