Ntp packet decode - autoit

At the moment I'm trying to decode a packet from a ntp server. I'm trying it in AutoIT, that's a simple script language.
I found some code but the problem is, I need the milliseconds too. The code only provides seconds.
Thats the code I found:
Local $ntpServer = 'pool.ntp.org'
UDPStartup()
Dim $socket = UDPOpen(TCPNameToIP($ntpServer), 123)
If #error <> 0 Then Return SetError(1)
; $status = UDPSend($socket, MakePacket('1b0e010000000000000000004c4f434ccb1eea7b866665cb00000000000000000000000000000000cb1eea7b866665cb'))
Local $status = UDPSend($socket, MakePacket('1b0e01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'))
If $status = 0 Then Return SetError(1)
Local $data = '', $a = TimerInit() ; Timer setzen, damit im Fehlerfall die Schleife abgebrochen wird
While $data = ''
$data = UDPRecv($socket, 100)
Sleep(100)
If TimerDiff($a) > 1000 Then ExitLoop ; Wenn Timer > 1sek. (Fehler), dann Schleife verlassen
WEnd
If $data <> '' Then
UDPShutdown()
Local $unsignedHexValue = StringMid($data, 83, 8) ; Extract time from packet. Disregards the fractional second.
Local $value = UnsignedHexToDec($unsignedHexValue)
Local $TZinfo = _Date_Time_GetTimeZoneInformation()
Local $TZoffset = -(UnsignedToLong($TZinfo[1]) + (UnsignedToLong($TZinfo[4])*($TZinfo[0]<2)) + (UnsignedToLong($TZinfo[7])*($TZinfo[0]=2)))
Local $UTC = _DateAdd('s', $value, '1900/01/01 00:00:00')
Return _DateAdd('n', $TZoffset, $UTC)
EndIf
SetError(1)
It works fine for seconds. As you see, there is a comment "Disregards the fractional second". So, I checked how a ntp packet works and it shows that you have 32bit for seconds, 32 bits for fractional seconds and then another 64 bits with secs and fractions sine 1.1.1900.
In the code he uses 8 chars from the packet (I have no clue why he starts at 83), convert that from hex to dec and then you have the time. So, my first thought to get the milliseconds was, get the next 8 chars, convert that and I have the milli secs, but it's not.
If I get the time four times and let the programm sleep between that, then print out the time with milliseconds I get stuff like that:
2019/02/14 14:29:56.987628606
2019/02/14 14:29:56.243...
2019/02/14 14:29:56.388...
2019/02/14 14:29:57.1107...
That can't be correct. So there is some mistake in decoding the time packet. Anyone know how? I don't need the time extrem exactly like on the millisecond, its enough in a range of 200 - 300 ms.
Edit:
Okay, as far as I tested out, the problem is at the converting of the fraction to millisecs.
For all the infos wanted in the comments, thats the complete code:
Func _TimeSync()
Local $ntpServer = 'pool.ntp.org'
UDPStartup()
Dim $socket = UDPOpen(TCPNameToIP($ntpServer), 123)
If #error <> 0 Then Return SetError(1)
;$status = UDPSend($socket, MakePacket('1b0e010000000000000000004c4f434ccb1eea7b866665cb00000000000000000000000000000000cb1eea7b866665cb'))
Local $status = UDPSend($socket, MakePacket('1b0e01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'))
If $status = 0 Then Return SetError(1)
Local $data = '', $a = TimerInit() ; Timer setzen, damit im Fehlerfall die Schleife abgebrochen wird
While $data = ''
$data = UDPRecv($socket, 100)
sleep(100)
If TimerDiff($a) > 1000 Then ExitLoop ; Wenn Timer > 1sek. (Fehler), dann Schleife verlassen
WEnd
If $data <> '' Then
UDPShutdown()
Local $unsignedHexValue = StringMid($data,83,8); Extract time from packet. Disregards the fractional second.
Local $unsignedHexValue2 = StringMid($data,91,8); Extract time from packet. Disregards the fractional second.
Local $value = UnsignedHexToDec($unsignedHexValue)
Local $value2 = UnsignedHexToDec($unsignedHexValue2)
Local $TZinfo = _Date_Time_GetTimeZoneInformation()
Local $TZoffset = -(UnsignedToLong($TZinfo[1]) + (UnsignedToLong($TZinfo[4])*($TZinfo[0]<2)) + (UnsignedToLong($TZinfo[7])*($TZinfo[0]=2)))
Local $UTC = _DateAdd('s',$value,'1900/01/01 00:00:00')
Return _DateAdd('n',$TZoffset,$UTC)&"."&($value2)
EndIf
SetError(1)
EndFunc
Func MakePacket($d)
Local $p = ''
While $d
$p &= Chr(Dec(StringLeft($d,2)))
$d = StringTrimLeft($d,2)
WEnd
Return $p
EndFunc
Func UnsignedHexToDec($n)
Local $ones = StringRight($n,1)
$n = StringTrimRight($n,1)
Return dec($n)*16+dec($ones)
EndFunc
Func UnsignedToLong($Value)
Local Const $OFFSET_4 = 4294967296
Local Const $MAXINT_4 = 2147483647
If $Value < 0 Or $Value >= $OFFSET_4 Then Return SetError(1,0,$Value) ;' Overflow
If $Value <= $MAXINT_4 Then
Return $Value
Else
Return $Value - $OFFSET_4
EndIf
EndFunc
I couldnt figured out how to convert the fractions to a normal int or float

