【问题标题】:MsiEnumRelatedProducts returns ERROR_INVALID_PARAMETER with iProductIndex > 0MsiEnumRelatedProducts 返回 ERROR_INVALID_PARAMETER 且 iProductIndex > 0
【发布时间】:2026-01-05 09:20:03
【问题描述】:

我正在尝试编写一个 AutoIt 脚本来卸载具有特定 Upgrade Code 的所有 MSI 软件包。到目前为止,这是我的代码:

$i = 0
Do
  $buffer = DllStructCreate("wchar[39]")
  $ret = DllCall("msi.dll", "UINT", "MsiEnumRelatedProductsW", _
    "wstr", "{a1b6bfda-45b6-43cc-88de-d9d29dcafdca}", _ ; lpUpgradeCode
    "dword", 0, _ ; dwReserved
    "dword", $i, _ ; iProductIndex
    "ptr", DllStructGetPtr($buffer)) ; lpProductBuf
  $i = $i + 1
  MsgBox(0, "", $ret[0] & " " & DllStructGetData($buffer, 1))
Until($ret[0] <> 0)

这可以完美地确定第一个安装产品的产品代码,但是一旦 iProductIndex 增加到 1,它就会返回 87 (ERROR_INVALID_PARAMETER)。通常在 the input GUID is malformed 时会返回此错误,但如果是这种情况,它也不应该与 iProductIndex = 0 一起使用...

我对这段代码的期望(当安装了 2 个具有相同升级代码的软件包时)是:

  1. 打印“0 ”
  2. 打印“0 ”
  3. 打印“259”(ERROR_NO_MORE_ITEMS)

目前在做什么:

  1. 打印“0 ”
  2. 打印“87”(ERROR_INVALID_PARAMETER)

有什么想法吗?

(如果您想在自己的计算机上测试此代码,则需要安装两个具有相同 UpgradeCode 的 MSI 包。这是我的 WiX 测试包:http://pastie.org/3022676

【问题讨论】:

    标签: autoit


    【解决方案1】:

    好的,我找到了一个简单的解决方法:我只需在循环中删除 iProductIndex = 0 时可以找到的所有产品。

    Func GetProduct($UpgradeCode)
      $buffer = DllStructCreate("wchar[39]")
      $ret = DllCall("msi.dll", "UINT", "MsiEnumRelatedProductsW", _
        "wstr", $UpgradeCode, _ ; lpUpgradeCode
        "dword", 0, _ ; dwReserved
        "dword", 0, _ ; iProductIndex
        "ptr", DllStructGetPtr($buffer)) ; lpProductBuf
      Return DllStructGetData($buffer, 1)
    EndFunc
    
    $Last = ""
    $Product = ""
    Do
      $Last = $Product
      $Product = GetProduct("{a1b6bfda-45b6-43cc-88de-d9d29dcafdca}")
      If $Product = "" Then Exit
    
      $Ret = RunWait("msiexec /qn /x " & $Product)
      ConsoleWrite($Ret & " " & $Product & @CRLF)
      If $Product = $Last Then Exit 1
    Until($product = "")
    

    【讨论】:

      【解决方案2】:

      这不起作用,因为使用DllCall() DLL 不会保持打开状态。函数MsiEnumRelatedProducts 可能具有枚举所需的内部状态,并且仅在索引为零时才初始化。当 DLL 关闭时,这种状态就丢失了。

      要解决此问题,请在循环前调用 DllOpen()。在循环运行时保持 DLL 处于打开状态,并将 DLL 句柄而不是其文件名传递给DllCall()。循环结束后使用DllClose()关闭DLL。

      这是一个函数,它返回给定 UpgradeCode 的 ProductCodes 数组。如果函数没有找到任何产品,它会返回Null

      Func GetRelatedProducts( $UpgradeCode )
      
          Local $result[ 1 ]   ; Can't declare empty array :/
      
          Local $dll = DllOpen( "msi.dll" )
          If @error Then Return SetError( 1, @error, Null )
      
          Local $buffer = DllStructCreate( "wchar[39]" )
      
          Local $index = 0
      
          Local $success = False
      
          Do
              Local $ret = DllCall( $dll, "UINT", "MsiEnumRelatedProductsW", _
                  "wstr", $UpgradeCode, _ ; lpUpgradeCode
                  "dword", 0, _ ; dwReserved
                  "dword", $index, _ ; iProductIndex
                  "ptr", DllStructGetPtr($buffer)) ; lpProductBuf
      
              If @error Then 
                  DllClose( $dll )
                  Return SetError( 1, @error, Null )
              EndIf   
      
              $success = $ret[ 0 ] = 0    ; $ret[ 0 ] contains the DLL function's return value
      
              If( $success ) Then
                  Local $productCode = DllStructGetData( $buffer, 1 )
      
                  Redim $result[ $index + 1 ]
                  $result[ $index ] = $productCode 
      
                  $index += 1
              EndIf
          Until( Not $Success )
      
          DllClose( $dll )
      
          if( $index ) Then 
              Return $result 
          Else 
              Return Null 
          EndIf
      EndFunc
      

      用法:

      Local $productCodes = GetRelatedProducts( "{insert-upgradecode-here}" )
      
      If( IsArray( $productCodes ) ) Then
          MsgBox( 0, "Success!", "Found products:" & @CRLF & _ArrayToString( $productCodes, @CRLF ) )
      EndIf
      

      【讨论】: