Automatic Updating in C#
Once you start writing programs in C♯ that are actually useful, at some point the question of how you are going to deliver updates to your users is sure to come up.
You could upload new versions to your website (and you should!) and ask everyone to subscribe to your blog, but that's a hassle for users. Rather, you want some kind of automatic system that at least lets your users know if there's an update available. It would be even better if it could update itself automatically.
Unfortunately, building a fully automatic update system in C# is not a particularly trivial task, as I have discovered. Thankfully though, I've done most of the hard work for you, so you don't have to.
Before we continue, it should be noted that the code I'm about to show you should not be taken as is. There are several serious pitfalls that I haven't fixed in the code below (some of which I'll point out and explain). Rather, this code should be taken as a starting point for your own system.
To build an automatic updating system, we need to know 3 things:
- The local version of the program installed
- The version of the program on the server
- Whether the version on the server is newer than the version currently installed locally.
These problems are quite easily solved. Let's start with a simple example program:
using System;
class Program
{
static string version = "0.1";
static void Main(string[] args)
{
// Continue on with the main body of the program
Console.WriteLine("Rockets are cool.");
}
}
This is practically a hello world program - yours will be much more complicated than this. Problem #1 is already solved in the above - we have a variable version
that contains the current version of the software. Note that it's a a string because in the future we might want more than one decimal point. If you are interested in version numbering, check out semantic versioning. Anyway, let's move on.
The first snag I hit here was that a running C# program cannot replace itself while it is running. This is easily solved, however, by a helper program that will do the actual updating. This presents another issue though, how do we extract the version from the program itself? The helper application won't have access to anything contained within the main program (not without some seriously clever wizardry at least). There are several things we can do here. We could get the program to write out its version to a text file every time it runs, or we could build in a special flag that outputs the version number and then exits. We could also bundle the version number in a separate text file along with the program itself, although this would be prone to tampering. You decide what's best for you.
My solution was to write out the version number to a text file:
// Write out our current version
File.WriteAllText("version.txt", version);
// Exit now if we were just asked for our version
if (args.Length > 0 && args[0].Trim().ToLower() == "writeversion")
return;
Next up we need a helper program that can ask a remote server what the latest version of the program is, and download the new version if the version on the server is newer than the version that is installed on the local machine.
We can use a WebClient
to download a text file that describes the version currently on the remote server, and the Version
class to compare versions. Here's the code I came up with:
using System;
using System.Net;
using System.IO;
using System.IO.Compression;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Text;
using System.Security.Cryptography;
using System.Security.Permissions;
class Program
{
static string remoteVersionURL = "http://localhost:8090/program-version.txt";
static void Main(string[] args)
{
if(args.Length == 0)
{
Console.WriteLine("This helper program updates the main program. Call it like this:");
Console.WriteLine("\thelper.exe update");
return;
}
WebClient webClient = new WebClient();
switch(args[0].Trim().ToLower())
{
case "update":
Console.WriteLine("Checking for updates...");
// Format:
// <version> <url> <hash>
string remoteVersionText = webClient.DownloadString(remoteVersionURL).Trim();
string[] remoteVersionParts = (new Regex(@"\s+")).Split(remoteVersionText);
string remoteUrl = remoteVersionParts[1];
string remoteHash = remoteVersionParts[2];
Version localVersion = new Version(File.ReadAllText("version.txt").Trim());
Version remoteVersion = new Version(remoteVersionParts[0]);
if(remoteVersion > localVersion)
{
// Do an update here
}
break;
default:
Console.WriteLine("Unknown command.");
break;
}
}
}
The above reads the version.txt
file that the main program generates, and downloads a text file from a remote url that contains information about the latest version of the program in question. If you were doing this for real you'd point this as a https url to prevent man-in-the-middle attacks. For testing purposes, I've started a server on my local machine with PHP. I've also added a couple of using
statements that we will be using later.
The file that it downloads has several parts. Here's the example file I used:
0.2 http://localhost:8090/release.zip e8643f52dbaff369614128b9683550879a01c3f4
Where:
- The version number stored on the server.
- The url at which the latest version can be found.
- The hash of the compress file that the url points to.
The third part of this version file is important. We can hash the file that we have downloaded, and if the hash we compute identical to that of the remote file, we know that the downloaded file is safe, assuming that an attacker hasn't modified the version file that we downloaded in the first place.
Next we need to actually do the update. Before that though, we ought to ask the user first. I used a quick Console.ReadLine()
loop, but you should probably put together some sort of GUI.
// There is a new version on the server!
Console.WriteLine("There is a new version available on the server.");
Console.WriteLine("Current Version: {0}, New version: {1}", localVersion, remoteVersion);
while (true)
{
Console.Write("Perform update? ");
string response = Console.ReadLine().Trim().ToLower();
if (response.StartsWith("y"))
{
PerformUpdate(remoteUrl, remoteHash);
break;
}
else if (response.StartsWith("n"))
{
Console.WriteLine("Abort.");
break;
}
}
With that out of the way, we can actually do the update. Let's start by writing the beginnings of thePerformUpdate()
method:
static bool PerformUpdate(string remoteUrl, string expectedHash)
{
Console.WriteLine("Beginning update.");
string downloadDestination = Path.GetTempFileName();
Console.Write("Downloading {0} to {1} - ", remoteUrl, downloadDestination);
WebClient downloadifier = new WebClient();
downloadifier.DownloadFile(remoteUrl, downloadDestination);
Console.WriteLine("done.");
return true;
}
Next, we need to use that hash I mentioned above. That particular hash is SHA1 (although you should use SHA2 or even better SHA3), so we need to hash our zip that we downloaded with SHA1:
Console.Write("Validating download - ");
string downloadHash = GetSHA1HashFromFile(downloadDestination);
if (downloadHash.Trim().ToLower() != expectedHash.Trim().ToLower()) {
// The downloaded file looks bad!
// Destroy it quick before it can do any more damage!
File.Delete(downloadDestination);
// Tell the user about what has happened
Console.WriteLine("Fail!");
Console.WriteLine("Expected {0}, but actually got {1}).", expectedHash, downloadHash);
Console.WriteLine("The downloaded update may have been modified by an attacker in transit!");
Console.WriteLine("Nothing has been changed, and the downloaded file deleted.");
return false;
}
else
{
Console.WriteLine("ok.");
}
The above calculates the hash of the downloaded file, and alerts the user if the hash computed is different to that reported by the remote server. Here's the GetSHA1HashFromFile()
method:
/// <summary>
/// Gets the SHA1 hash from file.
/// Adapted from https://stackoverflow.com/a/16318156/1460422
/// </summary>
/// <param name="fileName">The filename to hash.</param>
/// <returns>The SHA1 hash from file.</returns>
static string GetSHA1HashFromFile(string fileName)
{
FileStream file = new FileStream(fileName, FileMode.Open);
SHA1 sha1 = new SHA1CryptoServiceProvider();
byte[] byteHash = sha1.ComputeHash(file);
file.Close();
StringBuilder hashString = new StringBuilder();
for (int i = 0; i < byteHash.Length; i++)
hashString.Append(byteHash[i].ToString("x2"));
return hashString.ToString();
}
It's lifted straight from stackoverflow, but it does the job I need it to do. Now we've verified that the downloaded file is (probably) ok, we can proceed to unpack it. Note here that the built in ZipFile
class throws an exception if it encounters a pre-existing file, so I'm unpacking it to a temporary directory and moving it from there.
// Since the download doesn't appear to be bad at first sight, let's extract it
Console.Write("Extracting archive - ");
string extractTarget = @"./downloadedFiles";
ZipFile.ExtractToDirectory(downloadDestination, extractTarget);
// Copy the extracted files and replace everything in the current directory to finish the update
// C# doesn't easily let us extract & replace at the same time
// From http://stackoverflow.com/a/3822913/1460422
foreach (string newPath in Directory.GetFiles(extractTarget, "*.*", SearchOption.AllDirectories))
File.Copy(newPath, newPath.Replace(extractTarget, "."), true);
Console.WriteLine("done.");
// Clean up the temporary files
Console.Write("Cleaning up - ");
Directory.Delete(extractTarget, true);
Console.WriteLine("done.");
A word of warning: The above is windows only! Mono (the open source C# compiler for other platforms) does not support the ZipFile
class as of the time of posting. A (much) better and cross platform solution would be to use an external library here, such as SevenZipSharp. You could also use a different archive type to obtain a greater compression ratio, such as .xz
or .7z
.
If you put it all together, you get a working auto update mechanism. The full source code can be found here: Full source code.