How do I add resources to my AutoIt exe file? - autoit

I want to include a few txt files to my AutoIT application.
#AutoIt3Wrapper_Res_Field=#AutoIt3Wrapper_icon|C:\Scripts\AutoIt\HelpDeskIT\images\helpdesk_icon.ico
#AutoIt3Wrapper_Res_File_Add=C:\Scripts\AutoIt\HelpDeskIT\Instructions.txt
#AutoIt3Wrapper_Res_File_Add=C:\Scripts\AutoIt\HelpDeskIT\reportaproblem.txt
but it's not being added to my exe file. (when I launch the app it can't find the files)

From the SciTE4AutoIt3 help (under Extra Utilities --> AutoIt3Wrapper)...
Adding a file:
Files are stored in the RT_RCDATA section and are usually stored byte
for byte. If the section value is set to -10 (RT_RCDATA is internally
evaluated as 10) then the file is compressed before being added:
#AutoIt3Wrapper_Res_File_Add=file_name, RT_RCDATA : UNCOMPRESSED
#AutoIt3Wrapper_Res_File_Add=file_name, 10 ; UNCOMPRESSED
#AutoIt3Wrapper_Res_File_Add=file_name, -10 ; COMPRESSED
When the file is required it can be extracted using this code. Set the
$isCompressed parameter to match the Res_File_Add directive:
#include <WinAPIRes.au3>
#include <WinAPIInternals.au3>
Func _FileInstallFromResource($sResName, $sDest, $isCompressed = False, $iUncompressedSize = Default)
Local $bBytes = _GetResourceAsBytes($sResName, $isCompressed, $iUncompressedSize)
If #error Then Return SetError(#error, 0, 0)
FileDelete($sDest)
FileWrite($sDest, $bBytes)
EndFunc
Func _GetResourceAsBytes($sResName, $isCompressed = False, $iUncompressedSize = Default)
Local $hMod = _WinAPI_GetModuleHandle(Null)
Local $hRes = _WinAPI_FindResource($hMod, 10, $sResName)
If #error Or Not $hRes Then Return SetError(1, 0, 0)
Local $dSize = _WinAPI_SizeOfResource($hMod, $hRes)
If #error Or Not $dSize Then Return SetError(2, 0, 0)
Local $hLoad = _WinAPI_LoadResource($hMod, $hRes)
If #error Or Not $hLoad Then Return SetError(3, 0, 0)
Local $pData = _WinAPI_LockResource($hLoad)
If #error Or Not $pData Then Return SetError(4, 0, 0)
Local $tBuffer = DllStructCreate("byte[" & $dSize & "]")
_WinAPI_MoveMemory(DllStructGetPtr($tBuffer), $pData, $dSize)
If $isCompressed Then
Local $oBuffer
_WinAPI_LZNTDecompress($tBuffer, $oBuffer, $iUncompressedSize)
If #error Then Return SetError(5, 0, 0)
$tBuffer = $oBuffer
EndIf
Return DllStructGetData($tBuffer, 1)
EndFunc
Func _WinAPI_LZNTDecompress(ByRef $tInput, ByRef $tOutput, $iUncompressedSize = Default)
; if no uncompressed size given, use 16x the input buffer
If $iUncompressedSize = Default Then $iUncompressedSize = 16 * DllStructGetSize($tInput)
Local $tBuffer, $ret
$tOutput = 0
$tBuffer = DllStructCreate("byte[" & $iUncompressedSize & "]")
If #error Then Return SetError(1, 0, 0)
$ret = DllCall("ntdll.dll", "long", "RtlDecompressBuffer", "ushort", 2, "struct*", $tBuffer, "ulong", $iUncompressedSize, "struct*", $tInput, "ulong", DllStructGetSize($tInput), "ulong*", 0)
If #error Then Return SetError(2, 0, 0)
If $ret[0] Then Return SetError(3, $ret[0], 0)
$tOutput = DllStructCreate("byte[" & $ret[6] & "]")
If Not _WinAPI_MoveMemory(DllStructGetPtr($tOutput), DllStructGetPtr($tBuffer), $ret[6]) Then
$tOutput = 0
Return SetError(4, 0, 0)
EndIf
Return $ret[6]
EndFunc

