Friday 29 April 2005

Old blog - Ever needed to install more than one instance of the same .NET service on the same machine?

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.

Wednesday 27 April 2005

Old blog - 27 Apr 2005

Why is it that my lovely Logitech wireless keyboard (with 17, yes, that's right - 17 extra silver buttons along the top) doesn't have a caps lock light? Or a num lock light. Or a scroll lock one (though who cares about that one).

Sunday 17 April 2005

Old blog - When is an XmlValidatingReader not an XmlValidatingReader?

It's a pretty common thing to want to do - someone gives you an XML document and you want to check it matches your schema. In the .NET world you'd use the XmlValidatingReader and some code like this:

Private Sub validate()

Dim doc As New Xml.XmlDocument

Dim reader As New Xml.XmlTextReader("c:\input.xml")

Dim validatingReader As New Xml.XmlValidatingReader(reader)

validatingReader.Schemas.Add("my namespace", "c:\myschema.xsd")

AddHandler validatingReader.ValidationEventHandler, AddressOf Me.ValidationEventHandler

doc.Load(validatingReader)


End Sub


Private Sub ValidationEventHandler(ByVal sender As Object, ByVal Arguments As Xml.Schema.ValidationEventArgs)

'do something with the errors

End Sub

Unfortunately if someone has sent you a document that's part of a completely different namespace to your schema the XmlValidatingReader simply loads merrily away and doesn't throw any errors at all.

The validation engine says to itself "hmm, can't find a schema for namespace A, therefore I can't do any validation'. Once you think about this it makes perfect sense - you've given it the schemas to use, it's not its fault if the schema it needs isn't in there.

A very simple work around is just to do something like the following:

If doc.DocumentElement.NamespaceURI <> "my target namespace" Then
Throw New ApplicationException("Target namespace of the document was crap")

End If

This very simple gotcha passed me by for a long time till one of our apps started to fall over because the XML didn't look like what it was expecting even though it had validated it successfully.