Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
287 views
in Technique[技术] by (71.8m points)

powershell - How can I find the Upgrade Code for an installed MSI file?

In certain cases the need to retrieve MSI upgrade codes for deployed packages can arise.

Common scenarios:

  • I took over someone else's MSI project, and I need to determine what upgrade codes were used for previous versions that are already in the wild. This is necessary to handle upgrade scenarios. I have no archive of releases anywhere.
  • I accidentally changed the upgrade code for my WiX package several times during development and I need to find all Upgrade Code versions "in the wild". I was not aware that Upgrade Codes should remain stable between versions.

This is a Q/A style question.

This question has come up before in various incarnations, but this is not a duplicate. I am posting a way to do it that uses the main MSI automation interface (or strictly speaking WMI). It should be more reliable than registry based approaches from previous answers. This answers also tries to summarize other retrieval approaches.

Question&Answers:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

MSI Upgrade Code Retrieval (via PowerShell / WMI)

Uninstalling?: Via Upgrade Code, Via Product Code, Via Product Name, etc...

The PowerShell script below should retrieve all related product codes, upgrade codes and product names installed on your machine (table output).

Screenshot of output (full script below):

powershell output

These are the real, live values directly from the Windows Installer database on the machine in question. There is no need for any conversion or interpretation. We are going through the proper APIs.

Technical note!: Be aware that checking properties directly in your original MSI file (property table) or WiX source file, may not match actual installed values since properties can be overridden at install time via transforms (more info below) - or property values specified at the command line. The moral of the story: retrieve property values directly from the system when you can.

Quick disclaimer: In rare cases running the script can trigger a Windows Installer self-repair. Read more in "disclaimer section" below. Just a potential nuisance, but read the disclaimer please.

As a digression, there is also a one-line PowerShell command which will retrieve product codes and upgrade codes only - without the package name included. This might actually suffice for some users (I would recommend the full script below however). There is a screenshot of the output of this one-liner in a section below. Note: this command appears a lot faster than the larger script (the "Value" field is the upgrade code). Also note: product codes without associated upgrade codes will not show up as far as I can tell - they will in the larger script:

gwmi -Query "SELECT ProductCode,Value FROM Win32_Property WHERE Property='UpgradeCode'" | Format-Table ProductCode,Value

To run the full PowerShell script below:

  1. Launch PowerShell (hold down the Windows key, tap R, release the Windows key, type in "powershell" and press OK or hit enter).
  2. Copy the script below in its entirety, and then just right click inside the PowerShell window.
  3. This should start the script, and it will take quite a while to run.
  4. Please report any problems. I am no PowerShell expert - I am a deployment specialist not a coder, but the script should do the job.
  5. Performance note: I just get the whole Win32_Product WMI object
    • Cherry picking properties seemed to actually make it marginally slower (VBScript test).
    • I guess we need to get all rows anyway, and cherry picking columns is just extra lifting?
    • For Win32_Property we filter both rows and columns (upgrade code is just one of many row-types). Be prepared for a slow operation, WMI is very slow.
$wmipackages = Get-WmiObject -Class win32_product
$wmiproperties = gwmi -Query "SELECT ProductCode,Value FROM Win32_Property WHERE Property='UpgradeCode'"
$packageinfo = New-Object System.Data.Datatable
[void]$packageinfo.Columns.Add("Name")
[void]$packageinfo.Columns.Add("ProductCode")
[void]$packageinfo.Columns.Add("UpgradeCode")

foreach ($package in $wmipackages) 
{
    $foundupgradecode = $false # Assume no upgrade code is found

    foreach ($property in $wmiproperties) {

        if ($package.IdentifyingNumber -eq $property.ProductCode) {
           [void]$packageinfo.Rows.Add($package.Name,$package.IdentifyingNumber, $property.Value)
           $foundupgradecode = $true
           break
        }
    }
    
    if(-Not ($foundupgradecode)) { 
         # No upgrade code found, add product code to list
         [void]$packageinfo.Rows.Add($package.Name,$package.IdentifyingNumber, "") 
    }
}

$packageinfo | Sort-Object -Property Name | Format-table ProductCode, UpgradeCode, Name

# Enable the following line to export to CSV (good for annotation). Set full path in quotes
# $packageinfo | Export-Csv "[YourFullWriteablePath]MsiInfo.csv"

# copy this line as well

