Monday, December 10, 2012

"Clever" uninstall of msi packages/applications using PowerShell

When I created my own PowerShell script library for BizTalk deployment automation I ran across the need to uninstall applications, both BizTalk applications and non-BizTalk ones, by only knowing their name.

At first, I solved it using a WMI query like so:
$name = "application name"
$product = Get-WmiObject -Class Win32_Product -Filter "name='$name'" -ComputerName "localhost"
[void]$product.Uninstall()

This is not a recommended way of doing it though. It is both very slow and also causes a bit of spamming in the Windows Eventlog since the query in fact does a reconfigure of ALL applications installed. This reconfiguration can also cause a bit of other issues in some cases.

I then created another way of trying to uninstall applications in a more failsafe and secure way by using msiexec with the uninstall flag. The tricky part was to find a way to get the product key in order to be able to use msiexec since it requires this for uninstalling an application. The result can be found below.

The script function will take the application name as argument. It will then via the registry (note that this is configured for x64, so change the path if you are running x86) look up the application settings. If the application can be found via name (it should), we extract the product key. Then this is used as a parameter to msiexec.

If the product key cannot be found (it happens), we will instead try to read the uninstall string that is set when installing the application. Windows will run this string when you choose to uninstall an application, so why do not we use it? If found, we extract the product key and do our msiexec call.

If all fail, we throw an exception to be caught in the real part of the script.

This is the full script function:

Function Uninstall-Program([string]$name)
{
    $success = $false

    # Read installation information from the registry
    $registryLocation = Get-ChildItem "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\"
    foreach ($registryItem in $registryLocation)
    {
        # If we get a match on the application name
        if ((Get-itemproperty $registryItem.PSPath).DisplayName -eq $name)
        {
            # Get the product code if possible
            $productCode = (Get-itemproperty $registryItem.PSPath).ProductCode
           
            # If a product code is available, uninstall using it
            if ([string]::IsNullOrEmpty($productCode) -eq $false)
            {
                Write-Host "Uninstalling $name, ProductCode:$code"
           
                $args="/uninstall $code"

                [diagnostics.process]::start("msiexec", $args).WaitForExit()
               
                $success = $true
            }
            # If there is no product code, try to read the uninstall string
            else
            {
                $uninstallString = (Get-itemproperty $registryItem.PSPath).UninstallString
               
                if ([string]::IsNullOrEmpty($uninstallString) -eq $false)
                {
                    # Grab the product key and create an argument string
                    $match = [RegEx]::Match($uninstallString, "{.*?}")
                    $args = "/x $($match.Value) /qb"

                    [diagnostics.process]::start("msiexec", $args).WaitForExit()
                   
                    $success = $true
                }
                else { throw "Unable to uninstall $name" }
            }
        }
    }
   
    if ($success -eq $false)
    { throw "Unable to find application $name" }
}

8 comments:

  1. Can you give an example of how to use this script to uninstall a program, say Java, from a list computers? Something along the lines of

    foreach ($computer in $computers) {yourscript.ps1 uninstall-program (Java) }

    ? Thanks!

    ReplyDelete
  2. There are as far as I know a number of ways to do that. It all depends on your setup and preferences, but google PowerShell Remoting for instance for an input on how to do what you want.

    ReplyDelete
  3. @Marcus, I am familiar with Powershell Remoting, I guess I'm not sure how to use your script in general, even on a local machine. I could not get it to work, I'm not even sure how to input the variable for $name because usually I do this in the script with something like $name = 'java'.

    I have a script that does something similar to this:
    --------------------------------------------
    $computers = Get-Content 'C:\computerlist.txt'
    $program = 'java'
    foreach ($computer in $computers) {
    Invoke-Command -ComputerName $computer -ScriptBlock {$app = Get-WmiObject -Class Win32_Product | Where-Object {
    $_.Name -like "*$($using:program)*" }
    $app.Uninstall()
    }
    }
    --------------------------------------------
    But I would like to make yours work, just need a quick tip on how to run it, and how it gets the $name populated so it knows what software to look for. Then I can work on incorporating it into ps remoting. Thanks Marcus!

    ReplyDelete
    Replies
    1. Ok.
      Without testing it, you should be able to execute it like this:
      Invoke-Command -computer $computer -ScriptBlock { Uninstall-Program "Java" }

      Where you replace "Java" with the correct application name.

      For information on how to include custom written functions in your scripts, this is a pretty good guide: http://blogs.technet.com/b/heyscriptingguy/archive/2010/08/10/how-to-reuse-windows-powershell-functions-in-scripts.aspx

      Delete
  4. This is great, this is exactly what I was looking for. Thanks! I tried using Get-WMIObject to get the program but it took several minutes. Then I read about all of the pitfalls.

    ReplyDelete
  5. This is perfect. It helped me with a pattern matching problem I was having with Remove-MSIApplications function from the Powershell App Deploy Toolkit. Added this function to my deployment script and called the function. Thank you so much!

    ReplyDelete
  6. I would use the PSChildName (MSI GUID) in these cases:

    Function MSI-Uninstall($Product) {

    # Reboot suppresion - comment out if not needed

    $SupReb = "REMOVE=ALL REBOOT=ReallySuppress"

    # Collects installed app data matching specified product

    $AppCollect = Get-ItemProperty "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",` #32-Bit
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" #64-bit

    $RemProg = $AppCollect | Where { $_.DisplayName -like "*$($Product)*" }

    # Sets up arguments based on collected items - with logging

    $ArgList = "/x $(($RemProg).PSChildName) $($SupReb) /QN /L*V $($Log)"

    # Removes the above specified application
    If($RemProg -gt $Null){ Start-Process MSIExec $ArgList -Wait}

    }

    ReplyDelete
  7. Just posted as anonymous, by mistake, also didnt copy the right file haha. I would always use the "PSChildName" when uninstalling as it is the MSI GUID, Like below (obviously you can add in success checks etc):

    Function MSI-Uninstall($Product) {

    #Logging

    $ProdLog = $Product.Replace(" ", "_")
    $UniLog = "C:\Windows\Logs\Software\$($ProdLog)_MSI_Uninstall.log"

    # Reboot suppresion - comment out if not needed
    #
    #$SupReb = "REMOVE=ALL REBOOT=ReallySuppress"
    #
    # Collects installed app data matching specified product

    $AppCollect = Get-ItemProperty "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",` #32-Bit
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" #64-bit

    $RemProg = $AppCollect | Where { $_.DisplayName -like "*$($Product)*" }

    # Sets up arguments based on collected items - with logging

    $ArgList = "/x $(($RemProg).PSChildName) $($SupReb) /QN /L*V $($UniLog)"

    # Removes the above specified application

    If($RemProg -gt $Null){ Start-Process MSIExec $ArgList -Wait}

    }

    ReplyDelete