When you’re deploying an application, you’ll usually want to start by removing any previous installations of it. With MSI files, this is often less trivial than at first it appears.
Typically you have two options if you’re going to script this on the command line: either you uninstall the MSI by ProductCode, or you uninstall it by pointing to the previous MSI file and running msiexec /x. So far, all well and good. You will however run into problems in the following situations:
– The previous media is no longer available at the expected location
– The ProductCode has changed
– Because of changes to your UpgradeCode, you now have multiple copies of your MSI installed
– You made a mistake with an Instance Transform
Most of these issues can be resolved by manually uninstalling the MSI in question from “Add/Remove Programs.” To do it programmatically is a bit harder.
Let’s suppose you have a product called “Dunedin Software Distribution Service”, and you want to remove any existing instances prior to redeploying. There’s a very easy way to do this, in which you search for installed products by *name* rather than by ProductCode. The following line will achieve what you want:
Get - WmiObject - Class Win32_Product -
Filter "(Caption = 'Dunedin Software Distribution Service' )" |
% {
$_.Uninstall()
}
But that’s not the whole story, and this is the reason I’m writing today. This code runs like a dog, and, when run locally, appears to thrash the machine on which it’s running. The less prescriptive the caption (and therefore the more matching products it locates), the slower it runs. Search for installed MSIs containing the letter “a”, and you could be waiting minutes for it to complete on a fairly well-specified Windows Server. To be clear, I’m not talking about the time it takes to uninstall the application(s) in question; I’m simply talking about the performance of the product search itself.
Here’s something I’ve come up with that performs a lot better. It relies upon the fact every installed MSI gets a Registry entry with an installation string. That’s how you can Remove or Repair it in the ARP. So all we need to do is make sure we search both the 64-bit as well as the 32-bit hive, and we should be able to pull out command lines we can then customise to uninstall any MSI that matches our filter.
#Uninstall product matching a specified filter
$filterSpec = "<Partial or complete DisplayName for your MSI>"
$hives =
@("HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall")
$hives |
% {
Write - Host "Checking for products in hive: $_"
$keys = gci $_ - Recurse
$subkeys = $keys | % {
$displayName = [string] $_.GetValue("DisplayName")
$uninstallString =
([string] $_.GetValue("UninstallString"))
.ToLower()
.Replace("/i", "")
.Replace("msiexec.exe", "")
if ($displayName.StartsWith($filterSpec)) {
Write -
Host "Uninstalling product: $displayName"
Write -
Host "$uninstallString"
start -
process "msiexec.exe" - arg "/X $uninstallString /qn" - Wait
}
}
}
I’ve also put this script into an Octopus Deploy Step template, the JSON for which you can import below. Just copy and paste, and I hope you find it useful.
{
"Id" : "ActionTemplates-229",
"Name" : "Uninstall MSI",
"Description" : null,
"ActionType" : "Octopus.Script",
"Version" : 3,
"Properties" : {
"Octopus.Action.Script.ScriptBody": "\r\n# Uninstall product matching a specified filter\r\n$filterSpec = $OctopusParameters['Filter']\r\n$hives = @(\"HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\
\Windows\\CurrentVersion\\Uninstall\", \"HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\")\r\n\r\n$hives |% { \r\n Write-Host \"Checking for products in hive: $_\"\r\n $keys = gci
$_ -Recurse\r\n $subkeys = $keys |% { \r\n $displayName = [string]$_.GetValue(\"DisplayName\")\r\n $uninstallString = ([string]$_.GetValue(\"UninstallString\")).ToLower().Replace(\"/i\",
\"\").Replace(\"msiexec.exe\", \"\")\r\n if ($displayName.StartsWith($Filter)) { \r\n Write-Host \"Uninstalling product: $displayName\"\r\n Write-Host \"$uninstallString\"\r\n
start-process \"msiexec.exe\" -arg \"/X $uninstallString /qn\" -Wait\r\n }\r\n }\r\n}"
},
"SensitiveProperties": {},
"Parameters": [
{
"Name": "Filter",
"Label": "Filter product",
"HelpText": "Specify either the full Display Name for the product you want to uninstall, or the first few letters if you want to match multiple products. The script will perform a
string.StartsWith() comparison, and uninstall any products matching the specified filter",
"DefaultValue": "IE",
"DisplaySettings": {
"Octopus.ControlType": "SingleLineText"
}
}
],
"LastModifiedOn": "2014-12-22T16:30:45.503+00:00",
"LastModifiedBy": "AHildyard@ie.com",
"$Meta": {
"ExportedAt" : "2014-12-22T16:47:49.298Z",
"OctopusVersion" : "2.5.12.666",
"Type" : "ActionTemplate"
}
}
Posted on Tuesday, January 13, 2015 12:51 AM | Back to top