Sunday, September 26, 2004

Installing Windows Services (Created with .NET) with WIX

This is a pretty obscure topic and there is not enough literature on this. Installing a .NET Service is a fairly simple task. The Visual Studio.NET interface allows you to add the System.Configuration.Installer class to your assembly that enables managed installations. Developers often test their services by using the installutil.exe command line tool. But this tool is not the most appropriate for packaging as it shows an ugly command window during installation, which no setup developer would desire. Microsoft, includes a DLL named installutillib.dll with a single MSI entry point function named ManagedInstall. This function can be called via a MSI custom action to handle the four overridable functions exposed by the Installer Class. I am an InstallShield X user and installing .NET service is as easy as setting a property for the component from the IDE. I was a little lost when I wanted to achieve the same with WIX. Although, I knew that I could create these custom actions myself, I was hunting for a way by which I could do it in an easier fashion with WIX. After hours of searching the WiX.chm file, I decided to get on with it myself. I still am not sure if its hidden somewhere in the Wix.chm file.

I was a little skeptical about this and hence started of with an empty .NET enabled Windows Service that does nothing. I added the Installer class to the service and built it. I was too lazy to change the name. So my service was just called Service1 as christened by VS.NET 2003. Once we have our service executable ready, we have to create a little configuration file in XML that specifies the supported frameworks. I called it the IUConfig.XML For people wondering what IU stands for, it is short for InstallUtil <smile/>. The file is fairly simple and it goes something like this.

<?xml version="1.0"?>
<configuration>
<startup>
<supportedRuntime version="v1.1.4322"/>
</startup>
</configuration>


I put my InstallUtilLib.dll, FirstWindowsService.exe (My Windows Service) and IUConfig.xml in a folder called src. You can find the InstallUtilLib.dll in your [WindowsFolder]\Microsoft.NET\Framework\v1.1.4322\ directory. Once you have these three files, its time to start coding the WXS file. Again, just for the sake of simplicity, I am going to install only this service and nothing else. So here is my WXS file. As you can see it is not much. It has only a feature with one component, containing the service executable and the iuconfig.xml file. I have added the InstallUtilLib.dll as a binary.

<?xml version='1.0'?>
<Wix xmlns='http://schemas.microsoft.com/wix/2003/01/wi'>
<Product Id='F47A6F48-86C1-47A8-B404-35656C908BEB' Name='DotNetService' Language='1033' Version='1.0.0.0' Manufacturer='Vagmi' UpgradeCode='5BDA92CF-5D2B-4638-8550-4B8BE5BA8F24'>

<Package Id='????????-????-????-????-????????????' Description='Dot Net Service' Comments='Creating a .NET service' Manufacturer='Vagmi' InstallerVersion='200' Compressed='yes'/>
<Media Id='1' Cabinet='dotnet.cab' EmbedCab='yes' />

<Directory Id='TARGETDIR' Name='SourceDir'>
<Directory Id='ProgramFilesFolder' Name='PFiles'>
<Directory Id='DOTNETSERVICE' Name='DotNet' LongName='DotNetService'>
<Component Id='TheService' Guid='FF15180D-B296-4F30-9385-0F15B8ACC1FF'>
<File Id='WindowsService' Name='Firstw~1.exe' LongName='FirstWindowsService.exe' KeyPath='yes' DiskId='1' src='src\FirstWindowsService.exe' />
<File Id='ConfigFile' Name='IuConfig.xml' LongName='IuConfig.xml' DiskId='1' src='src\IuConfig.xml' CompanionFile='WindowsService'/>
</Component>
</Directory>
</Directory>
</Directory>

<Feature Id='TheOnlyFeature' Description='Feature contains the single component' Level='1'>
<ComponentRef Id='TheService'/>
</Feature>

<!-- Including the InstallUtilLib.dll. This file does all the magic of installing the services. -->
<Binary Id='InstallUtil' src='src\InstallUtilLib.dll' />
</Product>
</Wix>


To perform a managed install of the service, we need four custom actions - two deferred custom actions for installing and uninstalling, one commit custom action and one rollback custom action. As these custom actions execute in the higher security context, we need to pass data to these custom actions using four separate 'Set Property (Type 51)' custom actions. The ManagedInstall function expects the following parameters. The exact functionality of the parameters is still a mystery to me and I have to yet reasearch on it. But for now, we would take this for granted.

/installtype=notransaction /action=(install/uninstall/commit/rollback) /LogFile= "PathTo\Assembly" "PathTo\iuconfig.xml"

So the code for custom actions would look something like this.

