如何找到已安装 MSI 文件的升级代码?

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

在某些情况下,可能需要检索已部署包的 MSI 升级代码

常见场景:


这是一个 Q/A 风格的问题

这个问题以前曾以各种形式出现过,但这不是重复的。我发布了一种使用 main MSI 自动化接口(或严格来说是 WMI)的方法。它 应该比以前答案中基于注册表的方法更可靠 。这个答案也试图总结其他检索方法。

MSI 升级代码检索(通过 PowerShell / WMI)

UPDATE: HTML Export VBScript - .

Uninstalling?:

下面的 PowerShell 脚本 应该检索所有相关的 产品代码升级代码产品名称 安装在您的机器上(table 输出)。

输出的屏幕截图(完整脚本如下):

这些是真实的实时值,直接来自相关机器上的Windows安装程序数据库。不需要任何转换或解释。我们正在通过适当的 API。

技术说明!:请注意,直接在原始 MSI 文件 (属性 table) 或 WiX 源文件中检查属性可能不匹配实际安装的值,因为可以在安装时通过 transforms(更多信息见下文)或在命令行指定的 属性 值覆盖属性。故事的寓意:尽可能直接从系统中检索 属性 值。

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.

题外话,还有一个 one-line PowerShell 命令,它将仅检索产品代码和升级代码 - 不包含包名称。这对于某些用户来说实际上可能就足够了(不过我会推荐下面的完整脚本)。下面的部分中有此 one-liner 输出的屏幕截图。 注意:此命令比更大的脚本出现(“值”字段是升级代码)。另请注意:据我所知,没有相关升级代码的产品代码将不会显示 - 它们将显示在较大的脚本中:

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

到 运行 下面的完整 PowerShell 脚本:

  1. 启动 PowerShell按住 Windows 键,点击 R,松开 Windows 键,输入“powershell”然后按 OK 或回车 ).
  2. 完整复制下面的脚本,然后 在 PowerShell window.[=282 中右键单击=]
  3. 这应该会启动脚本,运行需要相当长的时间。
  4. 请报告任何问题。我不是 PowerShell 专家 - 我是部署专家而不是编码员,但脚本应该可以完成这项工作。
  5. 性能说明:我刚得到整个Win32_Product WMI对象
    • Cherry picking 属性似乎实际上使速度稍慢(VBScript 测试)。
    • 我猜我们无论如何都需要获取所有行,而挑选列只是额外的提升?
    • 对于Win32_Property我们过滤行和列(升级代码只是众多row-types之一)。做好运行缓慢的准备,WMI很慢。
$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

运行 在远程机器上

  • 在远程机器上将上面的脚本扩展到 运行 应该相对容易,但我目前还没有正确测试它的设置。
  • 下面的信息有点乱,如果有不明白或不清楚的地方请告诉我。
  • 真正的Windows域中(理论上)应该只是将远程机器添加到WMI调用本身(并循环列表)机器 - 请参阅下面的 mock-up)。 关键是:您应该使用真正的域管理员帐户来 运行 查询 。有可能我在下面列出的使 WMI 在工作组环境中工作的更改对于某些域也可能是必需的,我不知道(防火墙规则和 UAC 注册表调整)。我想真正的域管理员帐户应该具有所需的权限和访问权限。
  • WMI 中的远程连接(至少)受 Windows 防火墙DCOM 设置CIMOM 设置 用户帐户控制 (UAC)(加上任何其他 non-Microsoft 因素 - 例如真正的防火墙、第三方软件防火墙、安全软件各种等等...)。以下是一些细节:
  • non-domain 网络(小型办公室、家庭等)中,您可能必须将用户凭据直接添加到 WMI 调用才能使其正常工作。您可能必须在相关机器上拥有“真正的管理员权限”才能在家庭网络(工作组)中远程进行查询 运行。听说built-in Administrator帐号没有任何UAC问题,但我没试过。我认为:不要使用此帐户。
    • 在我的测试中 我不得不 (1) 更新 Windows 防火墙规则和 ( 2) 禁用远程 UAC 访问令牌过滤并在远程系统上使用真实的本地管理员帐户。请注意,我不推荐这些更改中的任何一个,只是报告对我有用的内容。
    • 更改 1:Windows 防火墙,运行 命令(cmd.exe,运行 作为管理员):netsh advfirewall firewall set rule group="windows management instrumentation (wmi)" new enable=yessource - 请参阅此 link 命令行以再次禁用此新规则如果你只是测试。基本上只是设置 enable=no)。请参阅 linked 来源,了解可能也适用的限制性更强的规则。
    • 更改 2:禁用远程 UAC 访问令牌过滤:您需要设置以下注册表值:HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\ LocalAccountTokenFilterPolicy = 1source - 中间页,后面一半)。我设置了一个32位的DWORD。

