A reusable Windows service template - part 1

Edit: This open-source template has now been updated and placed in a GitHub repository.

A few years ago I wrote a bunch of C# Windows services that were all based around the same basic template. Recently I decided to extract what I learned from that experience to make a better template.

This Windows service template is designed to:

  • Provide all of the service infrastructure, so that the developer can focus primarily on the real work that the service does.
  • Create a service infrastructure that starts/restarts, monitors, and logs the worker thread where the service's work is occurring.
  • Isolate the service's work from the controlling infrastructure so that any crash is logged properly and doesn't bring down the service. 
  • Have relatively simple and small code, so that it's easy to understand, maintain, and debug.
  • Enable the developer to test and debug the service within Visual Studio.
  • Allow the service to install and uninstall itself from the command-line, without the use of InstallUtil.
  • Avoid any cross-thread interactions that involve polling or "busy" loops.
  • Reduce application-level cross-thread interactions that involve shared memory.
  • Log everything that the service is doing, and especially cross-thread interactions.

This is the first entry in a 3-part series:

  • This part discusses the purpose of the template, along with its start modes - install, uninstall, and debugging.
  • Part 2 describes the core of the service template - a detailed design analysis of the controller and worker threads.
  • Part 3 investigates the thread-safe logging class.

The purpose of this Windows service template is to provide a reusable framework where it's easy for you to insert the code representing the real work that the service should do. The starting point is the Main procedure, where a command-line parameter can be used to install the service programmatically, un-install it programmatically, or run the service within Visual Studio for testing and debugging. If no command-line parameter is passed, the service simply starts normally - this is the normal situation where the service is hosted by the Windows Service Control Manager (SCM).   


_____________________________________________________________________________________________________________________________________ 
public sealed partial class ServiceMain : ServiceBase
{
    // Service startup modes.
    private const string DEBUG = @"/d";                       
    private const string INSTALL = @"/i";             
    private const string UNINSTALL = @"/u";           
 
    // Control how long to wait for environment to stabilise 
    // before restarting the worker thread after it has crashed.
    // This delay is in milliseconds.
    private const string CONFIG_WORKER_RESTART_DELAY = "WorkerThreadRestartDelay";
    private const Int32 DEFAULT_WORKER_RESTART_DELAY = 1000;
 
    // Names for application-level threads.
    private const string THREAD_NAME_CONTROLLER = "Controller";
    private const string THREAD_NAME_WORKER = "Worker";
 
    // Using this to coordinate controller and worker threads rather than polling with Thread.Sleep.
    // http://msmvps.com/blogs/peterritchie/archive/2007/04/26/thread-sleep-is-a-sign-of-a-poorly-designed-program.aspx 
    private static ManualResetEventSlim ReleaseControllerThread = new ManualResetEventSlim(false);
 
    // Backing fields for the ServiceStopRequested property.
    private static readonly object LockFlag = new object();
    private bool m_ServiceStopRequested = false;

_____________________________________________________________________________________________________________________________________

The three constants representing the command-line parameters of the service are shown towards the top of the code fragment above. The way that these parameters are used is shown in the code fragment below.


_____________________________________________________________________________________________________________________________________

 // This is the entry point for this service. 
// This method runs on an SCM thread.
public static void Main(string[] args)
{
    if (Environment.UserInteractive && args.Length > 0)
    {
        switch (args[0])
        {
            // Debug the service as a normal app, presumably within Visual Studio.
            case DEBUG:
                ServiceMain DebugService = new ServiceMain();
                DebugService.OnStart(null);
            break;
            // Install the service programatically.
            case INSTALL:
                ManagedInstallerClass.InstallHelper(new string[] { Assembly.GetExecutingAssembly().Location });
                break;
            // Un-install the service programatically.
            case UNINSTALL:
                ManagedInstallerClass.InstallHelper(new string[] { UNINSTALL, Assembly.GetExecutingAssembly().Location });
                break;
            // We don't understand this request!
            default:
                string message = string.Concat(DEBUG, " to debug service in VS.", Environment.NewLine);
                message += string.Concat(INSTALL, " to install service.", Environment.NewLine);
                message += string.Concat(UNINSTALL, " to un-install service.", Environment.NewLine);
                message += string.Concat("Do not understand the command-line parameter ", args[0]);
                throw new System.NotImplementedException(message);
        }
    }
    // If no startup mode specified, start the service normally.
    else
    {
        ServiceBase[] ServicesToRun = new ServiceBase[] { new ServiceMain() };
        ServiceBase.Run(ServicesToRun);
    }
}
_____________________________________________________________________________________________________________________________________

The template contains three installers - for the service itself, the service's credentials, and the Windows event log installer. The latter defines the location for logging. All of the parameters needed to install the service are held in the service's configuration file.


_____________________________________________________________________________________________________________________________________