Related

Object Shell.Application Open Folder In Same Explorer Window

I want to ask you a question regarding Shell.Application COM Object. The programming language I work in is AutoIT. I would like to know, how can I open a folder in the exact same Window a previous folder has been opened.
For instance, if I open the C:\Folder in a Window and it has a subfolder, then how can I make the Script open that subfolder in the same window the "parent folder" has been opened, without creating a new window for that. The only way I can open a Folder is like this:
global $ShellApplication = ObjCreate("Shell.Application")
$ShellApplication.Explore("C:\Folder")
Sleep(100)
$ShellApplication.Explore("C:\Folder\Subfolder")
but this way is the wrong way. I hope someone has got any idea about how this could be achieved.
I don't know whether this is what you want, but you can automate the current window like this:
#include <Array.au3>
Opt("SendKeyDelay", 1) ;5 milliseconds
Opt("SendKeyDownDelay", 1) ;1 millisecond
$pid = ShellExecute("explorer.exe ", #DesktopDir)
Sleep(300)
;~ $aWinList=WinList("[REGEXPCLASS:(Explore|Cabinet)WClass]")
;~ _ArrayDisplay($aWinList)
ConsoleWrite($pid & #CRLF)
$handle = _WinGetHandleByPID($pid)
Sleep(2000)
SendKeepActive($handle)
Send('{F4}')
Send('^a')
Send(#AppDataCommonDir & "{ENTER}")
Func _WinGetHandleByPID($vProcess, $nShow = 1)
; Get Window Handle by PID
; Author Hubertus
; derived from Smoke_N's script with the same name,
; but to handle more than one process with the same name
; and to return handle of newest window ($nShow = 2).
;
; $vProcess = Processname(e.g. "Notepad.exe") or Processnumber(e.g. 4711 )
; $nShow = -1 "All (Visble or not)", $nShow = 0 "Not Visible Only", $nShow = 1 "Visible Only", $nShow = 2 "return handle of newest window "
; #error = 0 Returns array of matches. Array[0][0] and #extended shows number of matches.
; #error = 0 and $nShow = 2 return handle of newest window
; Array[n][0] shows windows title. Array[n][1] shows windows handle. n = 1 to #extended
; #error = 1 Process not found.
; #error = 2 Window not found.
If Not ProcessExists($vProcess) Then Return SetError(1, 0, 0) ; no matching process
Local $iWinList, $aWinList = WinList()
Local $iResult, $aResult[UBound($aWinList)][2]
Local $iProcessList, $aProcessList = ProcessList($vProcess)
If $aProcessList[0][0] = 0 Then Local $aProcessList[2][2] = [[1, 0], ["", $vProcess]]
For $iWinList = 1 To $aWinList[0][0]
For $iProcessList = 1 To $aProcessList[0][0]
If WinGetProcess($aWinList[$iWinList][1]) = $aProcessList[$iProcessList][1] Then
If $nShow > -1 And Not $nShow = (2 = BitAND(WinGetState($aWinList[$iWinList][1]), 2)) Then ContinueLoop
$iResult += 1
$aResult[$iResult][0] = $aWinList[$iWinList][0]
$aResult[$iResult][1] = $aWinList[$iWinList][1]
EndIf
Next
Next
If $iResult = 0 Then Return SetError(2, 0, 0) ; no window found
ReDim $aResult[$iResult + 1][2]
$aResult[0][0] = $iResult
If $nShow = 2 Then Return SetError(0, $iResult, $aResult[1][1])
Return SetError(0, $iResult, $aResult)
EndFunc ;==>_WinGetHandleByPID

winhttp.winhttprequest.5.1 with progress bar

It is possible, in AU3, use WinHttp.WinHttpRequest.5.1 with a progress bar? Actually what I need is to know how many MB have already been downloaded.
I need to use "WinHttp.WinHttpRequest.5.1" because I have to use the parameter "Referer" in the header.
Of course it is
#include "WinHttp.au3"
; Download some gif
;~ http://33.media.tumblr.com/dd3ffab90cc338666f192fd86f6a4f8f/tumblr_n0pefhIpss1swyb6ao1_500.gif
; Initialize and get session handle
$hOpen = _WinHttpOpen()
; Get connection handle
$hConnect = _WinHttpConnect($hOpen, "http://33.media.tumblr.com")
; Specify the reguest
$hRequest = _WinHttpOpenRequest($hConnect, Default, "dd3ffab90cc338666f192fd86f6a4f8f/tumblr_n0pefhIpss1swyb6ao1_500.gif")
; Send request
_WinHttpSendRequest($hRequest)
; Wait for the response
_WinHttpReceiveResponse($hRequest)
;~ ConsoleWrite(_WinHttpQueryHeaders($hRequest) & #CRLF)
ProgressOn("Downloading", "In Progress...")
Progress(_WinHttpQueryHeaders($hRequest, $WINHTTP_QUERY_CONTENT_LENGTH))
Local $sData
; Check if there is data available...
If _WinHttpQueryDataAvailable($hRequest) Then
While 1
$sChunk = _WinHttpReadData_Ex($hRequest, Default, Default, Default, Progress)
If #error Then ExitLoop
$sData &= $sChunk
Sleep(20)
WEnd
Else
MsgBox(48, "Error", "Site is experiencing problems (or you).")
EndIf
Sleep(1000)
ProgressOff()
; Close handles
_WinHttpCloseHandle($hRequest)
_WinHttpCloseHandle($hConnect)
_WinHttpCloseHandle($hOpen)
; Do whatever with data, write to some file or whatnot. I'll just print it to console here:
ConsoleWrite($sData & #CRLF)
Local $hFile = FileOpen(#DesktopDir & "\test.gif", 26)
FileWrite($hFile, $sData)
FileClose($hFile)
Func Progress($iSizeAll, $iSizeChunk = 0)
Local Static $iMax, $iCurrentSize
If $iSizeAll Then $iMax = $iSizeAll
$iCurrentSize += $iSizeChunk
Local $iPercent = Round($iCurrentSize / $iMax * 100, 0)
ProgressSet($iPercent, $iPercent & " %")
EndFunc
Func _WinHttpReadData_Ex($hRequest, $iMode = Default, $iNumberOfBytesToRead = Default, $pBuffer = Default, $vFunc = Default)
__WinHttpDefault($iMode, 0)
__WinHttpDefault($iNumberOfBytesToRead, 8192)
__WinHttpDefault($vFunc, 0)
Local $tBuffer, $vOutOnError = ""
If $iMode = 2 Then $vOutOnError = Binary($vOutOnError)
Switch $iMode
Case 1, 2
If $pBuffer And $pBuffer <> Default Then
$tBuffer = DllStructCreate("byte[" & $iNumberOfBytesToRead & "]", $pBuffer)
Else
$tBuffer = DllStructCreate("byte[" & $iNumberOfBytesToRead & "]")
EndIf
Case Else
$iMode = 0
If $pBuffer And $pBuffer <> Default Then
$tBuffer = DllStructCreate("char[" & $iNumberOfBytesToRead & "]", $pBuffer)
Else
$tBuffer = DllStructCreate("char[" & $iNumberOfBytesToRead & "]")
EndIf
EndSwitch
Local $sReadType = "dword*"
If BitAND(_WinHttpQueryOption(_WinHttpQueryOption(_WinHttpQueryOption($hRequest, $WINHTTP_OPTION_PARENT_HANDLE), $WINHTTP_OPTION_PARENT_HANDLE), $WINHTTP_OPTION_CONTEXT_VALUE), $WINHTTP_FLAG_ASYNC) Then $sReadType = "ptr"
Local $aCall = DllCall($hWINHTTPDLL__WINHTTP, "bool", "WinHttpReadData", _
"handle", $hRequest, _
"struct*", $tBuffer, _
"dword", $iNumberOfBytesToRead, _
$sReadType, 0)
If #error Or Not $aCall[0] Then Return SetError(1, 0, "")
If Not $aCall[4] Then Return SetError(-1, 0, $vOutOnError)
If IsFunc($vFunc) Then $vFunc(0, $aCall[4])
If $aCall[4] < $iNumberOfBytesToRead Then
Switch $iMode
Case 0
Return SetExtended($aCall[4], StringLeft(DllStructGetData($tBuffer, 1), $aCall[4]))
Case 1
Return SetExtended($aCall[4], BinaryToString(BinaryMid(DllStructGetData($tBuffer, 1), 1, $aCall[4]), 4))
Case 2
Return SetExtended($aCall[4], BinaryMid(DllStructGetData($tBuffer, 1), 1, $aCall[4]))
EndSwitch
Else
Switch $iMode
Case 0, 2
Return SetExtended($aCall[4], DllStructGetData($tBuffer, 1))
Case 1
Return SetExtended($aCall[4], BinaryToString(DllStructGetData($tBuffer, 1), 4))
EndSwitch
EndIf
EndFunc
You here find the WinHTTP.au3 here: https://github.com/dragana-r/autoit-winhttp/releases.
It is my favorite UDF!
I have comments all around the code to explain you the process.

Autoit Add data instead of replace data

I start a server with:
$BungeeServer = Run("java -Xmx512M -jar " & '"' & $file0 & "\BungeeCord.jar" & '"', $file0, $Hide, $STDERR_MERGED)
Then I get the data from the server with(It's in a while loop):
Assign("sOutput", StdoutRead($BungeeServer, False, False) & #CRLF)
GUICtrlSetData($Edit1, $sOutput)
The problem is that It only returns the latest string of information. I need it to add instead of replace the values. Anybody know an easy way to do that?
Edit: When a server starts, you normally get a cmd window. I want to be able to have it in a Gui instead. Also I only want it to appear when I click a button, still it should start getting data when the server starts. The lines of text also went outside the box. I tried:
Do
$msg1 = GUIGetMsg()
$line1 = StdoutRead($BungeeServer, True)
$totalOutput1 &= $line1 & #CRLF
GUICtrlSetData($Edit1, $totalOutput1)
$line2 = StdoutRead($1, True)
$totalOutput2 &= $line2 & #CRLF
GUICtrlSetData($Edit2, $totalOutput2)
$line3 = StdoutRead($2, True)
$totalOutput3 &= $line3 & #CRLF
Until $msg = $GUI_EVENT_CLOSE
The edit boxes were made with:
$Form2 = GUICreate("Servers", #DesktopWidth - 15, #DesktopHeight - _GetTaskBarHeight() - 35, -1, -1)
$Edit1 = GUICtrlCreateEdit("", 0, 0, 634, 334, BitOR($ES_AUTOVSCROLL, $ES_AUTOHSCROLL, $ES_READONLY, $ES_WANTRETURN, $WS_VSCROLL))
Font()
$Edit2 = GUICtrlCreateEdit("", 635, 0, 634, 334, BitOR($ES_AUTOVSCROLL, $ES_AUTOHSCROLL, $ES_READONLY, $ES_WANTRETURN, $WS_VSCROLL))
Font()
$Edit3 = GUICtrlCreateEdit("", 634 + 634, 0, 634, 334, BitOR($ES_AUTOVSCROLL, $ES_AUTOHSCROLL, $ES_READONLY, $ES_WANTRETURN, $WS_VSCROLL))
Font()
GUISetState(#SW_SHOW)
Edit: I found a better way to do it where I made a child window and didn't need all the other stuff:
$GUI = GUICreate("Consoles", 1020, 600, 1282, 300, BitOR($WS_MINIMIZEBOX, $WS_SYSMENU, $WS_CAPTION, $WS_CLIPCHILDREN, $WS_POPUP, $WS_POPUPWINDOW, $WS_GROUP, $WS_BORDER, $WS_CLIPSIBLINGS))
If GUICtrlRead($Bungee) = 1 Then
$BungeeServer = Run("java -Xmx512M -jar " & '"' & $file0 & "\BungeeCord.jar" & '"', $file0, $Hide)
If Not ProcessWait($BungeeServer) = 0 Then
WinSetTitle("C:\Windows\system32\java.exe", "", "Bungee")
WinSetTitle("C:\WINDOWS\SYSTEM32\java.exe", "", "Bungee")
Global $hwnd0 = WinGetHandle("Bungee")
EndIf
EndIf
_WinAPI_SetWindowLong($hwnd0, $GWL_EXSTYLE, $WS_EX_MDICHILD)
_WinAPI_SetParent($hwnd0, $GUI)
WinMove($hwnd0, "", 0, 0, 340, 300)
Still need to find a way to use ControlSend to the child windows :/
Use StdoutRead($BungeeServer, True)
StdoutRead ( process_id [, peek = false [, binary = false]] )
peek [optional] If true the function does not remove the read characters from the stream.
OR
Consider storing the previous data before getting the new one. Instead of assigning the new data to the variable, you can just add it.
eg.
$totalOutput &= $newOutput & #LF
About StdoutRead: In order to get all data, it must be called in a loop.
From the help file:
; Demonstrates StdoutRead()
#include <Constants.au3>
Local $foo = Run(#ComSpec & " /c dir foo.bar", #SystemDir, #SW_HIDE, $STDERR_CHILD + $STDOUT_CHILD)
Local $line
While 1
$line = StdoutRead($foo)
If #error Then ExitLoop
MsgBox(0, "STDOUT read:", $line)
WEnd
While 1
$line = StderrRead($foo)
If #error Then ExitLoop
MsgBox(0, "STDERR read:", $line)
WEnd
MsgBox(0, "Debug", "Exiting...")
Considering all above, StdoutRead should be used like this:
Local $sOutput
While 1
$line = StdoutRead($foo, True)
If #error Then ExitLoop
$sOutput = $line
WEnd
OR
Local $sOutput
While 1
$line = StdoutRead($foo)
If #error Then ExitLoop
$sOutput &= $line & #LF
WEnd

My script is returning 0 even though the CSV source is not empty

I'm in the process of making a small program to tell me on the fly how many employees are currently punched in via AvayaCMS Reporting.
Right now, the way that it is setup is that I have an Avaya script to take a quick report and export it in a CSV that is used by my Autoit script.
In terms of debugging it, I feel like I missing something and need another set of eyes.
launching the Staffing.au3 triggers the CSV script I'm using against the report. Even having the exact same data my message box still reports "0"
#include <Array.au3>
#include <CSV.au3>
$i_AgentCount = AvayaData(#ScriptDir & '\Report.csv', #ScriptDir & '\Name List.csv')
MsgBox(0x40, "", $i_AgentCount)
Func AvayaData($s_ReportSource, $s_NameList)
$av_LiveData = _ParseCSV($s_ReportSource)
If #error Then Return -1
$av_NameList = _ParseCSV($s_NameList)
If #error Then Return -1
Local $i_AgentCount = 0
For $i_RowCnt = 1 To (UBound($av_LiveData, 1) - 1) Step +1
For $i_AgtLst = 1 To (UBound($av_NameList) - 1) Step +1
If StringStripWS($av_LiveData[$i_RowCnt][1], 3) = StringStripWS($av_NameList[$i_AgtLst][0], 3) Then
$i_AgentCount += 1
EndIf
Next
Next
;Return the Agent Count
Return $i_AgentCount
EndFunc
Name List.csv
Agent Name
"Doe, Jane"
"Doe, John"
Report.csv
,Agent Name,Login ID,Extn,AUX Reason,State,Split/Skill,Time,VDN Name
5,"Doe, John",5930001,1000001,7,AUXOUT,999,51:32:00,
2,"Doe, Jane",5930002,1000002,7,AUXOUT,999,52:32:00,
Tested! It works well with the files supplied (copied them and names them as in your script)
please note the followings
_ParseCsv() has been rewritten add parameters like $Dchar as delimeter etc. (see script)
Note how I locate the names in the file (you can easily add feature to extend search but it won't be becessary)
msgboxes are for explanatory purposes; comment them out if not need them anymore
array.au3 is not needed
and the code:
$i_AgentCount = AvayaData(#ScriptDir & '\Report.csv', #ScriptDir & '\Name List.csv')
MsgBox(0x40, "", $i_AgentCount)
Func AvayaData($s_ReportSource, $s_NameList)
$av_LiveData = _ParseCSV($s_ReportSource,",","oops...",True)
If #error Then Return -1
$av_NameList = _ParseCSV($s_NameList,",","oops: 2",True)
If #error Then Return -1
MsgBox(0,default,"$av_NameList (Number of lines) -> " & $av_NameList[0][0])
MsgBox(0,default,"$av_NameList (Number of data in each line) -> " & $av_NameList[0][1])
MsgBox(0,default,"$av_LiveData (Number of lines) -> " & $av_LiveData[0][0])
MsgBox(0,default,"$av_LiveData (Number of data in each line) -> " & $av_LiveData[0][1])
Local $i_AgentCount = 0
for $i = 1 to $av_NameList[0][0] Step 1
For $j= 1 To $av_LiveData[0][0] Step 1
;we can have names from $av_NameList as well from $av_LiveData but in Live Data 2nd abd 3rd values are the names
MsgBox(0,default,$av_NameList[$i][1] & $av_NameList[$i][2] & " <------------> " & $av_LiveData[$j][2] & $av_LiveData[$j][3])
;~ let's compare them if matched with any of the liveData lines
If StringCompare($av_NameList[$i][1] & $av_NameList[$i][2],$av_LiveData[$j][2] & $av_LiveData[$j][3]) == 0 Then
$i_AgentCount += 1
MsgBox(0,default,"Match found: counter: " & $i_AgentCount)
EndIf
Next
Next
;Return the Agent Count
Return $i_AgentCount
EndFunc
Func _ParseCSV($f,$Dchar,$error,$skip)
Local $array[500][500]
Local $line = ""
$i = 0
$file = FileOpen($f,0)
If $file = -1 Then
MsgBox(0, "Error", $error)
Return False
EndIf
;skip 1st line (since it is the header)
If $skip Then $line = FileReadLine($file)
While 1
$i = $i + 1
Local $line = FileReadLine($file)
If #error = -1 Then ExitLoop
;skip 1st line
$row_array = StringSplit($line,$Dchar)
If $i == 1 Then $row_size = UBound($row_array)
If $row_size <> UBound($row_array) Then MsgBox(0, "Error", "Row: " & $i & " has different size ")
$row_size = UBound($row_array)
$array = _arrayAdd_2d($array,$i,$row_array,$row_size)
WEnd
FileClose($file)
$array[0][0] = $i-1 ;stores number of lines
$array[0][1] = $row_size -1 ; stores number of data in a row (data corresponding to index 0 is the number of data in a row actually that's way the -1)
Return $array
EndFunc
Func _arrayAdd_2d($array,$inwhich,$row_array,$row_size)
For $i=1 To $row_size -1 Step 1
$array[$inwhich][$i] = $row_array[$i]
Next
Return $array
EndFunc
#region ;************ Includes ************
#include "csv.au3"
#include <Array.au3>
#endregion ;************ Includes ************
$i_AgentCount = AvayaData(#ScriptDir & '\Report.csv', #ScriptDir & '\Name List.csv')
MsgBox(0x40, "", $i_AgentCount)
Func AvayaData($s_ReportSource, $s_NameList)
Local $i_AgentCount = 0
Local $s = FileRead($s_NameList)
Local $loggedInNames = FileRead($s_ReportSource)
$av_NameList = _ParseCSV($s_NameList)
If #error Then Return -1
;~ _ArrayDisplay($av_NameList)
ConsoleWrite($s & #CRLF)
For $i = 1 To UBound($av_NameList) - 1
ConsoleWrite($i & " " & $av_NameList[$i][0] & #CRLF)
If StringInStr($s, $av_NameList[$i][0]) <> 0 Then
$i_AgentCount += 1
EndIf
Next
Return $i_AgentCount
EndFunc ;==>AvayaData

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:
Print "0 <first Product Code>"
Print "0 <second Product Code>"
Print "259" (ERROR_NO_MORE_ITEMS)
What it currently does:
Print "0 <first Product Code>"
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 )
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 = "")
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

Resources