Tutorial 18: Getting component attributes

Ahhhh. A lovely topic.

A couple of weeks ago, I needed to scan an MSI for 64-bit components. I knew each component had an ‘attribute’ value (which is actually a bit flag), but I needed to figure out a way to extract each bit flag so that I could find out how the component was configured and which attributes were set.

Here’s how I did it….

Firstly, let’s list down the component attributes and their corresponding bit flags:

Bit Flag Description Decimal Value
Component cannot be run from source 0
Component can only be run from source 1
Component can run locally or from source 2
Has a Registry Keypath 4
Increments Shared DLL Registry Count 8
Permanent Component (doesn’t remove on uninstall) 16
Has an ODBC Data Source Keypath 32
Re-evaluates Condition on Reinstall 64
Do Not Overwrite if Component Exists 128
64-Bit Component 256
Disable Registry Reflection 512
Prevent Leaving Orphan Components 1024
Share Component 2048

When we want to set multiple bit flags on a component, we add the decimal values together. For example, let’s say we have a component which contains a 64-bit DLL as the keypath, and we want to incrememnt the shared DLL reference count. The attribute of our component would be:

(Increments Shared DLL Registry Count + 64-Bit Component)
= 8 + 256
= 264

Now let’s reverse things. If we have a value of 264 in our attribute column, how do we work out which decimal values were added together to make this up?
The answer is that we need to convert the decimal value to a binary value. Here goes a quick binary lesson:

A 16-bit binary value would consist of 16 1’s and 0’s. For example, 0010101000101101. Each ‘bit’ flag has a corresponding decimal value. In the table below, we’ll use our 16-bit shared DLL component as an illustrated example:

component_attributes

The MSI component attribute only uses the first 13 bits. The last 3 bits will always be zero. By converting the decimal value of 264 in to binary, we can see which bit flags are set and therefore decipher the attribute which has been applied to the component.

Now let’s see it in code….

'Function to convert decimal to 16-bit binary
Function DecToBin(intDec)
  dim strResult
  dim intValue
  dim intExp
  strResult = ""

  intValue = intDEC
  intExp = 32768
  while intExp >= 1
    if intValue >= intExp then
      intValue = intValue - intExp
      strResult = strResult & "1"
    Else
      strResult = strResult & "0"
    end if
    intExp = intExp / 2
  Wend

  DecToBin = strResult
End Function

Dim componentBinary : componentBinary = 0
Dim componentName : componentName = ""

Dim isComponent64Bit : isComponent64Bit = 0
Dim isComponentSharedDLL : isComponentSharedDLL = 0

'Select the name and attribute for each component 
Set View = Database.OpenView("SELECT Component, Attributes FROM `Component`")
View.Execute
Set rec = View.Fetch

Do While Not rec Is Nothing	

	'get component name
	componentName = rec.StringData(1)

	'get the attribute (decimal value) and convert it to 16-bit binary
	componentBinary = DecToBin(Cint(rec.StringData(2)))

	'get bit flag for shared dll (4th bit) - remember vbscript arrays start at 0!
	isComponentSharedDLL = Mid(componentBinary,3,1)

	'get bit flag for 64-bit (9th bit) - remember vbscript arrays start at 0!
	isComponent64Bit = Mid(componentBinary,8,1)

	If isComponentSharedDLL = "1" Then
		MsgBox "Component '" & componentName & "' is a shared DLL resource!"
	End If

	If isComponent64Bit = "1" Then
		MsgBox "Component '" & componentName & "' is a 64-bit component!"
	End If

  	Set rec = View.Fetch
Loop
View.Close
Set View = Nothing
Set rec = Nothing

Tutorial 17: Editing MSIs in the Windows Installer cache

In this example, let’s say a user has an MSI installed and they want to remove it. Every time they try to remove it, a Custom Action called ‘AlkaneCustomAction’ keeps throwing an error, and as a result the uninstall fails.

This piece of code searches all products installed or advertised for the current user and machine. When we find the product code, we open the database in trasact mode, set the condition of the Custom Action to ‘1=0’ (I.e, NEVER run it) and commit the change. The MSI will now remove without error.

Obviously this is just an example. Simply not running the Custom Action may not be the optimal solution!

Const msiOpenDatabaseModeTransact = 1  

'Enter ProductCode of the MSI we want to amend
Const ProdCodeToFind = "{00000000-0000-0000-0000-000000000000}"

Dim productCodeFound : productCodeFound = False
Dim currentProductCode, oTempDatabase

