Martin von Wittich
Martin von Wittich

Reputation: 393

MsiEnumRelatedProducts returns ERROR_INVALID_PARAMETER with iProductIndex > 0

I'm trying to write an AutoIt script that uninstalls all MSI packages with a specific Upgrade Code. This is my code so far:

$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)

This works flawlessly to determine the Product Code for the first installed product, but it returns 87 (ERROR_INVALID_PARAMETER) as soon as iProductIndex is incremented to 1. Usually this error is returned when the input GUID is malformed, but if that would be the case, it shouldn't work with iProductIndex = 0 either...

What I expected from this code (when 2 packages with the same Upgrade Code are installed) is:

  1. Print "0 <first Product Code>"
  2. Print "0 <second Product Code>"
  3. Print "259" (ERROR_NO_MORE_ITEMS)

What it currently does:

  1. Print "0 <first Product Code>"
  2. Print "87" (ERROR_INVALID_PARAMETER)

Any ideas?

(If you want to test this code on your own computer, you will need to have two MSI packages with the same UpgradeCode installed. Here are my WiX test packages: http://pastie.org/3022676 )

Upvotes: 1

Views: 708

Answers (2)

zett42
zett42

Reputation: 27776

This doesn't work because using DllCall() the DLL is not kept open. The function MsiEnumRelatedProducts propably has internal state that is required for the enumeration and is only initialized when the index is zero. When the DLL is closed, this state is lost.

To fix this, call DllOpen() before the loop. Keep the DLL open while the loop is running and pass the DLL handle instead of its filename to DllCall(). Close the DLL using DllClose() when the loop has been finished.

Here is a function that returns an array of ProductCodes for the given UpgradeCode. It returns Null in case the function didn't found any products.

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

Usage:

Local $productCodes = GetRelatedProducts( "{insert-upgradecode-here}" )

If( IsArray( $productCodes ) ) Then
    MsgBox( 0, "Success!", "Found products:" & @CRLF & _ArrayToString( $productCodes, @CRLF ) )
EndIf

Upvotes: 1

Martin von Wittich
Martin von Wittich

Reputation: 393

OK, I've found a simple workaround: I just remove every product I can find with iProductIndex = 0 in a loop.

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 = "")

Upvotes: 2

Related Questions