Geeks With Blogs
Alex Hildyard

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


Comments on this post: Uninstalling MSI Files with Powershell and WMI

# re: Uninstalling MSI Files with Powershell and WMI
Requesting Gravatar...
Thanks a lot for the powershell code to uninstall applications - it really is much faster than the Get-WmiObject method.
However your example above declares a filter variable $filterSpec, but actually uses an unspecified variable $filter for the actual filtering. Executing that would result in an empty filter,
possibly resulting in the uninstall of all applications. It is good practice not to execute code pasted from the internet, that one doesn't understand, but one of your readers might not be so cautious and be in for a nasty surprise...
Left by SB on Jan 16, 2015 7:51 AM

# re: Uninstalling MSI Files with Powershell and WMI
Requesting Gravatar...
SB, many thanks. I'm grateful for your eagle eyes! As you point out, an empty filter string would be a rather unpleasant experience. Inline code now updated (the Octopus template is correct, and doesn't need an update ...)
Left by Alex Hildyard on Jan 16, 2015 1:05 PM

# re: Uninstalling MSI Files with Powershell and WMI
Requesting Gravatar...
Many thanks for this. Both for the code, and the explaination of why WMI is so bad.
Left by David on May 25, 2015 8:03 AM

# re: Uninstalling MSI Files with Powershell and WMI
Requesting Gravatar...
Microsoft don't recommend querying Win32_Product:

This process also initiates a consistency check of packages installed, verifying and repairing the install.


Official KB Article here:
https://support.microsoft.com/en-au/kb/974524
Left by Daniel on Aug 18, 2015 6:59 AM

Your comment:
 (will show your gravatar)


Copyright © Alex Hildyard | Powered by: GeeksWithBlogs.net