<CustomAction Id='InstallServiceSetProp' Property='InstallService' Value='/installtype=notransaction /action=install /LogFile= "[#WindowsService]" "[DOTNETSERVICE]iuconfig.xml"'/>
<CustomAction Id='InstallService' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='deferred' />
<CustomAction Id='UnInstallServiceSetProp' Property='UnInstallService' Value='/installtype=notransaction /action=uninstall /LogFile= "[#WindowsService]" "[DOTNETSERVICE]iuconfig.xml"'/>
<CustomAction Id='UnInstallService' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='deferred' />
<CustomAction Id='CommitServiceSetProp' Property='CommitService' Value='/installtype=notransaction /action=commit /LogFile= "[#WindowsService]" "[DOTNETSERVICE]iuconfig.xml"'/>
<CustomAction Id='CommitService' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='commit' />
<CustomAction Id='RollbackServiceSetProp' Property='RollbackService' Value='/installtype=notransaction /action=rollback /LogFile= "[#WindowsService]" "[DOTNETSERVICE]iuconfig.xml"'/>
<CustomAction Id='RollbackService' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='rollback' />


You would not have to sequence these custom actions such that the uninstall custom actions run before the RemoveFiles action, and install, rollback & commit custom actions are scheduled after the InstallFiles action in the same order. So your <InstallExecuteSequence> would look something like this.

<InstallExecuteSequence>
<Custom Action='UnInstallServiceSetProp' After='MsiUnpublishAssemblies'>$TheService=2</Custom>
<Custom Action='UnInstallService' After='UnInstallServiceSetProp'>$TheService=2</Custom>
<Custom Action='InstallServiceSetProp' After='StartServices'>$TheService>2</Custom>
<Custom Action='InstallService' After='InstallServiceSetProp'>$TheService>2</Custom>
<Custom Action='RollbackServiceSetProp' After='InstallService'>$TheService>2</Custom>
<Custom Action='RollbackService' After='RollbackServiceSetProp'>$TheService>2</Custom>
<Custom Action='CommitServiceSetProp' After='RollbackService'>$TheService>2</Custom>
<Custom Action='CommitService' After='CommitServiceSetProp'>$TheService>2</Custom>
</InstallExecuteSequence>


Putting all this together, we would have a file like this.

<?xml version='1.0'?>
<Wix xmlns='http://schemas.microsoft.com/wix/2003/01/wi'>
<Product Id='F47A6F48-86C1-47A8-B404-35656C908BEB' Name='DotNetService' Language='1033' Version='1.0.0.0' Manufacturer='Vagmi' UpgradeCode='5BDA92CF-5D2B-4638-8550-4B8BE5BA8F24'>

<Package Id='????????-????-????-????-????????????' Description='Dot Net Service' Comments='Creating a .NET service' Manufacturer='Vagmi' InstallerVersion='200' Compressed='yes'/>
<Media Id='1' Cabinet='dotnet.cab' EmbedCab='yes' />

<Directory Id='TARGETDIR' Name='SourceDir'>
<Directory Id='ProgramFilesFolder' Name='PFiles'>
<Directory Id='DOTNETSERVICE' Name='DotNet' LongName='DotNetService'>
<Component Id='TheService' Guid='FF15180D-B296-4F30-9385-0F15B8ACC1FF'>
<File Id='WindowsService' Name='Firstw~1.exe' LongName='FirstWindowsService.exe' KeyPath='yes' DiskId='1' src='src\FirstWindowsService.exe' />
<File Id='ConfigFile' Name='IuConfig.xml' LongName='IuConfig.xml' DiskId='1' src='src\IuConfig.xml' CompanionFile='WindowsService'/>
</Component>
</Directory>
</Directory>
</Directory>

<Feature Id='TheOnlyFeature' Description='Feature contains the single component' Level='1'>
<ComponentRef Id='TheService'/>
</Feature>

<!-- Including the InstallUtilLib.dll. This file does all the magic of installing the services. -->
<Binary Id='InstallUtil' src='src\InstallUtilLib.dll' />

<!--Write custom actions to install, uninstall, commit and rollback the changes-->

<CustomAction Id='InstallServiceSetProp' Property='InstallService' Value='/installtype=notransaction /action=install /LogFile= "[#WindowsService]" "[DOTNETSERVICE]iuconfig.xml"'/>
<CustomAction Id='InstallService' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='deferred' />

<CustomAction Id='UnInstallServiceSetProp' Property='UnInstallService' Value='/installtype=notransaction /action=uninstall /LogFile= "[#WindowsService]" "[DOTNETSERVICE]iuconfig.xml"'/>
<CustomAction Id='UnInstallService' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='deferred' />

<CustomAction Id='CommitServiceSetProp' Property='CommitService' Value='/installtype=notransaction /action=commit /LogFile= "[#WindowsService]" "[DOTNETSERVICE]iuconfig.xml"'/>
<CustomAction Id='CommitService' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='commit' />