随着远程系统上的这些更改,我还通过提示用户 $Cred = Get-Credential 向每个调用添加了用户凭据。还有更多用于定义用户凭据的高级选项,如此处所述: (and here)。为了测试 运行,这里有一个小测试脚本。复制以下所有行,修改远程计算机名称并通过右键单击粘贴到 PowerShell(系统将提示您输入凭据):

$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

对于上面的大型 PowerShell 脚本,在 Windows 域 中的多台计算机上远程 运行ning 的基本添加可能是这样的(我不会更新上面的脚本,因为我无法真正正确地测试它)。请记住使用域管理员帐户更新脚本顶部的远程计算机名称列表和 运行:

# 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)
}

要为 non-domain 网络 调整相同的机器循环,您可以向 WMI 调用添加凭据。类似这样的事情(系统会提示您输入每台机器的凭据 - 这可能会造成混淆)。请记住更新脚本顶部的远程计算机名称列表,并在目标框上使用具有本地管理员权限的帐户:

# 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) 
}

真正的答案到此为止。我相信上面较新的脚本应该涵盖大部分 use-cases,但我也会保留下面的内容,因为它并没有过时,只是效率可能不如上面的脚本。读起来估计会重复

如果您想从位于 run-time 的自己的应用程序中检索单个升级代码,那么下面用于检索单个升级代码而不是整个列表的脚本可能会很有趣。我将保留较旧的内容。

Disclaimer: The above script uses WMI, and when you access the class Win32_Product it triggers an integrity check of installed packages. This is quite slow, and can in very special cases trigger an MSI self-repair. This is not good if you are heading into an important meeting :-). Luckily you should be able to cancel any triggered self-repairs (but your query will probably not complete until you let the repair finish). Quick context link (for safekeeping).

IMHO: don't let this stop you from using WMI - it is just an annoyance. Note: both the PowerShell and VBScript approaches described below use WMI and can trigger this issue as well.


正在为未安装的 MSI 文件检索升级代码

如果您的计算机上未安装的 MSI 软件包需要升级代码,请阅读“升级代码的手动检索" 几个选项底部的部分(基本上查看 MSI 文件本身,或者用于编译它的源文件)。

从原始 MSI 安装文件本身或用于编译 MSI 的 (WiX) 源获取 已安装软件包 的升级代码是不安全的,因为 升级代码可以在安装时使用 transforms 覆盖(详情见下文 - 转换是在安装时应用的小数据库片段,有关详细信息,请参阅 Symantec link)。

升级代码的编程检索依赖于 WMI,您可以使用 PowerShellVBScript 来调用 WMI。下面介绍了这两种方法。本质上,以下 WMI 查询 是 运行 检索指定产品代码的升级代码:

SELECT * FROM Win32_Property WHERE Property='UpgradeCode' AND ProductCode='{YourProdGuid}'

这是用于 VBScript 和 PowerShell 的相同查询。您还可以 运行 使用诸如 WMIExplorer.exe 之类的工具将其作为直接 WMI 查询。一个非常有用的工具 - 强烈推荐。我相信这是他们的网站:https://github.com/vinaypamnani/wmie2/releases


通过 PowerShell/WMI 检索单个升级代码

您可以检索指定产品代码的单个升级代码,而不是输出包含所有产品代码和升级代码的完整 table。如果您尝试从您自己的应用程序代码内部进行检索(那么它只是一个标准的 WMI 查询,与 PowerShell 无关),这很好。

