Continued from “Building a Windows Service – Part 2: Adding a Service to a Console Application

We are going to take this in two steps, first the minimal we need to do to make something *sorta* work, then we are going to get fancy. This post will focus on the first part, making something work. To build a service install there are two choices, you can simply open the service in the designer view and right-click to select “Add Installer”. This really doesn’t do much to benefit you IMO, so we are just going to write one from scratch. The first thing we need is just a basic installer…

[RunInstaller(true)]
public class Installer : System.Configuration.Install.Installer
{
}

During the construction of this installer we need to create and initialize our installers. This is pretty easy to do, we add a few variables to store our child installers and add those to the Installers collection. For a service you need two installers, the first is a ServiceProcessInstaller to define the credentials the service will use. Next we will need to add a ServiceInstaller to define the service name, display name, description, etc. This class is pretty weak in it’s abilities and we will look at extending it in the next post. Until then we’ll just use what we have for a bit.

private readonly ServiceProcessInstaller _installProcess;
private readonly ServiceInstaller _installService;

public Installer()
{
    _installProcess = new ServiceProcessInstaller();
    _installProcess.Account = ServiceAccount.NetworkService;

    _installService = new ServiceInstaller();
    _installService.StartType = ServiceStartMode.Automatic;

    //Remove built-in EventLogInstaller:
    _installService.Installers.Clear(); 

    Installers.Add(_installProcess);
    Installers.Add(_installService);
}

You’ll notice a couple of odd things here. The first is that we did not assign the ServiceName or DisplayName to a value. This is because we actually want this value to come from the command-line during installation. The second is that we clear all child installers of the ServiceInstaller class. This is due to the rather poor default event log installer, we are going to deal with that later.

So how can we install a service without a value for ServiceName? Well, put simply you can’t, we aren’t done yet. We want to retrieve this from the context parameters during install. This is easy to do, we just need to override the various install methods (Install, Uninstall, and Rollback). Let’s first look at the Install routine as it will differ slightly from the Uninstall and Rollback.

public override void Install(IDictionary stateSaver)
{
    if (Context.Parameters.ContainsKey("username"))
        _installProcess.Account = ServiceAccount.User;

    string svcName = Context.Parameters["ServiceName"];
    if (String.IsNullOrEmpty(svcName))
        throw new ArgumentException("Missing required parameter 'ServiceName'.");
    _installService.ServiceName = svcName;
    _installService.DisplayName = svcName;
    stateSaver.Add("ServiceName", svcName);

    //run the install
    base.Install(stateSaver);
}

There you have it, let’s take it apart for a second. The first thing we do is check the parameters for the existence of a ‘username’ argument. This argument (along with the ‘password’ argument) is for specifying the credentials used to run the service. We initialized the ServiceProcessInstaller’s Account setting to be the NetworkService account. Obviously if the user supplied a username they don’t want that account, unfortunately the installer is too dumb to figure that out so we change the account type here to a user-defined account.

The next thing we need from the parameters is our service’s name, the ‘ServiceName’ argument. This isn’t an option for us since we did not define a default (although you could if you desired). Once we have a service name we have to use that weird ‘stateSave’ thing we were given. We must place our service name in this bag in order to retrieve it later during uninstall and rollback. Let’s look at those now…

public override void Rollback(IDictionary savedState)
{
    _installService.ServiceName = (string)savedState["ServiceName"];
    base.Rollback(savedState);
}

public override void Uninstall(IDictionary savedState)
{
    _installService.ServiceName = (string)savedState["ServiceName"];
    base.Uninstall(savedState);
}

We now have something we can install, but we need to provide the service name when installing:

C:\Windows\Microsoft.NET\Framework\v2.0.50727\InstallUtil.exe /ServiceName=foo ServiceTemplate.exe
C:\Windows\Microsoft.NET\Framework\v2.0.50727\InstallUtil.exe /uninstall ServiceTemplate.exe

So now we can install this service, but we can not run it. Why? Because our service that we wrote in the last post requires specific arguments to run as a service, and it must be told what service name it will be responding to. To make all that work we need to seriously overhaul the default service installer.

Continued on “Building a Windows Service – Part 4: Extending the Service Installer

Comments