'Loop through every product, and see if the product code matches the one we're looking for
Dim oInstaller : Set oInstaller = CreateObject("WindowsInstaller.Installer")  
For Each currentProductCode In oInstaller.Products  
 	If currentProductCode = ProdCodeToFind Then  
    		oTempDatabase = oInstaller.productInfo(currentProductCode, "LocalPackage")  
		productCodeFound = True
 	End If  
Next  

If productCodeFound Then  
	Set database = oInstaller.OpenDatabase(oTempDatabase, msiOpenDatabaseModeTransact)  
	Set view = database.OpenView("UPDATE `InstallExecuteSequence` SET `Condition`='1=0' WHERE `Action`='AlkaneCustomAction'")  
	view.Execute  
	database.Commit
	view.Close
        set view = Nothing
	set database = Nothing
End If

Set oTempDatabase = Nothing
Set oInstaller = Nothing

Tutorial 16: Writing to the Windows Installer log file from a Custom Action

Logging is extremely useful when debugging MSIs. Most people that write Custom Actions don’t write logs, but if you’re writing quite extensive logic you may wish to consider it. Simply paste the following logic into your CA:

'Subroutine to write to log file 
Const msiMessageTypeInfo = &H04000000
Sub writeToLog(ByVal msg)
	Set record = Installer.CreateRecord(1)
	record.stringdata(0) = "AlkaneLog: [1]"
	'This value gets subbed in to the [1] placeholder above
	record.stringdata(1) = msg
	record.formattext
	message msiMessageTypeInfo, record
	Set record = Nothing
End Sub

and write to the log like this:

writeToLog("This is a log message")

Tutorial 15: Using SQL inside a running Windows Installer Session

As well as creating administrative scripts like the previous examples, we can also use SQL queries during the installation of an MSI. When an MSI is installing, we
can refer to this as a ‘session’. Take the following excerpt – this is from a tutorial I wrote regarding using VBScript Custom Actions to automate the installation and removal of Excel add-ins:

...
...
Dim sql : sql = "SELECT File.FileName,File.Component_,Component.Directory_ FROM File, Component WHERE File.Component_ = Component.Component"

'start searching through file table for add-ins (.XLA or .XLL files)
Set fileView= Session.Database.OpenView(sql)
fileView.Execute
Set fileRecord = fileView.Fetch
While Not fileRecord Is Nothing

    componentState = Session.ComponentRequestState(fileRecord.StringData(2))

    'if component is being installed locally
    If componentState = 3 Then

	'get filename
        tempFileName = LCase(fileRecord.StringData(1))

        If InStr(tempFileName,"|") Then
            'if filename is currently in sfn form, try and retrieve the full file name
            tempFileName = Split(tempFileName,"|")(1)
        End If 

        'retrieve 3-length extension (that's all
        tempExtension = Right(tempFileName,Len(tempFileName)-InStrRev(tempFileName ,"."))

        If (tempExtension = "xla" OR tempExtension = "xll") Then       
                'return full path to Excel add-in file.
                filePath = Session.TargetPath(fileRecord.StringData(3)) & tempFileName          
                'install/add add-in using automation....(logic not included in this example)
        End If 

    End If 
    Set fileRecord = fileView.Fetch
Wend

fileView.Close
Set fileView = Nothing
Set fileRecord = Nothing
...
...

You can see that we don’t explicitly create an Installer object now, because we’re already in the session of one! Instead, we use this syntax:

Session.Database.OpenView(sql)

another handy tip in here is the use of:

Session.ComponentRequestState(fileRecord.StringData(2))

to detect if our session is installing/uninstalling or repairing. In this example, I’m detecting if we’re installing and if so I search for all Excel add-ins, finding their install locations by using:

Session.TargetPath

Tutorial 14: Generating Transforms

Now we’re going to attempt to generate a transform from changes which we make to an MSI. An example of this could be adding certain properties for a customer, or possible an audit key in the registry table.

When we’re generating an MST from a script, here’s the idea:

  • We take a copy of the original base MSI, and create a new temporary MSI
  • We apply any changes we make to this temporary MSI
  • We find the difference between the temporary MSI and the base (original) MSI
  • We generates a transform from the differences between the base MSI and the temporary MSI.

So, here goes. We’re going to insert the following entry into the Registry table:

generate_xfm

'create 2 constants - one for when we want to just query the MSI (read) and one for when we want to make changes (write)