Running on Remote Machines

  • It should be relatively easy to extend the script above to run on remote machines, but I am not set up to test it properly at the moment.
  • The information below has gotten a bit messy, let me know if it is not understandable or unclear.
  • In a real Windows domain it should (in theory) just be a matter of adding the remote machines to the WMI calls themselves (and loop over a list of machines - see mock-up below). And crucially: you should use a real domain admin account to run the query. It is possible that the changes I list below to make WMI work in workgroup environments also could be required for some domains, I don't know (firewall rule and UAC registry tweak). I would guess that a real domain admin account should have the privileges and access required though.
  • Remote connections in WMI are affected by (at least) the Windows Firewall, DCOM settings, CIMOM Settings and User Account Control (UAC) (plus any additional non-Microsoft factors - for instance real firewalls, third party software firewalls, security software of various kinds, etc...). Here are some details:
  • In non-domain networks (small office, home, etc...) you probably have to add user credentials directly to the WMI calls to make it work. And you probably must have "real admin rights" on the machines in question to make the queries run remotely in a home network (workgroup). I have heard that the built-in Administrator account does not have any UAC issues, but I have never tried it. In my opinion: don't use this account.
    • In my testing I had to (1) update the Windows firewall rules and (2) disable the Remote UAC access token filtering and use a real, local admin account on the remote system. Note that I don't recommend either of these changes, just reporting what worked for me.
    • Change 1: Windows Firewall, run command (cmd.exe, run as admin): netsh advfirewall firewall set rule group="windows management instrumentation (wmi)" new enable=yes (source - see this link for command line to disable this new rule again if you are just testing. Essentially just set enable=no). See the linked source for potentially more restrictive rules that could also work.
    • Change 2: Disable Remote UAC access token filtering: you need to set the following registry value: HKLMSOFTWAREMicrosoftWindowsCurrentVersionPoliciesSystem LocalAccountTokenFilterPolicy = 1 (source - mid page, latter half). I set a 32-bit DWORD.

With those changes in place on the remote system, I also added user credentials to each call by prompting the user $Cred = Get-Credential. There are also more advanced options for defining the user credentials, as explained here: Pass password into -credential (and here). To test run, here is a little test script. Copy all lines below, modify the remote machine name and paste into PowerShell by right clicking (you will be prompted for credentials):

$Cred = Get-Credential
gwmi -ComputerName RemoteMachineName -credential $Cred -Query "SELECT ProductCode,Value FROM Win32_Property WHERE Property='UpgradeCode'" | Format-Table ProductCode,Value
# copy this line too

For the large PowerShell script above, the basic additions for remote running on several machines in a Windows domain, could be something like this (I won't update the above script since I can't really test this properly). Remember to update the list of remote computer names at the top of the script and run with a domain admin account:

# DOMAIN NETWORK: mock-up / pseudo snippet ONLY - lacks testing, provided "as is"
$ArrComputers = "Computer1", "Computer2", "Computer3"
foreach ($Computer in $ArrComputers) 
{
    # here we modify the WMI calls to add machine name
    $wmipackages = Get-WmiObject -Class win32_product -ComputerName $Computer
    $wmiproperties = gwmi  -ComputerName $Computer -Query "SELECT ProductCode,Value FROM Win32_Property WHERE Property='UpgradeCode'"

    # the rest of the above, large script here (minus the first 2 WMI lines)
}

To adapt the same machine loop for a non-domain network you can add credentials to the WMI calls. Something like this (you will be prompted for credentials for each machine - which might be confusing). Remember to update the list of remote computer names at the top of the script and use an account with local admin rights on the target box:

# WORKGROUP NETWORK: mock-up / pseudo snippet ONLY - lacks testing, provided "as is"
$ArrComputers = "Computer1", "Computer2", "Computer3"
foreach ($Computer in $ArrComputers) 
{
     $Cred = Get-Credential

     # here we modify the WMI calls to add machine name AND credentials
     $wmipackages = Get-WmiObject -Class win32_product -ComputerName $Computer -credential $cred
     $wmiproperties = gwmi  -ComputerName $Computer -credential $cred -Query "SELECT ProductCode,Value FROM Win32_Property WHERE Property='UpgradeCode'"

     # the rest of the above, large script here (minus the first 2 WMI lines) 
}

The real answer ends here. I believe the above newer script should cover most use-cases, but I will leave the content below as well since it is not obsolete, just probably less efficient than the above script. Reading it will probably be repetitious.

The scripts below for retrieval of single upgrade codes rather than the whole list, could be of interest if you want to retrieve a single upgrade code from within your own application at run-time. I'll leave that older content in.

Disclaimer


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...