Preventing Same Version Downgrades with WiX
This would not normally be a huge concern since the members of the components, the files, are not going to change. However, I did run into an interesting issue. Because my company’s versioning scheme is MAJOR.MINOR.REVISION.BUILD where BUILD is a value auto-incremented by the build server, as far as MSI is concerned 18.104.22.168 is the same version as 22.214.171.124. And because our patch strategy is to simply release a hot-fixed MSI, we have to allow same-version upgrades.
Anyone familiar with MSI upgrades knows the dirty secret that allowing same version upgrades allows same version downgrades. Except these downgrades would result in files disappearing from the filesystem. After much research I determined the issue was because of my stupid decision to group multiple files into single Components. MSI was not applying the standard file versioning rules correctly, and thus during a downgrade the following steps were occurring:
- Version X is installed on a system and user attempts to install version X-1.
- The installer appears to calculate which Components need to be installed by version and date. It does not include the X-1′s Components because they are already on the system.
- The installer removes X’s Components from the file system.
- Finally, the installer chooses not to install X-1′s Components because the installer has already calculated that the Components on the filesystem, at version X, were newer.
I found myself in quite a pickle. How do I prevent same version downgrades when the only varying component of the version is the 4th component (which again, MSI ignores)?
First of all, let me clear the air and say that I’m using the original WiX method of handling major upgrades.
<Upgrade Id="$(var.UpgradeGuid)"> <UpgradeVersion Minimum="3.9.9" IncludeMinimum="yes" Maximum="$(var.Version)" IncludeMaximum="yes" Property="OLDERVERSIONBEINGUPGRADED" /> <UpgradeVersion Minimum="$(var.Version)" IncludeMinimum="no" OnlyDetect="yes" Property="NEWERVERSIONDETECTED" /> </Upgrade> <CustomActionRef Id="WixExitEarlyWithSuccess"/>
This bit of code uses the WiX’s custom action WixExitEarlyWithSuccess in conjunction with the NEWERVERSIONDETECTED property to detect whether or not a new version is detected, and thus the installer will exist early with a successful error code (ERROR_NO_MORE_ITEMS) if a newer version of the product is already installed. I know that more recent versions of WiX include a MajorUpgrade element that makes this process more concise (cleaner), but this new method does not work in conjunction with the WixExitEarlyWithSuccess custom action.
Since the WixExitEarlyWithSuccess custom action works by reading the NEWERVERSIONDETECTED property, I thought I’d be tricky and try populating that property myself. To that end I used a FileSearch element to look for a key file that I knew would always have the same MAJOR.MINOR.REVISION.BUILD version that I needed to compare against.
<Property Id="NEWERVERSIONDETECTED"> <DirectorySearch Id="NewerFileVersionDirSearch" Path="[INSTALLDIR]"> <FileSearch Name="KeyFile.dll" MinVersion="$(var.Version4)"/> </DirectorySearch> </Property>
This worked perfectly! Or so I thought. While this fix worked in conjunction with WiX’s own WixExitEarlyWithSuccess custom action during UI-enabled installations, it failed when the MSI was run in any type of quiet mode. A quick examination of the MSI log files with the verbose option enabled revealed that it was because the WixExitEarlyWithSuccess custom action is scheduled after FindRelatedProducts and my instance of the NEWERVERSIONDETECTED property is not set yet when run in silent mode when the WixExitEarlyWithSuccess custom action is processed.
My solution was to create my own custom action: ExitEarlyWithSuccess.
<CustomAction Id="ExitEarlyWithSuccess" VBScriptCall="Main" Property="ExitEarlyWithSuccessScript" /> <InstallExecuteSequence> <Custom Action="ExitEarlyWithSuccess" After="InstallInitialize">NOT Installed AND NEWERFILEVERSIONDETECTED</Custom> </InstallExecuteSequence> <Property Id="ExitEarlyWithSuccessScript"> <![CDATA[ Function Main() Main = 5 End Function ]]> </Property> <Property Id="NEWERFILEVERSIONDETECTED"> <DirectorySearch Id="NewerFileVersionDirSearch" Path="[INSTALLDIR]"> <FileSearch Name="KeyFile.dll" MinVersion="$(var.Version4)"/> </DirectorySearch> </Property>
The secret to my custom action is that it is scheduled after InstallInitialize giving the normal WiX properties, such as Installed a chance to be populated. The ExitEarlyWithSuccessScript is an embedded VBScript that returns the ERROR_NO_MORE_ITEMS code to indicate to the installer that it should exit immediately with a successful exit code. Also, the custom action is set to run only when the product is not installed since you don’t want this action to occur during maintenance or removal (not that it matters too much since the file version comparison wouldn’t match — the MinVersion attribute in a File element matches the next version that it is compared to. For example, 126.96.36.199 would match 188.8.131.52, but not 184.108.40.206. In this way, MinVersion will never match the currently installed version).
I’ve tested this method of exiting an installer early over several MSI files now, and it works for each one of them.
Hope this helps!
Filed under: software development | 1 Comment
Tags: custom action, msi, windows installer, wix