下面是通过 PowerShell 完成的单个升级代码检索(启动 PowerShell:按住 Windows 键,点击 R,释放 Windows 键,输入“ powershell" 然后按 OK 或回车 ):

gwmi -Query "SELECT Value FROM Win32_Property WHERE Property='UpgradeCode' AND ProductCode='{YourGuid}'" | Format-Table Value

输出应该是这样的(可能有点难读,我应该使用更大的字体):

上面查询中指定的产品代码适用于“Windows SDK Intellidocs”。您显然必须将其替换为您自己的产品代码 guid。要查找您需要传入的产品代码,您还可以使用此处所述的 PowerShell 查询:

返回的升级代码直接来自真实的 Windows 安装程序注册表数据库。它不需要进一步的处理或解释或手动转换步骤。它也将是正确的,即使在安装 MSI 时转换更改了原始升级代码(下面有关转换问题的详细信息)。

[=90=更新,特别通知:为了避免不必要的复杂化,我相信我在 WMI 中发现了一个非常具体的错误。当原始 MSI 没有升级代码集,而您通过转换添加一个时,WMI 似乎根本不报告升级代码。但是:如果原始 MSI 有升级代码,并且您在转换中覆盖它,WMI 会报告转换的升级代码(这是预期的)。我肯定看到了这一点,但需要再用一个测试包进行验证才能确定。 故事的寓意:始终在您的 MSI 中设置升级代码!然后你就永久地避免了整个问题。并且不要 auto-generate 它 - 对其进行硬编码(阅读下面的“升级代码的手动检索”以获得解释)。


使用 VBScript/WMI(旧方法)检索单个升级代码

下面发现的 VBScript 解决方案没有任何问题 - 它甚至比 PowerShell 有一些优势 - 尽管 VBScript 现在是一项遗留技术。好处是它应该可以在所有机器上运行,即使在缺少(或锁定).NET 框架的情况下,以及在缺少(或锁定)PowerShell 的机器上也是如此。这是一个过时但可行的解决方案,非常灵活(除非 VBScript 也被锁定,但所有现代 OS 版本都完全支持 VBScript)。

为了尽可能简单地检索您的升级代码,我创建了一个“bare-bone VBScript”,它应该可以解决问题。它尚未针对远程计算机进行测试,即使 WMI 应该能够通过设计这样做。该脚本旨在 运行 在安装了具有未知升级代码的神秘 MSI 的系统上。

此VBScript需要输入产品代码(脚本为运行时显示的输入对话框),然后它会继续查找相应的升级代码(如果有)。如上所述,要找到 MSI 的产品代码,您可以使用以下方法:。获得产品代码 (guid) 后,您可以在目标机器上 运行 此 VBScript,您应该会在几秒钟内获得返回给您的升级代码。 WMI 检索可能非常慢。

'
' Purpose: Barebone / minimal VBScript implementation to allow retrieval of MSI UpgradeCodes via WMI.
'
' Version: 0.2, September.2017 - Stein Åsmul.
'
' Notes:
'
'  - As it stands, this script is intended to be run interactively (WScript).
'  - Conversion to run via CScript should be trivial (nothing ever is...)
'  - The script will ask the user to provide a valid product GUID for an installed MSI.
'  - To find a valid product GUID for your system, perhaps see this SO answer: 
'  - The script does not RegEx anything to check for valid GUID format (this is barebone - as terse as possible,
'    with as little as possible included that can break).
'
' UPDATE: for information on remote running, check "Running on remote machines" section here:
'  (firewall and registry change seems to be needed).

strComputer = "."
' Remote connections was NOT tested for this script. In principle you should just add the machine name to "strComputer" above.
' AFAIK you must have "real" admin rights on the box you try to connect to. Many users report intermittent problems running remote WMI.
' Remote connections in WMI are affected by the Windows Firewall, DCOM settings, and User Account Control (UAC).
'    - Setting up a Remote WMI Connection: https://msdn.microsoft.com/en-us/library/aa822854(v=vs.85).aspx
'    - Connecting to WMI on a Remote Computer: https://msdn.microsoft.com/en-us/library/aa389290(v=vs.85).aspx
'    - Perhaps useful: https://social.technet.microsoft.com/Forums/lync/en-US/05205b52-0e43-4ce3-a8b8-58ec4c2edea5/wmi-generic-failure-when-accessing-win32product-remotely?forum=winserverManagement
'    - Maybe it is also worth noting that I think WMI queries can be slow enough to trigger timeouts,
'      and then you have the old favorite: intermittent bugs.

Set owmi = GetObject("winmgmts:{impersonationLevel=impersonate}!\" & strComputer & "\root\cimv2")

' User interaction
productcode = InputBox("Please paste or type in the product code for the product whose upgrade code you want " + _
                       "to retrieve (not case sensitive, a blank product code will abort the script)." + vbNewLine + vbNewLine + _
                       "Please note that the script can take up to a minute to run due to WMI's slowness.", "UpgradeCode retrieval:")
If productcode = vbCancel Or Trim(productcode) = "" Then
   WScript.Quit(0)
End If

' Run WMI call and verify that it completes successfully.
On Error Resume Next
Set upgradecode = owmi.ExecQuery("SELECT Value FROM Win32_Property WHERE Property='UpgradeCode' AND ProductCode='" & productcode & "'")
If (Err.number <> 0) Then
   MsgBox "The WMI query failed, this is a critical error - aborting.", vbCritical, "Fatal error."
   WScript.Quit(2) ' Following exit code "standard" from MSI SDK automation samples
End If
On Error GoTo 0

' Report results.
Select Case upgradecode.count

   Case 0
       ' We have to provide a separate message for this state, since some packages may not have an UpgradeCode.
       ' However, the product GUID could also have been misspelled.
       MsgBox "No UpgradeCode was found, are you sure you entered the correct product GUID?" & vbNewLine & vbNewLine & _
              "Note: It is possible for a product to NOT have an UpgradeCode.", vbInformation, "No UpgradeCode found."

   Case 1
      ' The "default state" - should cover almost all normal packages.

      ' Only one upgrade code should have been retrieved, and it can be referenced by upgradecode.ItemIndex(0).Value on newer systems 
      ' (Vista and later), but on XP this apparently does not work (never tested by me), for compatibility we use a standard For Each 
      ' enumeration instead. Source: 

      For Each u in upgradecode
        Msgbox "The Upgrade Code is: " & u.Value & vbNewLine & vbNewLine & _
              "Just press CTRL + C to copy all text in this dialog (then paste to notepad or similar to extract the GUID).", _
              vbInformation, "UpgradeCode found."
          ' Exit For
      Next

   Case Else
       ' Should never get here - let us know if you do get this message.
       MsgBox "An error occurred, the query returned more than one result. There can only be one UpgradeCode. " & _ 
              "Please report this error on Whosebug", vbInformation, "Error while retrieving UpgradeCode."
End Select

检索机器上的所有升级代码和产品代码

I should mention that I have a large VBScript which will generate a comprehensive HTML report for all installed MSI packages on the machine it runs on. This includes all upgrade code and a list of related product codes (product codes that share the same upgrade code). However, I am not too happy with the code (I am a deployment specialist, not a coder). The script is too large, too slow and too untested for use, so I create the bare-bone VBScript found above to do retrieval for a single package only. This script is much easier to test and modify for your own use. I can provide this large VBScript for testing if of interest. It is read-only apart from a single HTML file output to "My Documents". It should be possible to adapt this script for use on remote computers as well.

有一个 one-line PowerShell 命令 可以检索所有产品代码和相关升级代码,但此输出填充缺少产品名称。为了完整起见,我将其包含在这里:

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

输出将类似于此(“值”字段是升级代码 - 据我所知,没有关联升级代码的产品代码不会显示):


手动检索升级代码

本部分列出了一些不需要任何编码或命令行即可检索升级代码的“手动方法”。这些手动方法 不是 推荐的方法。我将它们包括在内只是因为这试图成为“参考答案”。应该提供几个不同的选项。 我的建议 是使用上面提供的 PowerShell 或 VBScript。

话虽如此,升级代码通常不应在您的产品版本之间发生变化,因此您可以尝试在 MSI 文件本身或用于编译它的源代码中找到的代码,如下所述。已经多次提到的问题是转换可以在安装时更改升级代码,因此如果您想确保找到正确的升级代码,则需要以编程方式检索升级代码。除非您尝试从系统上未安装的 MSI 获取升级代码。那么您只需要一个 MSI 文件查看器,如下面要点 1 中所述。

A transform 只是一个 数据库片段 ,在安装时对原始 MSI 进行了更改。它是一种主要用于企业应用程序打包 的工具,可以在不直接修改 MSI 文件的情况下修改安装程序。转换的扩展名为 .mst。通过转换更改升级代码是不常见的,但并非闻所未闻 - 特别是对于企业重新打包。在 极少数情况下 应用程序打包者可能会故意更改升级 GUID 以使他们能够将自己的升级交付到已安装的软件包(而不是直接依赖供应商更新)。很少见,但我已经看到了。这是否是一件好事是高度评价table。

轻松,手动方式查找 MSI 升级代码:

  1. 虽然很明显,但找到升级代码的最简单方法是 打开用于安装产品的原始 MSI找到属性table中的升级代码。您所需要的只是一个能够打开 MSI 文件的工具。这里有一些工具:What installation product to use? InstallShield, WiX, Wise, Advanced Installer, etc。你最快的赌注可能是 Ora 如果你安装了 Visual Studio(搜索 Orca-x86_en-us.msi 并安装它 - 这是微软自己的官方 MSI 查看器和编辑器),或者如果你没有安装 Visual Studio Super Orca(按照上面的link找到它)。

  2. 如果您是使用 WiX(或任何其他部署工具)的开发人员,显然您可以 在您的 WiX 源文件 中轻松找到升级代码用于编译您的 MSI(或 Installshield 源、Advanced Installer 源或您正在使用的任何部署工具)。

    • 我们不要在这里用太多善意的建议来扰乱主要问题,但显然你应该 硬编码升级代码 在你的源代码中,并且 从不 auto-generate 它!
    • 升级代码定义了“相关产品系列”并且应该在发布(版本)之间保持 stable。在大多数情况下,跨语言版本它也应该保持 stable。确切的设置取决于部署要求。
    • 如果产品应该能够存在 side-by-side 您通常对需要 co-exist 的产品有不同的升级代码。
    • 经验法则:尽可能长时间保留升级代码 stable。当需求绝对需要时更改它们。
    • 总结一下:切勿对具有自己的“生命周期”并且彼此没有实际关系的不同产品使用相同的升级代码。他们没有关系。这与保留相关产品的升级代码 stable 一样重要。想想“生命周期”和“家庭关系”和“co-existence”的要求.
    • 这是一个很大的题外话,回到手头的问题:寻找升级代码。
  3. 即使您没有原始 MSI,也可以在 %SystemRoot%\Installer 文件夹。这里的 MSI 文件有一个神秘的 hex-name,但它们只是用于安装不同产品的原始 MSI 文件的副本 - 缓存在安全的地方以供修改、修复和卸载操作使用。 不管你做什么,都不要在这个文件夹里乱来。永远,永远不要删除任何东西。您可以找到安装您产品的 MSI,方法是选择第一个 MSI 文件,然后检查 Windows Explorer 状态栏什么是旧 Windows 版本的产品名称。在 Windows 10 中,您似乎可以使用指针将鼠标悬停在 MSI 上,然后您会得到一个带有一些 MSI 详细信息的 pop-up。然后您只需单击列表直到找到正确的产品并打开 MSI 并在 属性 table.

    中找到升级代码
  4. 有些人使用注册表读取升级代码:How can I find the upgrade code for an installed application in C#?. In my opinion this is not a good approach, there are better ways - such as just using PowerShell as explained above. There is no need for all this conversion and interpretation of packed GUIDs(这是 Windows 安装程序注册表数据库中使用的 GUID 格式)。

这应该完成主要的“手动方法”以快速检索升级代码。只是一些武器库的方法有时就足够了。可能还有几种方法我忘记了。

确实更喜欢程序化方法,但如果您赶时间并且在没有所有可用工具的情况下工作,一些手动选项也不错。然而,这些手动方法中的一些需要比 PowerShell 命令行更多的工具(如果你正在对某人的机器执行“支持任务”,则你需要一个 MSI 文件查看器,它并不总是在盒子上可用)。是时候使用 PowerShell 了(是的,我也觉得落伍了)。

顺便说一下,MSI 文件本质上是精简的 SQL 服务器数据库存储为 COM-structured 存储文件(MS Office 文件格式)。本质上是文件中的文件系统,具有各种类型的存储流。

如果您无法使用没有 MSI 查看器的计算机,您可以直接从 PowerShell 查询缓存的 MSI 数据库:

为了满足您直接使用 WMI 的要求,或者那些时候您只需要一次性使用而无需 Powershell(或需要使用 .bat 或其他),请使用 wmic:

    C:\>wmic product list brief
    Caption                                                                                              IdentifyingNumber                       Name                                                                                                 Vendor                          Version
        Sourcetree                                                                                           {1B05DFFD-1DB9-48CD-9265-F3976512A579}  Sourcetree                                                                                           Atlassian                       2.6.10.0
        Microsoft Office Access database engine 2007 (English)                                               {90120000-00D1-0409-0000-0000000FF1CE}  Microsoft Office Access database engine 2007 (English)                                               Microsoft Corporation           12.0.4518.1031
        Office 16 Click-to-Run Extensibility Component                                                       {90160000-008C-0000-0000-0000000FF1CE}  Office 16 Click-to-Run Extensibility Component    

有多种格式和输出选项。