Build A Windows Service Framework

This is part of a series on creating and installing Windows services.

  1. How to Create a Windows Service
  2. How to Install a Windows Service
  3. How to Run a Windows Service from Visual Studio
  4. How to Avoid Windows Service Start-up Timeouts
  5. How to Make a Windows Service Install Itself
  6. How to Handle Windows Service Errors
  7. How to Build a Windows Service Framework

This will be the final episode in our series on Windows Services. In this post, we will review what we have covered in the series and take a look at the reusable infrastructure we have created.

Our initial goal was to create a Windows Service without relying on templates. As we have discussed elsewhere, templates scream “copy-paste” and should be avoided. Most things that can be done with templates can also be done with code reuse. Using our infrastructure, we have no need for templates to create a new service:

class Program
{
    static void Main(string[] args)
    {
        BasicServiceStarter.Run<MyService>("DemoService");
    }
}

class MyService : IDisposable
{
    public void Start()
    {
    }

    public void Dispose()
    {
    }
}

Our framework strips away all of the boilerplate repetitive code (typically generated by templates) needed to get a service going. All our application code needs to do is make one simple call to the framework and provide an implementation of IDisposable that provides application-specific start up and shut down logic (those who have been following the series will notice that this represents a small simplification – we no longer require an implementation of IService and its associated Start method, as the constructor gives the application code enough opportunity to get things rolling). The framework takes care of the rest, including:

  • Error handling
  • Installation
  • Service and console mode

To see how the framework does all that, let’s review the infrastructure code:

public static class BasicServiceStarter
{
    public static void Run<T>(string serviceName) where T : IDisposable, new()
    {
        AppDomain.CurrentDomain.UnhandledException += (s, e) =>
            {
                if (EventLog.SourceExists(serviceName))
                {
                    EventLog.WriteEntry(serviceName,
                        "Fatal Exception : " + Environment.NewLine +
                        e.ExceptionObject, EventLogEntryType.Error);
                }
            };

        if (Environment.UserInteractive)
        {
            var cmd =
                (Environment.GetCommandLineArgs().Skip(1).FirstOrDefault() ?? "")
                .ToLower();
            switch (cmd)
            {
                case "i":
                case "install":
                    Console.WriteLine("Installing {0}", serviceName);
                    BasicServiceInstaller.Install(serviceName);
                    break;
                case "u":
                case "uninstall":
                    Console.WriteLine("Uninstalling {0}", serviceName);
                    BasicServiceInstaller.Uninstall(serviceName);
                    break;
                default:
                    using (var service = new T())
                    {
                        Console.WriteLine(
                            "Running {0}, press any key to stop", serviceName);
                        Console.ReadKey();
                    }
                    break;
            }
        }
        else
        {
            ServiceBase.Run(new BasicService<T> { ServiceName = serviceName });
        }
    }
}

BasicServiceStarter is responsible for determining the mode of operation – install, uninstall, console, or service. It also provides a global error handler. Its “Run” method is the public entry point into the framework and is therefore the place where we can hook in more functionality in the future – allowing all applications based on the framework to reap the benefits without having to do any work.

class BasicService<T> : ServiceBase where T : IDisposable, new()
{
    private IDisposable _service;

    protected override void OnStart(string[] args)
    {
        try
        {
            _service = new T();
        }
        catch
        {
            ExitCode = 1064;
            throw;
        }
    }

    protected override void OnStop()
    {
        _service.Dispose();
    }
}

BasicService provides our hook into the service control manager by implementing ServiceBase. Remember that it is a good idea to wait until the OnStart method is called to do any possibly long-running operations (this is why we wait until this point to instantiate the client class). Also, returning a non-zero exit code in the event of start-up failure will cause the service control manager to relay that information to the user who is trying to start up the service.

static class BasicServiceInstaller
{
    public static void Install(string serviceName)
    {
        CreateInstaller(serviceName).Install(new Hashtable());
    }

    public static void Uninstall(string serviceName)
    {
        CreateInstaller(serviceName).Uninstall(null);
    }

    private static Installer CreateInstaller(string serviceName)
    {
        var installer = new TransactedInstaller();
        installer.Installers.Add(new ServiceInstaller
        {
            ServiceName = serviceName,
            DisplayName = serviceName,
            StartType = ServiceStartMode.Manual
        }); 
        installer.Installers.Add(new ServiceProcessInstaller
        {
            Account = ServiceAccount.LocalSystem
        });
        var installContext = new InstallContext(
            serviceName + ".install.log", null);
        installContext.Parameters["assemblypath"] =
            Assembly.GetEntryAssembly().Location;
        installer.Context = installContext;
        return installer;
    }
}

BasicServiceInstallerhandles our self-install and self-uninstall functionality. This eliminates the need for applications to provide their own Installer. Remember that we need to pass the entry assembly’s location as the installer’s “assemblypath” parameter so that the installer knows where the executable containing the service is located.

We have put a lot of time into demonstrating how to build out service infrastructure, and we realize this isn’t a very typical “DevOps” thing to do. While most “DevOps” seems to focus on taking any old software and automating its operation, here at DevOps on Windows, our goal is to create software that is inherently easy to operate. Consistent and quality application frameworks are a key element of our approach. When all your services are based on a framework like the one above, they will behave in the same predictable way at a high level. They will all install with “i” and uninstall with “u”. They will all log an exception to the event viewer if they fail. They will all return error code 1064 if they fail to start. Making your applications consistent and predictable by utilizing a framework makes life easier on the whole team.

 
Comments

Hi Stuart, thanks for the great article.
I’m constantly struggling with services, and even thought theirs frameworks and libraries out there it does not solve your problems if you use them incorrectly. This article gave me a simple and affective solution and an understanding of how things work behind the scene. I can see the time and effort going into this article and I thank you for this and keep up the excellent material on this site.

Francois Taljaard

Is there a place where I can get the whole example? I know you say we should kill copy-paste, but I’m sure you don’t mean that we have to start rewriting from png files ;)

Thomas, good point. I’ll migrate these screenshots to copy-paste friendly code over the weekend. We’ve moved away from screenshots with our more recent posts, so it makes sense to update our earlier posts as well.

Great. I already rewrote it from the png if you want ;) By the way what is license of the code? I really like it and I would like to extend it with a bit better handling of the OnStop event, and make it into a nice drop-in library published on github etc (sort of “dapper for services”)? The thing is that I’m building similar services very often and just this summer I wrote at least 15 services doing the same (start a service, configure logger, watch a folder and do something every x seconds).

Thomas, my apologies for the long delay. As you can see, we’ve updated all the posts in this series to include copy-paste friendly formatted code. In addition, the code is available on github at https://github.com/devopsonwindows/BasicService. We have made this available under the Apache 2 license, so feel free to fork and modify to your heart’s content.

Leave a Reply