Installing a Topshelf Service using Wix

The idea of this post is not to introduce Topshelf or Wix. If you are here I assume you figured out how cool Topshelf is, you created your service but when it came time to install it using Wix your service is not starting. That’s what happened to me.

The Solution

Basically what you need to do is add an installer class derived from System.Configuration.Install.Installer.

To make it as simple as possible I'm taking the sample from the Topshelf documentation page (topshelf-project.com/documentation/getting-started/) and I'm defining the necessary Installer class so it can be installed from Wix or InstallUtil.exe.

This is the main class that will be run from the Windows Service:

internal class Program
{
    private static void Main(string[] args)
    {
        log4net.Config.XmlConfigurator.Configure();
        HostFactory.Run(
            x =>
                {
                    x.Service<TownCrier>(
                        s =>
                            {
                                s.SetServiceName("tc");
                                s.ConstructUsing(name => new TownCrier());
                                s.WhenStarted(tc => tc.Start());
                                s.WhenStopped(tc => tc.Stop());
                            });
                    x.RunAsLocalSystem();

                    x.SetDescription("Sample Topshelf Host");
                    x.SetDisplayName("TownCrierService");
                    x.SetServiceName("TownCrierService");
                });
    }
}

This is the Service class itself:

public class TownCrier
{
    private readonly Timer _timer;

    public TownCrier()
    {
        ILog log = LogManager.GetLogger(typeof(TownCrier));
        log.Debug("User:"+WindowsIdentity.GetCurrent().Name);
        _timer = new Timer(1000) { AutoReset = true };
        _timer.Elapsed += (sender, eventArgs) => Console.WriteLine("It is {0} an all is well", DateTime.Now);
    }

    public void Start()
    {
        _timer.Start();
    }

    public void Stop()
    {
        _timer.Stop();
    }
}

This is the required installer class.

[RunInstaller(true)]
public class ProjectInstaller : Installer
{
    private ServiceInstaller _serviceInstaller;

    private ServiceProcessInstaller _serviceProcessInstaller;

    public ProjectInstaller()
    {
        this.InitializeComponent();
    }

    private void InitializeComponent()
    {
        this._serviceInstaller = new ServiceInstaller();
        _serviceProcessInstaller = new ServiceProcessInstaller();

        this._serviceProcessInstaller.Account = ServiceAccount.LocalService;

        this._serviceInstaller.DisplayName = "TownCrierService";
        this._serviceInstaller.ServiceName = "TownCrierService";

        this.Installers.AddRange(new Installer[] { this._serviceProcessInstaller, this._serviceInstaller });
    }
}

Notice that the Installer class and the main program class use the same ServiceName, if you don't use the same name your service will not start.

Finally this is a sample Wix installer for the service:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="4bafff3b-b638-4e81-ae2c-97a13c50631d" 
           Name="TownCrierInstaller" 
           Language="1033" 
           Version="1.0.0.0" 
           Manufacturer="TownCrierInstaller" 
           UpgradeCode="57c76289-42b4-40b2-95f0-1652b703cefa">
    
		<Package InstallerVersion="200" Compressed="yes" />

		<Media Id="1" Cabinet="media1.cab" EmbedCab="yes" />

		<Directory Id="TARGETDIR" Name="SourceDir">
			<Directory Id="ProgramFilesFolder">
				<Directory Id="INSTALLLOCATION" Name="TownCrierInstaller" 
                   FileSource="C:\YourPath\TopShelfWix\TownCrierService\bin\Debug\">
          <Component Id="Service" Guid="{B5DC8DF4-1B45-47F4-A2B2-791CEB3E97DF}" >
            <File Id="TCFile" KeyPath="yes" Name="TownCrierService.exe" />
            <File Id="a1ac9979e3f2f48e39252e024a9aa75bb" Name="TownCrierService.exe.config" />
            <File Id="ad345d1dff94646dc81e54e095d1aa7c7" Name="log4net.dll" />
            <File Id="a6f1bc7613d5449e7a3d539422299b580" Name="Topshelf.dll" />
            <ServiceInstall Id="TCInstall"
                     Type="ownProcess"
                     Name="TownCrierService"
                     DisplayName="TownCrierService"
                     Start="demand"
                     Account="yourdomain\youruser"
                     Password="password"
                     ErrorControl="normal" />
            <ServiceControl Id="TCControl"
                     Stop="both"
                     Remove="uninstall"
                     Name="TownCrierService"
                     Wait="no"/>
          </Component>
				</Directory>
			</Directory>
		</Directory>

		<Feature Id="ProductFeature" Title="TownCrierInstaller" Level="1">
			 <ComponentRef Id="Service" /> 		
			<ComponentGroupRef Id="Product.Generated" />
		</Feature>
	</Product>
</Wix> 

Why?

To install a regular (non Topshelf) Windows Service you use InstallUtil.exe. When decompiling it I found out that what it does is scan the service assembly looking for classes derived from the System.Configuration.Install.Installer. Topshelf does have such a class but it’s in the Topshelf assembly which is not the Service assembly itself hence InstallUtil can’t find it.

I'll probably add more information on this issue later, for now I just want to document the solution for me and for anyone else going through the same issue.

I hope this helps.