In a recent project of ours, the problem how to update a C# windows service arose.  We came up with a pretty simple way of doing it all. Here is how:

There are three issues to tackle here:

  1. How to check whether a new version is available.
  2. How to download the new version
  3. How to install it, replacing the old one.

The solutions to the first two problems are readily available on the internet, but we will go over them for the sake of completeness.

Checking for a new version of your service, program, what have you, is rather straightforward – place a file (say XML, to be able to insert both the version and the location of the latest update), or a script on a website, and have your program access it regularly. We opted for a web service method that reads the version from the .MSI installation file and returns it to the caller. The caller can then compare the result with its assembly version. Here is a quick snippet of how to obtain the assembly version of a .NET program:

Assembly asm = Assembly.GetEntryAssembly();
AssemblyName asmName = asm.GetName();

string[] sArray = asmName.Version.ToString().Split(new char[] { ‘.’ });

double asmVersion = double.Parse(sArray[0].ToString() + NumberFormatInfo.CurrentInfo.CurrencyDecimalSeparator + sArray[1].ToString());

?

Here the version of the assembly is reduced to the major and minor numbers, converted to a floating point number.

Once you receive a version number, greater than the program’s one, then you know you need to update. The location of the newer version could be hardcoded (URL, network share, etc.), or could be located in the XML file, as mentioned above, or returned by a script. We again opted for a web service method that will return the new .MSI installation file as a byte array. Here is a simple implementation of that web service method (the location of the file is stored as an application setting in the web.config file):

[WebMethod]
public byte[] DownloadLatestVersionAvailable() {

byte[] bArray = null;

string sPathToFile = System.Configuration.ConfigurationManager.AppSettings[“LatestVersionLocation”];

System.IO.FileStream fStream = new System.IO.FileStream(sPathToFile, System.IO.FileMode.Open, System.IO.FileAccess.Read);

bArray = new byte[fStream.Length];

fStream.Read(bArray, 0, (int)fStream.Length);

fStream.Close();
fStream.Dispose();

return bArray;
}

Please note that all error checking and authorization code has been stripped for brevity. If this is a commercial application then it is best that the download service method is protected with some authentication, for security purposes.

And here is a simple version of the download code:

MyWebServiceSoapClient myWebservice = new MyWebServiceSoapClient();
byte[] bArray = m_serviceCSWeb.DownloadLatestVersionAvailable();
string sPathToFile = “C:\temp\mysetup.msi”;

System.IO.FileStream fStream = new System.IO.FileStream(sPathToFile, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.ReadWrite);

fStream.Write(bArray, 0, bArray.Length);

fStream.Close();
fStream.Dispose();

As you can see, it is a rather straightforward implementation. Thus, we arrive at the final step, which is a bit more complicated.

We chose to perform the following steps to update the service at this moment:

  • stop the service, using the net.exe command line tool available under any Windows
  • deinstall the service, using the Windows Installer tool, msiexec.exe
  • install the service, again using msiexec.exe

Please refer to http://support.microsoft.com/kb/314881 for the complete list of msiexec.exe parameters.

The update must be as unobtrusive as possible, hidden if possible. This is the reason we decided to go with a .BAT file, started in a hidden window. Here is how the .BAT file is created:

string sRandBatFileName = System.IO.Path.Combine(“C:\temp”, System.IO.Path.GetFileNameWithoutExtension(System.IO.Path.GetRandomFileName()) + “.bat”);
string sPathToFile = “C:\temp\mysetup.msi”;

System.IO.StreamWriter sWriter = System.IO.File.CreateText(sRandBatFileName);

sWriter.WriteLine(“@echo off”);
sWriter.WriteLine(“NETSH Diag Ping Loopback > nul”);
sWriter.WriteLine(“net stop MYSERVICENAME”);
sWriter.WriteLine(“NETSH Diag Ping Loopback > nul”);
sWriter.WriteLine(“msiexec /quiet /x MYSERVICEPRODUCTCODE ALLUSERS=1″);

for (int i = 0; i < 10; i++)
{
sWriter.WriteLine(“NETSH Diag Ping Loopback > nul”);
}

sWriter.WriteLine(“msiexec /quiet /i \”” + sPathToFile + “\” ALLUSERS=1″);

for (int i = 0; i < 10; i++)
{
sWriter.WriteLine(“NETSH Diag Ping Loopback > nul”);
}

sWriter.WriteLine(“del \”” + sPathToFile + “\””);
sWriter.WriteLine(“del \”” + sRandBatFileName + “\””);

sWriter.Flush();
sWriter.Close();
sWriter.Dispose();

The “NETSH Diag Ping Loopback” command will simply ping the loopback interface, 127.0.0.1, and it will take about 5 (five) seconds (see details at http://www.robvanderwoude.com/wait.php), providing a delay for msiexec.exe to complete – since it is started in the background and returns immediately. There are other, better ways to monitor the execution of a command but, for clarity, they are omitted here.

The BAT file will first stop the service – “net stop MYSERVICENAME”. Then, the installation’s product code – found in the properties page of the installer in your Visual Studio project – is used to deinstall the service – “msiexec /quiet /x MYSERVICEPRODUCTCODE ALLUSERS=1″. You can also use the .MSI name, provided the product key in it is the same as for the currently installed service.

Then a delay is inserted, to allow msiexec.exe to finish its job.

Finally, the new setup is launched – “msiexec /quiet /i \”” + sPathToFile + “\” ALLUSERS=1″. The setup of the service also starts it, so no need for a “net start myservice” command.

And here is how to start the .BAT file from the service, at the end of the update process:

ProcessStartInfo psi = new ProcessStartInfo(sRandBatFileName);
psi.WindowStyle = ProcessWindowStyle.Hidden;
psi.UseShellExecute = true;
Process.Start(psi);

Then you just sit back and wait for the service to be stopped, uninstalled and installed again.

That’s it, lots of simple steps put together.