Const msiOpenDatabaseModeReadOnly = 0
Const msiOpenDatabaseModeTransact = 1

'create a file system object, so that we can copy the original MSI
Dim fso : Set fso = CreateObject("Scripting.FileSystemObject")

'get a reference to the original msi
Dim originalMSI : Set originalMSI = fso.GetFile(WScript.arguments(0))

'create a full path to our 'proposed' generated transform
Dim transformWithChanges: transformWithChanges = Left(originalMSI.Path, InStrRev(originalMSI.Path, ".") - 1) & "_Changes.MST"

'get random filename or a temporary MSI
temporaryMSI = originalMSI.ParentFolder & "\" & fso.GetTempName

'make a copy of the original MSI, and name it with our temporary name above
originalMSI.Copy (temporaryMSI)   'Make a backup of the MSI to work on

'set the attribute of the temp file - default to 'Normal'
fso.GetFile(temporaryMSI).Attributes = 0

'we can now use our temp file above as a base comparison

Set oInstaller = CreateObject("WindowsInstaller.Installer")

'open the temporary MSI in transact mode
Set oTempDatabase = oInstaller.OpenDatabase(temporaryMSI, msiOpenDatabaseModeTransact)

'insert our new value
Dim sql : sql = "INSERT INTO `Registry` (`Registry`,`Root`,`Key`,`Name`,`Value`,`Component_`) VALUES ('SampleTransformReg',2,'Software\AlkaneTest','testTransformName','testTransformValue','alkaneComponent')"

Dim regView : Set regView = oTempDatabase.OpenView(sql)

'execute the query
regView.Execute

'Now get a reference to the original, un-changed MSI.  Open it in Read-only mode
Set oOriginalDatabase = oInstaller.OpenDatabase(originalMSI.Path, msiOpenDatabaseModeReadOnly)

'get the differences between our original database (oOriginalDatabase) and our temp database (oTempDatabase), and generate transform to our new transform filename (transformWithChanges)
oTempDatabase.GenerateTransform oOriginalDatabase, transformWithChanges

'create transform summary information stream.  This was a bug which I found was missing from the SDK scripts
oTempDatabase.CreateTransformSummaryInfo oOriginalDatabase, transformWithChanges, 0, 0

regView.Close
Set regView = Nothing
Set oTempDatabase = Nothing
Set oOriginalDatabase = Nothing
Set oInstaller = Nothing

'delete our temporary database
Set File = fso.GetFile(temporaryMSI)
File.Delete

Wscript.Echo transformWithChanges & " Created!"

The comments throughout the example above explain what each section does. What I would say, though, is:

  • Always remember to call the CreateTransformSummaryInfo method! The Windows Installer SDK examples don’t include this. Omitting this may mean your transform wont generate/apply without error.
  • Ensure you close every view object, otherwise the temporary file will not delete at the end.
  • If no changes are made to the base MSI and you attempt to generate a transform, there will be an error because the difference between the base MSI and the new one will be nothing.

Tutorial 13: Applying Transforms

For this example I’ve created a sample MST file. Our test MSI has an entry in the Registry table with a primary key of ‘SampleReg’ and a Value of ‘This is an MSI’. To create the transform I used Orca. I opened our test MSI, selected the ‘Transform’ menu option, and scrolled down and clicked ‘New Transform’. I then changed the Value of our ‘SampleReg’ row to be ‘This is an MST’. To create the new transform file, I then selected the ‘Transform’ menu option, and scrolled down and clicked ‘Generate Transform’. By changing this value via a transform, it should enable us to see if the transform has applied.

To start with, copy and paste the following code into your script file:

'create 2 constants - one for when we want to just query the MSI (read) and one for when we want to make changes (write)

Const msiOpenDatabaseModeReadOnly = 0
Const msiOpenDatabaseModeTransact = 1

' Adds a row that already exists.
Const msiTransformErrorAddExistingRow = 1 
' Deletes a row that does not exist.
Const msiTransformErrorDeleteNonExistingRow = 2 
' Adds a table that already exists.
Const msiTransformErrorAddExistingTable = 4 
' Deletes a table that does not exist.
Const msiTransformErrorDeleteNonExistingTable = 8 
' Updates a row that does not exist.
Const msiTransformErrorUpdateNonExistingRow = 16 
' Transform and database code pages do not match and neither has a neutral code page.
Const msiTransformErrorChangeCodePage = 32 
' Creates the temporary _TransformView table.
Const msiTransformErrorViewTransform = 256 