<CustomAction Id='RollbackServiceSetProp' Property='RollbackService' Value='/installtype=notransaction /action=rollback /LogFile= "[#WindowsService]" "[DOTNETSERVICE]iuconfig.xml"'/>
<CustomAction Id='RollbackService' BinaryKey='InstallUtil' DllEntry='ManagedInstall' Execute='rollback' />

<!-- Now to sequence these CAs in the execute sequence -->

<InstallExecuteSequence>

<Custom Action='UnInstallServiceSetProp' After='MsiUnpublishAssemblies'>$TheService=2</Custom>
<Custom Action='UnInstallService' After='UnInstallServiceSetProp'>$TheService=2</Custom>

<Custom Action='InstallServiceSetProp' After='StartServices'>$TheService>2</Custom>
<Custom Action='InstallService' After='InstallServiceSetProp'>$TheService>2</Custom>

<Custom Action='RollbackServiceSetProp' After='InstallService'>$TheService>2</Custom>
<Custom Action='RollbackService' After='RollbackServiceSetProp'>$TheService>2</Custom>

<Custom Action='CommitServiceSetProp' After='RollbackService'>$TheService>2</Custom>
<Custom Action='CommitService' After='CommitServiceSetProp'>$TheService>2</Custom>


</InstallExecuteSequence>

<!--Now we're done-->

</Product>
</Wix>


Despite all my skepticism, the above code ran perfectly fine. Now, I have to work on the real services. Hope you find this useful.

12 comments:

Unknown said...
This comment has been removed by a blog administrator.
Unknown said...

I had given this link in the WIX users forum and Rob had suggested the use of the <ServiceInstall> tag as its the native way of installing services. I forgot to mention this in my article. This is only for services which implement the Installer class. It is not good setup design to implement the Installer class. But as we all live in a less than perfect world, you might have to do this sometime. This article covers this exception and is not for the regular services. If the service developer proposes to write code by implementing the Installer class, just let him know that it is not a good thing to do.

So as a part of best practices, DO NOT USE INSTALLER CLASS.

Anonymous said...

I am also using InstallShield 10.5 Pro and have some questions about installing Windows Services created with .NET. As I can see there are two ways of installing the service. Both work but both have limitations as far as I can see.

1. Create a component with the service executable as key file and set the ".NET Installer Class" property to Yes. This will install the service in the Service Manager in control panel, but I see no way of a) setting the startup type to autmatic instead of manual and b) start the service without rebooting the system. I guess that I can accomplish both with custom action at the end of installation but I am puzzled why this is not built into the component setup. I guess that this is the correct way of installing a .net service.

2. Create a component using the component wizard and select the "Install Service" type. This will alsp work with a .net service and it will allow me to change the user the service runs as, as well as the startup type. I guess that this method is intended for Win32 services and not .net services but it seems to work.

Are there any problems installing .net services with method 2? How can I set the startup type and start the service if I use mathod 1?

if you like you can send a copy of the reply to fredrik dot vestin at gmail dot com

Thanks a bunch

Anonymous said...

You assert, and attribute to Rob Mensching as well, the sentence:
It is not good setup design to implement the Installer class.

What are your or Rob's reasons for making such an assertion?

Anonymous said...

Shouldn't rollback come before install? http://msdn.microsoft.com/library/default.asp?url=/library/en-us/msi/setup/rollback_custom_actions.asp

- Kurt

Roberto Iza Valdés said...
This comment has been removed by a blog administrator.
Roberto Iza Valdés said...
This comment has been removed by a blog administrator.
Anonymous said...
This comment has been removed by a blog administrator.
Unknown said...

As was stated I use <ServiceInstall> with Wix to install my service and it seems to work (what ever works!). It doesn't seem to use all of the Installer class attributes. I hooked up an "AfterInstall" event in the constructor and it did not seem to be called. So I am guessing that <ServiceInstall> does not use all (or most) of the methods/events in the Installer base class.

Unknown said...

Kevin,

.NET windows services are just normal services. It is discouraged to write custom code in the service installer events. The Service installer is used to install and maintain services in the development environment. In the production environment, it is best left to windows installer's ServiceInstall table to manage its services.

One more problem that I noticed with Service Install is that it does not fail silently. If the service installation fails for some reason while using InstallUtilLib.dll, it throws a message box regardless of the UI mode that is set.

Anonymous said...

I feel that a better way to install the Windows service would be to add an entry for the service in the "ServiceInstall" table of the MSI. To delete the service during uninstallation, you would have to add a record for the service in the "ServiceControl" table. Check out the MSDN documentation for more info. on these tables.

Roberto Iza Valdés said...
This comment has been removed by the author.