What I needed was a way to install more than one instance of my service on a single machine (to support different test environments using a single box). Using the .NET project installer approach there's a pretty simple way to do this. At runtime the Context property of the project installer contains an assemblyName parameter that points to the service exe being installed. From this you can work out the name of the config file, load it up and pull out information such as the service name. You can then simply set the ServiceName property of your service installer and away you go. Simply running installutil against an instance of your service (with its own config file) means you can install more than one instance.
To support more than one service instance your service should set its ServiceName property to match the name you've set during installation. Whilst failing to do this doesn't make your machine melt it can cause issues e.g. an event source is created at install time based on the service name. If your service has a different name then at run time it will fall over if it doesn't have permission to create a new event source when it tries to log that it's started.
Below is the minimum amount of code you need in your project installer class (assumes you've added one using the Add Installer option on the properties window)
Public Overrides Sub Install(ByVal stateSaver As System.Collections.IDictionary)
Dim document As New Xml.XmlDocument
document.Load(Me.Context.Parameters("assemblyPath") & ".config")
Dim node As Xml.XmlNode = document.SelectSingleNode("/configuration/appSettings/add[@key='ServiceName']/@value")
Me.ServiceInstaller1.ServiceName = node.Value
Me.ServiceInstaller1.DisplayName = node.Value
stateSaver.Add("ServiceName", node.Value)
MyBase.Install(stateSaver)
End Sub
Public Overrides Sub Uninstall(ByVal savedState As System.Collections.IDictionary)
Me.ServiceInstaller1.ServiceName = savedState("ServiceName")
MyBase.Uninstall(savedState)
End Sub
Wanting to make things a little bit more user friendly I created a application that would wrap up the whole process of creating a new directory, copying over the service exe and required local files, add in the config file and then call install the service.
A little digging through the installutil code using Anakrino revealed that all the work is done by the System.Configuration.Install.ManagedInstallerClass class. You simply call the InstallHelper method, passing an array containing the same command line arguments you would pass to installutil.
Ignoring the "Do not use from your own code" MSDN warning I plugged this into my application and merrily started creating service instances. Unfortunately it didn't work all of the time. It turns out that this ManagedInstallerClass remembers the exe you first installed and then only ever works with this one until you restart your application. This doesn't bother installutil because it quits once it has done one installation.
It turns out though that it's pretty easy to create your own replacement for the ManagedInstallerClass.
Installing a service
To install a service you can create an instance of ServiceInstaller, set a few properties and call the Install method.
Public Sub Install(ByVal ServicePath As String, ByVal ServiceName As String)
Dim context As New System.Configuration.Install.InstallContext("c:\install.log", Nothing)
context.Parameters.Add("assemblyPath", ServicePath)
Dim serviceProcessInstaller As New System.ServiceProcess.ServiceProcessInstaller
serviceProcessInstaller.Account = ServiceProcess.ServiceAccount.LocalSystem
Dim serviceInstaller As New System.ServiceProcess.ServiceInstaller
With serviceInstaller
.Context = context
.ServiceName = ServiceName
.DisplayName = ServiceName
.Parent = serviceProcessInstaller
End With
serviceInstaller.Install(New Hashtable)
End Sub
Removing the service is even easier, all you need is the service name.
Public Sub Uninstall(ByVal ServiceName As String)
Dim context As New System.Configuration.Install.InstallContext("c:\uninstall.log", Nothing)
Dim serviceInstaller As New System.ServiceProcess.ServiceInstaller
With serviceInstaller
.Context = context
.ServiceName = ServiceName
End With
serviceInstaller.Uninstall(Nothing)
End Sub
Installing performance counters
If your service uses performance counters you can take a similar approach except this time you'd use an instance of System.Diagnostics.PerformanceCounterInstaller. Set its Context property, PerformanceCounterCategoryName and then add the counters you wish to install to its Counters collection. Then call Install.
Capturing installation output
As well as writing to the log file you supply with the Context object the installers write information to the Console.
To capture this output assign your own TextWriter to the standard output stream using Console.setOut. I created a TextWriter that wrote everything to a text box.