Dim errorCondition : errorCondition = msiTransformErrorChangeCodePage + msiTransformErrorUpdateNonExistingRow + msiTransformErrorDeleteNonExistingTable _
+ msiTransformErrorAddExistingTable + msiTransformErrorDeleteNonExistingRow + msiTransformErrorAddExistingRow

'create WindowsInstaller.Installer object
Dim oInstaller : Set oInstaller = CreateObject("WindowsInstaller.Installer")

'open the MSI (the first argument supplied to the vbscript)
Dim oDatabase : Set oDatabase = oInstaller.OpenDatabase(WScript.Arguments(0),msiOpenDatabaseModeReadOnly) 

Dim sql : sql = "SELECT `Value` FROM `Registry` WHERE `Registry` = 'SampleReg'"

'create a view of the registry we want to see
Dim regView : Set regView = oDatabase.OpenView(sql)

'execute the query
regView.Execute 

'fetch the first row of data (if there is one!)
Dim regRecord : Set regRecord = regView.Fetch

'whilst we've returned a row and therefore regRecord is not Nothing
While Not regRecord Is Nothing

	'print out the registry key
	wscript.echo "Value from MSI is: " & regRecord.StringData(1)

	'go and fetch the next row of data	
	Set regRecord = regView.Fetch
Wend
Set regRecord = Nothing

wscript.echo "(...applying MST)"

'apply every transform in the argument list
For i = 1 To Wscript.Arguments.Count - 1
	oDatabase.ApplyTransform Wscript.Arguments(i), errorCondition
	oDatabase.Commit
Next

sql = "SELECT `Value` FROM `Registry` WHERE `Registry` = 'SampleReg'"

'create a view of the registry we want to see
Set regView = oDatabase.OpenView(sql)

'execute the query
regView.Execute 

'fetch the first row of data (if there is one!)
Set regRecord = regView.Fetch

'whilst we've returned a row and therefore regRecord is not Nothing
While Not regRecord Is Nothing

	'print out the registry key
	wscript.echo "Value from MST is: " & regRecord.StringData(1)

	'go and fetch the next row of data	
	Set regRecord = regView.Fetch
Wend

regView.Close
Set regView = Nothing
Set regRecord = Nothing
Set oDatabase = Nothing
Set oInstaller = Nothing

To run it, we need to specify both our MSI file and our MST file:

Open up a Command Prompt
Type: cscript.exe alkaneTestScript.vbs “c:\alkaneMSIs\testMSI.msi” “c:\alkaneMSIs\testMST.mst”

You should see the following result:

The only new part to this script is that we use the ApplyTransform method to apply one (or many!) transforms to our MSI:

'apply every transform in the argument list
For i = 1 To Wscript.Arguments.Count - 1
	oDatabase.ApplyTransform Wscript.Arguments(i), 63
	oDatabase.Commit
Next

In the excerpt of code above, we loop through the arguments we’ve passed in to the VBCsript (exclusing the first argument – the MSI), we apply the transform to the database and we commit the changes.

Tutorial 12: Inner joins

Let’s say we want to know what directory a file gets installed too. In order to do that, we’d need to read the FileName column from the File table, and the Directory_
column from the Component table.

tipTip: All foreign key column names end with an underscore (_).

 

We can see that the File table includes a foreign key into the Component table (Component_). For this reason, we’ll join the two tables where:

File.Component_ equals Component.Component

'create 2 constants - one for when we want to just query the MSI (read) and one for when we want to make changes (write)

Const msiOpenDatabaseModeReadOnly = 0
Const msiOpenDatabaseModeTransact = 1

'create WindowsInstaller.Installer object
Dim oInstaller : Set oInstaller = CreateObject("WindowsInstaller.Installer")

'open the MSI (the first argument supplied to the vbscript)
Dim oDatabase : Set oDatabase = oInstaller.OpenDatabase(WScript.Arguments(0),msiOpenDatabaseModeReadOnly) 

Dim sql : sql = "SELECT `File`.`FileName`, `Component`.`Directory_` FROM `File`, `Component` WHERE `File`.`Component_` = `Component`.`Component`"

'create a view of the registry we want to see
Dim fileView : Set fileView = oDatabase.OpenView(sql)

'execute the query
fileView.Execute 

'fetch the first row of data (if there is one!)
Dim fileRecord : Set fileRecord = fileView.Fetch

