When you deploy a software product, you’ll want to have the opportunity to update the instances of the product installed on the computers of your customers or users. There are a myriad of ways and tools to go about doing that, and if you’re deploying via an app store (e.g. Apple App Store, Google Play Store, Windows Store) this functionality is already included. But if your app is a classic Win32 app, you’ll have to do a bit more yourself.
You may either choose to use an existing solution like Actual Updater or the auto updater included in Advanced Installer, which normally should be the way to go – or you roll your own. Some years ago I decided to do just that, and I’m going to write a bit about why and how I did it.
Rationale
The main reasons I wrote my own auto-updater were on the one hand that I’d already had experience from work-related projects but more importantly because I also wanted my updater to be highly flexible and customized to seamlessly integrate into my News App, which features a modern, OpenGL based, animated User Interface. I wanted my updater not only to have the same User Experience and Look and Feel as the main product, I also wanted it to behave a certain way so that I could have full control. E.g., I wanted to be able to use my already implemented PKI to secure my updates.
Full or incremental
There are two major paradigms when it comes to updating software and variations in implementation and challenges resulting from which paradigm you choose.
Full update installation – The auto-updater, upon verifying there’s a newer version of the software, downloads a full installation package and runs it. First. the previously installed version of the product is uninstalled. A reboot might be necessary to make sure locked files are deleted. Afterwards, the latest version is installed, in most cases prompting the user for access rights elevation to be able to write to C:\Program Files or C:\Program Files (x86). After installation, another reboot might be in order in some cases (like driver installation).
The reboot after deinstallation can often be avoided, because most times it’s just about removing locked files (files that are running at the moment, like the .exe of your software). Now, you can’t delete such files (in Windows anyway), but you can move them (on the same Volume). Which means, you just move any locked files to a temp folder, rename them (to a random name!) and try to delete them.
Deleting won’t work, but Windows will remember this (in the Windows Registry under PendingFileRenameOperations) and delete them on the next reboot, so you won’t have to worry about them anymore.
But still, with a full update the whole process requires a deinstallation and an installation, most times including a UI elevation prompt, so that’s a lot of your user’s precious time that’s being used up for maintenance and on top of that a lot of stuff that can go wrong.
And stuff will go wrong during reinstallation bonanzas for some users if your user base has any kind of meaningful size.
To avoid problems like this and to save some time, in come incremental updates!
Incremental update – The auto-updater again realizes there is a newer version of the software, only this time it loads an auto-generated update description file (XML or JSON) of what the current state of the software should be when it’s installed and compares it en detail with what’s actually installed. There’s a list of files and corresponding hashes for those files which the updater can match. This allows the updater to compile its own list of files that are different from what they should be – and to only download and install (= overwrite) those files!
<updatedefinition>
<package from="0.1.0" to="0.4.132" version="0.4.133">
<baseurl value="https://{product_update_url}" />
<corefiles>
<exe source="eula.txt" target="${InstallDir}eula.txt" size="1300" md5="28c98cbdf1e0581daefb8e1560ebbd1b" />
<exe source="license.xml" target="${InstallDir}license.xml" size="527" md5="e620ed9b6140f17306b0f03c6dc745e4" />
<exe source="nubusupdater.exe" target="${InstallDir}nubusupdater.exe" size="1146880" md5="e88e98e2dc17013450184308145572a6" />
<exe source="nubus_desktop.exe" target="${InstallDir}nubus_mini.exe" size="4485120" md5="97967b2e3f8309d8363ee4e3bb9003c4" />
<exe source="product.xml" target="${InstallDir}product.xml" size="1116" md5="228e87444f234a7fc20d21f131f46b38" />
<exe source="res/beach.res" target="${InstallDir}res/beach.res" size="1979878" md5="aad91517aa895adb82e2b34577797917" />
<exe source="res/common.res" target="${InstallDir}res/common.res" size="4995748" md5="30762972255241afa9b5087d5019070a" />
<exe source="res/daytime.res" target="${InstallDir}res/daytime.res" size="317552" md5="9c91ac1d1a3e14c588c81e1cc11695e4" />
<exe source="res/default.res" target="${InstallDir}res/default.res" size="277261" md5="40235c97c6c4da3d295565eefa913ff8" />
<exe source="res/evening.res" target="${InstallDir}res/evening.res" size="1245698" md5="8e67ada6619b8720593179804e39e5e8" />
<exe source="res/magellan.res" target="${InstallDir}res/magellan.res" size="1438381" md5="fe8901d9e568cfdacdeffeac41625e00" />
<exe source="res/matrix.res" target="${InstallDir}res/matrix.res" size="360653" md5="8eb1ad815b9a25ece3e5e0785948b9ff" />
<exe source="res/metro.res" target="${InstallDir}res/metro.res" size="305270" md5="51f2964aec60a04dd2a92ba36c4f1f83" />
<exe source="res/miami.res" target="${InstallDir}res/miami.res" size="8119951" md5="43d2d0f3ba70623ad7b4f032aa3ef4ce" />
<exe source="res/moonlight.res" target="${InstallDir}res/moonlight.res" size="895628" md5="05b9e11fda6f2777ff4b6e1019627479" />
<exe source="res/nature.res" target="${InstallDir}res/nature.res" size="2852601" md5="86d9a6f6cc0bd989dc6ae0db46b02028" />
<exe source="res/nightlights.res" target="${InstallDir}res/nightlights.res" size="2385574" md5="da593824f387af95f195186170b9a926" />
<exe source="res/sky.res" target="${InstallDir}res/sky.res" size="2544920" md5="8e3197f98f01e18e5ac9a19545ba04dc" />
<exe source="res/space.res" target="${InstallDir}res/space.res" size="1419130" md5="0c341f40ef5096f52264baf0772df6f4" />
<exe source="res/sunset.res" target="${InstallDir}res/sunset.res" size="344315" md5="b02c167109ece97af4f4ff6e7f6e727b" />
<exe source="res/water.res" target="${InstallDir}res/water.res" size="2839189" md5="ab81d1464ff6ee91ac4fab79b6f48462" />
</corefiles>
<extensions>
</extensions>
</package>
<signature value="CLYhl/P/NZ+bO4JbfRpBRtKn5I982UCL5vs215WlFkRS4mrneDSZY1aJ2B+hzCRQcQtgLS2pge1WsKSe24d/l4FYnkLYWfdmG7Gag1nUeAeSWjS8vdtVHmi0zLPJumIq7WMLeAimMLTrpR5GbiYdSJ6kw4WGgdW7p4mD+LpRp1I=" />
</updatedefinition>
This not only saves a lot of time and bandwidth, it also means no deinstallation/installation with all the potential trouble that comes with it.
Elevation versus Windows Service
As for the elevation UI prompt, this could be avoided in both cases by implementing your updater as a Windows service. Many vendors do this in order to be able to silently update their products in the background (Google is doing this for its products, Mozilla too and many more.)
The downside being that you have an executable with admin rights running all the time that could wreak havoc if it were to be replaced with a malicious executable by a malware. But if that happens, you’ve already lost control of the machine anyway…
Incremental File Replacement
In order to replace only files that need replacing the auto-updater in some way has to figure out which files are outdated. To do this, it downloads a list from the backend/update server containing all files of the product as they should be installed for the latest/current version of the product.
This list also contains MD5 hashes for these files.
The updater then iterates through the files that are currently installed, calculates their MD5 hashes and compares the hashes to those in the update list. For all files whose hashes don’t match the updater will know to replace them: it downloads each file, moves/renames the old file and writes the new file to the installation folder. After that, only the version number has to be updated in the Windows Registry.
This process is very fast, and if the elevation prompt can be avoided as well, makes for an extremely smooth User Experience.
Security
To make sure no malware is installed and that the update is verified during this process (or any other update process for that matter), some precautions need to be taken.
Firstly, the update backend server must use SSL, and the updater needs to make sure that what it downloads from the server is legit. To do this, it uses a PKI (Public key infrastructure): I generated a key pair whose public key is installed with every instance of the software that is being deployed.
The private key is used to sign the content of the update description XML or JSON, and the signature is a property behind the content in the update description.
The updater verifies the signature using the public key deployed with the software and only then starts replacing files.
As an added bonus the MD5 hashes used to compare installed vs. updated files also serve as a safeguard that the files have been correctly downloaded. If the hashes don’t match after the update, it just downloads the file(s) again.
It’s been a lot of fun implementing this auto-update procedure and I’ve learnt a lot when I did it back then. However, today I would probably use a tried and tested popular auto updater tool (which exhibits at the very least the security features listed above, but probably more) instead of building my own update strategy – it’s just another thing more to maintain and secure. But if you want to know how something like that could be implemented or for some reason need to do so yourself I hope I was able to shed some insight.
If you have any questions just leave a comment. Thanks for reading!
0 Comments