The fractional seconds are measured in units of 1/(2**numberOfBits) seconds = 0.000000000232831, where the numberOfBits is 4 Bytes * 8 bits/Byte = 32 bits. So the number of microseconds (6 decimal digits), for example, is: milliSecs = $value2 * 0.000232830643654. But displaying more than about 3 digits probably implies more accuracy than is true.

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

Autoit - how to find dupes in two strings

anyone help please
I have a function that outputs a list of names to an array. I've parsed the list into a string that shows exactly what I want.
I have a second list of names that I want to compare to the first list and print out the details if they are different.
Example: If string = CCleaner 5.5 then prog1 is out of date. I want it to show this
#Include <Date.au3>
#Include <MsgBoxConstants.au3>
Local $prog1 = "CCleaner 5.29"
Local $prog2 = "Defraggler 2.21"
Local $string
Local $aList = _UninstallList("", "","Publisher|InstallLocation|DisplayVersion")
for $1 = 0 to ubound($aList) - 1
$string &= $aList[$1][2] & " " & $aList[$1][6] & #CRLF
Next
msgbox(0, "", $string)
Several years ago, I wrote a function to compare lists or arrays. I think this can help you.
;==================================================================================================
; Function Name: _GetIntersection($Set1, $Set2 [, $GetAll=0 [, $Delim=Default]])
; Description:: Detect from 2 sets
; - Intersection (elements are contains in both sets)
; - Difference 1 (elements are contains only in $Set1)
; - Difference 2 (elements are contains only in $Set2)
; Parameter(s): $Set1 set 1 (1D-array or delimited string)
; $Set2 set 2 (1D-array or delimited string)
; optional: $GetAll 0 - only one occurence of every different element are shown (Default)
; 1 - all elements of differences are shown
; optional: $Delim Delimiter for strings (Default use the separator character set by Opt("GUIDataSeparatorChar") )
; Return Value(s): Succes 2D-array [i][0]=Intersection
; [i][1]=Difference 1
; [i][2]=Difference 2
; Failure -1 #error set, that was given as array, is'nt 1D-array
; Note: Comparison is case-sensitiv! - i.e. Number 9 is different to string '9'!
; Author(s): BugFix (AutoIt#bug-fix.info)
;==================================================================================================
Func _GetIntersection(ByRef $Set1, ByRef $Set2, $GetAll=0, $Delim=Default)
Local $o1 = ObjCreate("System.Collections.ArrayList")
Local $o2 = ObjCreate("System.Collections.ArrayList")
Local $oUnion = ObjCreate("System.Collections.ArrayList")
Local $oDiff1 = ObjCreate("System.Collections.ArrayList")
Local $oDiff2 = ObjCreate("System.Collections.ArrayList")
Local $tmp, $i
If $GetAll <> 1 Then $GetAll = 0
If $Delim = Default Then $Delim = Opt("GUIDataSeparatorChar")
If Not IsArray($Set1) Then
If Not StringInStr($Set1, $Delim) Then
$o1.Add($Set1)
Else
$tmp = StringSplit($Set1, $Delim)
For $i = 1 To UBound($tmp) -1
$o1.Add($tmp[$i])
Next
EndIf
Else
If UBound($Set1, 0) > 1 Then Return SetError(1,0,-1)
For $i = 0 To UBound($Set1) -1
$o1.Add($Set1[$i])
Next
EndIf
If Not IsArray($Set2) Then
If Not StringInStr($Set2, $Delim) Then
$o2.Add($Set2)
Else
$tmp = StringSplit($Set2, $Delim)
For $i = 1 To UBound($tmp) -1
$o2.Add($tmp[$i])
Next
EndIf
Else
If UBound($Set2, 0) > 1 Then Return SetError(1,0,-1)
For $i = 0 To UBound($Set2) -1
$o2.Add($Set2[$i])
Next
EndIf
For $tmp In $o1
If $o2.Contains($tmp) And Not $oUnion.Contains($tmp) Then $oUnion.Add($tmp)
Next
For $tmp In $o2
If $o1.Contains($tmp) And Not $oUnion.Contains($tmp) Then $oUnion.Add($tmp)
Next
For $tmp In $o1
If $GetAll Then
If Not $oUnion.Contains($tmp) Then $oDiff1.Add($tmp)
Else
If Not $oUnion.Contains($tmp) And Not $oDiff1.Contains($tmp) Then $oDiff1.Add($tmp)
EndIf
Next
For $tmp In $o2
If $GetAll Then
If Not $oUnion.Contains($tmp) Then $oDiff2.Add($tmp)
Else
If Not $oUnion.Contains($tmp) And Not $oDiff2.Contains($tmp) Then $oDiff2.Add($tmp)
EndIf
Next
Local $UBound[3] = [$oDiff1.Count,$oDiff2.Count,$oUnion.Count], $max = 1
For $i = 0 To UBound($UBound) -1
If $UBound[$i] > $max Then $max = $UBound[$i]
Next
Local $aOut[$max][3]
If $oUnion.Count > 0 Then
$i = 0
For $tmp In $oUnion
$aOut[$i][0] = $tmp
$i += 1
Next
EndIf
If $oDiff1.Count > 0 Then
$i = 0
For $tmp In $oDiff1
$aOut[$i][1] = $tmp
$i += 1
Next
EndIf
If $oDiff2.Count > 0 Then
$i = 0
For $tmp In $oDiff2
$aOut[$i][2] = $tmp
$i += 1
Next
EndIf
Return $aOut
EndFunc ;==>_GetIntersection

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

How to read Test.csv file format in autoit

I have test.csv file need to read all the data using autoit
As TeamKiller said your question is quite vague but here is a sample code that should give you an idea of how to read a CSV file.
#include <GUIConstants.au3>
#include <Array.au3>
#include <File.au3>
#include <String.au3>
Opt("MustDeclareVars", 1)
Global Const $CSVFILE = "C:\Temp\test.csv"
Global Const $DELIM = ";" ;the delimiter in the CSV file
Global $i, $arrContent, $arrLine, $res = 0
$res = _FileReadToArray($CSVFILE, $arrContent)
If $res = 1 Then
For $i = 1 To $arrContent[0]
$arrLine = StringSplit($arrContent[$i], $DELIM)
If IsArray($arrLine) And $arrLine[0]<>0 Then
_ArrayDisplay($arrLine)
; do something with the elements of the line
Else
MsgBox(48, "", "Error splitting line!")
EndIf
Next
Else
MsgBox(48, "", "Error opening file!")
EndIf
return value of _ParseCSV() is 2D array like
$array[1][1] first line first data
$array[1][2] first line second data
$array[3][5] 3rd line 5th data
$array[0][0] gives number of lines
$array[0][1] gives number of data in each line
no need for includes
params:
filename
delimeter
message display if cannot open file
logical true/false to skip the first line of the file
;_ParseCSV("filename",",","message if error happens",true)
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
If $skip Then $line = FileReadLine($file)
While 1
$i = $i + 1
Local $line = FileReadLine($file)
If #error = -1 Then ExitLoop
$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
As for parsing a CSV file, you are likely better off using a library (called user-defined functions in AutoIt), especially if you e.g. have complex CSVs with quoted strings (commas inside of the "cell"/string) or line breaks, which are hard to handle.
The best I can recommend is CSVSplit.
Basically you have a function _CSVSplit that takes a whole CSV file (content, i.e. string!) and returns you a two-dimensional array:
Local $sCSV = FileRead($sFilePath)
If #error Then ; ....
$aSplitArray = _CSVSplit($sCSV, ",")
You can then do everything you want with this array. Obviously, CSVSplit also provides the "reverse" function for turning an array into a CSV string again, _ArrayToCSV.
Originally posted as an answer here, which I consider a duplicate of this question.

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