'whilst we've returned a row and therefore fileRecord is not Nothing
While Not fileRecord Is Nothing

	'print out the registry key
	wscript.echo "The file '" & fileRecord.StringData(1) & "' gets installed to the directory '" & fileRecord.StringData(2) & "'"  

	'go and fetch the next row of data	
	Set fileRecord = fileView.Fetch
Wend

fileView.Close
Set fileView = Nothing
Set fileRecord = Nothing
Set oDatabase = Nothing
Set oInstaller = Nothing

Tutorial 11: Inserting a row using the Modify method

In this example, we change the SQL statement to include the columns of data which we want to insert. Note that we MUST include all of the non-nullable columns as a minimum. When a column is not nullable, it means we MUST specify a value when we insert the record. Otherwise the insertion will fail. To find the non-nullable columns, consult the Windows Installer help file (MSI.chm) or MSDN. For example:

The four non-nullable columns which we MUST include when inserting into the Registry table are:

Registry
Root
Key
Component_

Dim sql : sql = "SELECT `Registry`,`Root`,`Key`,`Component_` FROM `Registry` WHERE `Registry` = 'MyNewSampleReg'"

If we were lazy, we could just return all rows using the following:

Dim sql : sql = "SELECT * FROM `Registry` WHERE `Registry` = 'MyNewSampleReg'"

This time when we execute the query, we’re not going to loop through any records using the While…Wend loop. All we want to do is see if the record already exists:

If regRecord Is Nothing Then

and if it doesn’t, we’ll start to create a new record and populate the values….

'There are no records returned, so we create a new record
    Set regRecord = oInstaller.CreateRecord(4)

    'in this example, we've created a record ensuring that all the non-nullable field are populated
    regRecord.StringData(1) = "MyNewSampleReg"
    regRecord.IntegerData(2) = 2
    regRecord.StringData(3) = "SOFTWARE\alkaneTest"
    regRecord.StringData(4) = "alkaneComponent"

Note how we’ve used StringData for columns which can accept strings as their data types, and IntegerData for columns which accept numeric values only.
Again, to find columns with an ‘Integer’ data type you should consult the Windows Installer help file (MSI.chm) or MSDN.

Once we’ve created our new record, we insert it:

regView.Modify msiViewModifyInsert,regRecord

So, here it is:

'create 2 constants - one for when we want to just query the MSI (read) and one for when we want to make changes (write)

Const msiOpenDatabaseModeReadOnly = 0
Const msiOpenDatabaseModeTransact = 1

Const msiViewModifyInsert = 1
Const msiViewModifyUpdate = 2
Const msiViewModifyDelete = 6
Const msiViewModifyReplace = 4

'create WindowsInstaller.Installer object
Dim oInstaller : Set oInstaller = CreateObject("WindowsInstaller.Installer")

'open the MSI (the first argument supplied to the vbscript)
Dim oDatabase : Set oDatabase = oInstaller.OpenDatabase(WScript.Arguments(0),msiOpenDatabaseModeTransact) 

Dim sql : sql = "SELECT `Registry`,`Root`,`Key`,`Component_` FROM `Registry` WHERE `Registry` = 'MyNewSampleReg'"
'create a view of the registry we want to see
Dim regView : Set regView = oDatabase.OpenView(sql)

'execute the query
regView.Execute 
Set regRecord = regView.Fetch

If regRecord Is Nothing Then
	'There are no records returned, so we create a new record
	Set regRecord = oInstaller.CreateRecord(4)

	'in this example, we've created a record ensuring that all the non-nullable field are populated
	regRecord.StringData(1) = "MyNewSampleReg"
	regRecord.IntegerData(2) = 2
	regRecord.StringData(3) = "SOFTWARE\alkaneTest"
	regRecord.StringData(4) = "alkaneComponent"

	regView.Modify msiViewModifyInsert,regRecord

End If

oDatabase.Commit

regView.Close
Set regView = Nothing
Set regRecord = Nothing
Set oDatabase = Nothing
Set oInstaller = Nothing

Tutorial 9: Modify a primary key using the Modify method

Here we would change the SQL query to select our primary key value (in other words, we’re selecting the Registry column now instead of the Value column):

Dim regView : Set regView = oDatabase.OpenView("SELECT `Registry` FROM `Registry` WHERE `Registry` = 'SampleReg'")

and we would modify it like this:

regRecord.StringData(1) = "NewSampleReg"
regView.Modify msiViewModifyReplace, regRecord

Note that this essentially won’t update that particular row. Instead it will delete the current row, and insert the new row containing the new primary key value.