 <?xml version="1.0"?>
<configuration>
    <startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0,Profile=Client"/></startup>
    <appSettings>
        <add key="ServiceName" value="ServiceTemplate"/>
        <add key="ServiceDisplayName" value="Service Template"/>
        <add key="ServiceDescription" value="Demo of a generic service template."/>
        <add key="ServiceAccount" value="NetworkService"/>
        <add key="ServiceUserName" value=""/>
        <add key="ServiceUserPassword" value=""/>
        <add key="EventSource" value="Service Template"/>
        <add key="EventLog" value="Service Template"/>
        <add key="WorkerThreadRestartDelay" value="1000"/>
    </appSettings>
</configuration>
_____________________________________________________________________________________________________________________________________

The installers are configured using the config file. If the user account under which the service should be installed is empty, the service is configured to run using the LocalSystem account.


_____________________________________________________________________________________________________________________________________

 [RunInstaller(true)]
public partial class ServiceInstall : Installer
{
    // Config file settings.
    private const string CONFIG_SERVICE_NAME = "ServiceName";
    private const string CONFIG_DISPLAY_NAME = "ServiceDisplayName";
    private const string CONFIG_DESCRIPTION = "ServiceDescription";
    private const string CONFIG_SERVICE_ACCOUNT = "ServiceAccount";
    private const string CONFIG_USER_NAME = "ServiceUserName";
    private const string CONFIG_USER_PASSWORD = "ServiceUserPassword";
    private const string CONFIG_EVENT_SOURCE_NAME = "EventSource";
    private const string CONFIG_EVENT_LOG_NAME = "EventLog";
 
    // Constants for evaluating the account under which the service will run.
    // These correspond to the ServiceAccount enumeration.
    private const string ACCOUNT_LOCAL_SERVICE = "LocalService";
    private const string ACCOUNT_NETWORK_SERVICE = "NetworkService";
    private const string ACCOUNT_LOCAL_SYSTEM = "LocalSystem";
    private const string ACCOUNT_USER = "User";
 
    public ServiceInstall()
    {
        // Service installer - this defines the service's primary properties.
        var serviceInstaller = new ServiceInstaller();
        serviceInstaller = new ServiceInstaller();
        serviceInstaller.ServiceName = ConfigurationManager.AppSettings[CONFIG_SERVICE_NAME];
        serviceInstaller.DisplayName = ConfigurationManager.AppSettings[CONFIG_DISPLAY_NAME];
        serviceInstaller.Description = ConfigurationManager.AppSettings[CONFIG_DESCRIPTION];
        serviceInstaller.StartType = ServiceStartMode.Automatic;
 
        // Service process installer - this defines the service's credentials.
        var serviceProcessInstaller = new ServiceProcessInstaller();
        var serviceAccount = (ConfigurationManager.AppSettings[CONFIG_SERVICE_ACCOUNT]);
        switch (serviceAccount)
        {
            case ACCOUNT_NETWORK_SERVICE:
                serviceProcessInstaller.Account = ServiceAccount.NetworkService;
                serviceProcessInstaller.Username = null;
                serviceProcessInstaller.Password = null;
                break;
            case ACCOUNT_LOCAL_SERVICE:
                serviceProcessInstaller.Account = ServiceAccount.LocalService;
                serviceProcessInstaller.Username = null;
                serviceProcessInstaller.Password = null;
                break;
            case ACCOUNT_LOCAL_SYSTEM:
                serviceProcessInstaller.Account = ServiceAccount.LocalSystem;
                serviceProcessInstaller.Username = null;
                serviceProcessInstaller.Password = null;
                break;
            case ACCOUNT_USER:
                serviceProcessInstaller.Account = ServiceAccount.User;
                serviceProcessInstaller.Username = ConfigurationManager.AppSettings[CONFIG_USER_NAME];
                serviceProcessInstaller.Password = ConfigurationManager.AppSettings[CONFIG_USER_PASSWORD];
                break;
            default:
                serviceProcessInstaller.Account = ServiceAccount.User;
                serviceProcessInstaller.Username = ConfigurationManager.AppSettings[CONFIG_USER_NAME];
                serviceProcessInstaller.Password = ConfigurationManager.AppSettings[CONFIG_USER_PASSWORD];
                break;
        }
 
        // Event log installer - this defines where service activity will be logged.
        var eventLogInstaller = new EventLogInstaller();
        eventLogInstaller.Source = ConfigurationManager.AppSettings[CONFIG_EVENT_SOURCE_NAME];
        eventLogInstaller.Log = ConfigurationManager.AppSettings[CONFIG_EVENT_LOG_NAME];
 
        // Remove the rather poor default event log installer and use our own installers instead.
        serviceInstaller.Installers.Clear();
        this.Installers.AddRange(new Installer[] { serviceProcessInstaller, serviceInstaller, eventLogInstaller });
 
        InitializeComponent();
    }
}
_____________________________________________________________________________________________________________________________________

To install the service, start a command-shell session as an administrator and use the following command line.

To un-install the service, start a command-shell session as an administrator and use the following command line.

In the next entry of this series we'll look at the core of the service template, namely the controller and worker threads along with their interactions.