recently needed to deploy a Windows service as part of a Release Management pipeline. In the past, our internal systems I have only need to deploy DB (via SSDT Dacpacs) and Websites (via MSDeploy), so a new experience.
WIX Contents
The first step to to create an MSI installer for the service. This was done using WIX, with all the fun that usually entails. The key part was a component to do the actual registration and starting of the service
<Component Id ="ModuleHostInstall" Guid="{3DF13451-6A04-4B62-AFCB-731A572C12C9}" Win64="yes">
<CreateFolder />
<Util:User Id="ModuleHostServiceUser" CreateUser="no" Name="\[SERVICEUSER\]" Password="\[PASSWORD\]" LogonAsService="yes" />
<File Id="CandyModuleHostService" Name ="DataFeed.ModuleHost.exe" Source="$(var.ModuleHost.TargetDir)ModuleHost.exe" KeyPath="yes" Vital="yes"/>
<ServiceInstall Id="CandyModuleHostService" Name ="ModuleHost" DisplayName="Candy Module Host" Start="auto" ErrorControl="normal" Type="ownProcess" Account="\[SERVICEUSER\]" Password="\[PASSWORD\]" Description="Manages the deployment of Candy modules" />
<ServiceControl Id="CandyModuleHostServiceControl" Name="ModuleHost" Start="install" Stop="both" Wait="yes" Remove="uninstall"/>
So nothing that special here, but worth remembering if you miss out the ServiceControl block the service will not automatically start or be uninstalled with the MSI’s uninstall
You can see that we pass in the service account to be used to run the service as a property. This is an important technique for using WIX with Release Management, you will want to be able to pass in anything you may want to change as installation time as a parameter. This means we ended up with a good few properties such as
<Property Id="DBSERVER" Value=".sqlexpress" />
<Property Id="DBNAME" Value ="=CandyDB" />
<Property Id="SERVICEUSER" Value="Domainserviceuser" />
<Property Id="PASSWORD" Value="Password1" />
These tended to equate to app.config settings. In all cases I tried to set sensible default values so in most cases I could avoid passing in an override value.
These property values were then used to re-write the app.config file after the copying of the files from the MSI onto the target server. This was done using the XMLFile tools and some XPath e.g.
<Util:XmlFile Id="CacheDatabaseName"
Action="setValue"
Permanent="yes"
File="\[#ModuleHost.exe.config\]"
ElementPath="/configuration/applicationSettings/DataFeed.Properties.Settings/setting\[\[\]@name='CacheDatabaseName'\[\]\]/value" Value="\[CACHEDATABASENAME\]" Sequence="1" />
Command Line Testing
Once the MSI was built it could be tested from the command line using the form
msiexec /i Installer.msi /Lv msi.log SERVICEUSER="domainsvc\_acc" PASSWORD="Password1" DBSERVER="dbserver" DBSERVER="myDB" …..
I soon spotted a problem. As I was equating properties with app.config settings I was passing in connections strings and URLs, so the command line got long very quickly. It was really unwieldy to handle
A check of the log file I was creating, msi.log, showed the command line seemed to be truncated. This seemed to occur around 1000 characters. I am not sure if this was an artefact of the logging or the command line, but either way a good reason to try to shorten the property list.
I therefore decided that I would not pass in whole connection strings, but just the properties that might change, especially effective for connection strings to things such as Entity Framework. This meant I did some string building in WIX during the transformation of the app.config file e.g.
<Util:XmlFile Id='CandyManagementEntities1'
Action='setValue'
ElementPath='/configuration/connectionStrings/add\[\[\]@name="MyManagementEntities"\[\]\]/@connectionString'
File='\[#ModuleHost.exe.config\]' Value='metadata=res://\*/MyEntities.csdl|res://\*/MyEntities.ssdl|res://\*/MyEntities.msl;provider=System.Data.SqlClient;provider connection string="data source=\[DBSERVER\];initial catalog=\[DBNAME\];integrated security=True;MultipleActiveResultSets=True;App=EntityFramework"' />
This technique had another couple of advantages
- It meant I did not need to worry over spaces in strings, I could therefore lose the “ in the command line – Turns out this is really important later.
- As I was passing in just a ‘secret value’ as opposed to a whole URL I could use the encryption features of Release Management to hide certain values
It is at this point I was delayed for a long time. You have to be really careful when installing Windows services via an MSI that your service can actually start. If it cannot then you will get errors saying "… could not be installed. Verify that you have sufficient privileges to install system services". This is probably not really a rights issue, just that some configuration setting is wrong so the service has failed to start. In my case it was down to an incorrect connection string, stray commas and quotes, and a missing DLL that should have been in the installer. You often end up working fairly blind at this point as Windows services don’t give too much information when they fail to load. Persistence, SysInternals Tools and comparing to the settings/files on a working development PC are the best options
Release Management Component
Once I had working command line I could create a component in Release Management. On the Configure Apps > Components page I already had a MDI Deployer, but this did not expose any properties. I therefore copied this component to create a MSI deployer specific to my new service installer and started to edit it.
All the edits were on the deployment tab, adding the extra properties that could be configured.
Note: Now it might be possible to do something with the pre/post deployment configuration variables as we do with MSDeploy, allowing the MSI to run then editing the app.config later. However, given that MSI service installers tends to fail they cannot start the new service I think passing in the correct properties into MSIEXEC is a better option. Also means it is consistent for anyone using the MSI via the command line.
On the Deployment tab I changed the Arguments to
\-File ./msiexec.ps1 -MsiFileName "\_\_Installer\_\_" -MsiCustomArgs ‘SERVICEUSER=”\_\_SERVICEUSER\_\_” PASSWORD=”\_\_PASSWORD\_\_” DBSERVER=”\_\_DBSERVER\_\_” DBNAME=”\_\_DBNAME\_\_” …. ’
I had initially assumed I needed the quotes around property values. Turns out I didn’t, and due to the way Release Management runs the component they made matters much, much worse. MSIEXEC kept failing instantly. if I ran the command line by hand on the target machine it was actually showing the Help dialog, so I knew the command line was invalid.
Turns out the issue is Release Management calls PowerShell.EXE to run the script passing in the Arguments. This in turn calls a PowerShell Script which does some argument processing before running a process to run MSIEXEC.EXE with some parameters. You can see there are loads of places where the escaping and quotes around parameters could get confused.
After much fiddling, swapping ‘ for “ I realised I could just forget most of the quotes. I had already edited my WIX package to build complex strings, so the actual values were simple with no spaces. Hence my command line became
\-File ./msiexec.ps1 -MsiFileName "\_\_Installer\_\_" -MsiCustomArgs “SERVICEUSER=\_\_SERVICEUSER\_\_ PASSWORD=\_\_PASSWORD\_\_ DBSERVER=\_\_DBSERVER\_\_ DBNAME=\_\_DBNAME\_\_ …. “
Once this was set my release pipeline worked resulting in a system with DBs, web services and window service all up and running.
As is often the case it took a while to get this first MSI running, but I am sure the next one will be much easier.