Building an MSI File: Visual Studio and Orca - Into the Package with Programming
(Page 6 of 6 )
Windows has a collection of Win32 APIs, all starting with Msi, that you can use on the contents of installation packages. Other APIs interface with Windows to interact with the products installed on the system. There’s also a COM interface that is exposed mainly for administrators. I say this because the COM interface is scriptable, and VBScript with Windows Script Host (WSH) can take full advantage of it. You’ll create two VBScripts here: The first does an inventory of the package, and the second updates the package and modifies it. The complete scripts are in the sample code associated with this chapter, and I’ll go through the functionality a snippet at a time. In the interests of readability, not everything is declared explicitly.
The scripting interface into Windows Installer uses a ProgID of “WindowsInstaller.Installer”, so first a VBScript creates the interface with code such as this:
Dim installer
Set installer = CreateObject("WindowsInstaller.Installer")
Note that I’m skipping the error-checking code in these code fragments.
With an Installer object, you can now open a database package:
Dim database
Const msiOpenDatabaseModeReadOnly = 0
Set database = installer.OpenDatabase
("trynotepad.msi", msiOpenDatabaseModeReadOnly )
The first parameter to OpenDatabase is simply the path to the package: the actual MSI file. The second parameter says how you plan to use it. In this case, the value zero means that you aren’t planning to update the package.
You’re going to do an inventory of the files in the package, so you’ll be setting up a SQL-like query into the File table that you previously looked at with Orca. This works using the concept of a “view” based on a query. A View object is returned by calling the OpenView method of the Database object, passing the query that returns the data. The returned View object is a collection of records— a dataset if you like.
Dim view
Set view = database.OpenView
("SELECT `FileName`,`Version`, `FileSize` FROM `File`")
This query should look familiar if you’ve worked with databases. You’re getting items from the File table here; those items correspond to the column names in the Orca view of the File table. Incidentally, look carefully at what the column names are surrounded by. Those are grave characters, sometimes known as back quotes or peck marks: `. They are not single quotation marks. It is usually safer to quote the database content items with grave characters to avoid conflicts with reserved words.
The way you iterate through each of the records is to use the Fetch method of the View object:
Dim record
Set record = view.Fetch
At this point you have a record: a row from the File table. You can retrieve each of the columns with the StringData property of the Record object. You retrieve a single item by indexing StringData with an index that corresponds to its order in the original query, the SELECT statement. The query order was FileName, Version, FileSize, so StringData(1) returns FileName, StringData(2) returns Version, and StringData(3) the FileSize:
Dim afile
afile = record.StringData(1)
This gives you a string containing the FileName value of the particular row. When all the Record objects in the View have been returned with View.Fetch, the final returned Record object is empty, so the script can check to see if the Record object has the value Nothing to find out whether all the records have been returned. Putting this all together into a code fragment that loops through the File table, you have this code:
Set view = database.OpenView
("SELECT `FileName`,`Version`, `FileSize` FROM `File`")
view.Execute
Do
Set record = view.Fetch
If record Is Nothing Then Exit Do
afile = record.StringData(1)
aversion = record.stringdata (2)
fsize = record.Stringdata (3)
fullmsg = fullmsg & afile & " " & aversion & " " & fsize & vbcrlf
Loop
msgbox fullmsg
The principles of updating the package are similar. To make this updating script a bit more interesting, it reads a text file consisting of a series of commands that modify the database. You use FileSystemObject to read the modify.txt file containing the commands:
Set fso = CreateObject("Scripting.FileSystemObject")
Set afile = fso.GetFile("modify.txt")
set ts = afile.OpenAsTextStream (1, 0)
You create an Installer object and open the database package. However, this time you’re updating the package, so you open in transacted mode. This is important when you close the database after your changes:
Const msiOpenDatabaseModeTransact = 1
Set database = installer.OpenDatabase
("trynotepad.msi", msiOpenDatabaseModeTransact)
Given a command that updates the database package, as in the previous script you use the OpenView method of the Database object, passing the command you expect to run. With this command in a string called “thecommand” that you get by reading the text file, you do the OpenView:
Set view = database.OpenView(thecommand)
Because you’re updating, these commands use verbs such as Update, Insert, and Delete, so you use the Execute method of the View object to cause the change to occur. Put this loop together to read the text file and execute the SQL updates:
Dim thecommand, view
Do
thecommand = ts.readline
Set view = database.OpenView(thecommand) : CheckError
view.Execute : CheckError
if ts.AtEndofStream then exit do
Loop
The modify.txt file supplied with the book samples has two updates. The first inserts a new Property into the Notepad installation that you built. The text of this command follows:
INSERT INTO Property (Property.Property, Property.Value) VALUES ('ARPHELPLINK', 'http://www.microsoft.com')
Note that this is in two general pieces. The first names the table and the columns into which data is being inserted in the form <TABLE>. <COLUMN>. The second part lists the values corresponding to those columns. In this example, ARPHELPLINK is a standard Windows Installer property that is displayed in the Add/Remove Programs applet as a “Click here for support information” link. VS’s installer doesn’t let you specify one, so this example shows the way you add one to point to Microsoft’s Web site.
If you refer to Figure 2-9, you see an existing value for the property FolderForm_AllUsers with a value of ME. Your second update changes the value of that existing property in the property table from its previous ME to now say ALL.
UPDATE Property SET Property.Value = 'ALL' WHERE Property.Property = 'FolderForm_AllUsers'
When you opened the database, you opened it in transaction mode. This means that none of the changes you’ve made are in the database package yet. To save the updates, you must commit the changes with the Commit method of the Database object:
database.Commit
Now that you’ve been introduced to an example of how to alter the content of an installation package outside the VS environment, you may wonder about the effect on the installation of changing this particular FolderForm_AllUsers property. VS.NET generates this property—it’s not a standard Windows Installer one. What happens here is that the value of the FolderForm_AllUsers property drives the state of the Everyone or Just me choice in the installation dialog shown in Figure 2-3. After you change the property value from ME to ALL with that Update statement, when you install the new package you’ll note that the radio button now defaults to Everyone. How did I know that ALL is a legal value for the property value? You might have noticed that Orca has a Find choice under the Edit menu. If you do this Find and put FolderForm_AllUsers in the “Find what” text box, the search eventually shows you a ControlEvent table that has an entry with a condition FolderForm_AllUsers="ALL" (see Figure 2-11). I won’t go into the deep details of dialog behavior here, but the FolderForm user dialog (the one with those Everyone/Just me radio buttons) has a ControlEvent that is triggered off the Next button of the dialog. This event occurs when the user chooses the Next button, and the event sets the value of the ALLUSERS property to 2 if FolderForm_AllUsers="ALL". It’s this value of the ALLUSERS property that determines whether the installation is for the current user or for the system as a whole (meaning that the product is installed for all users of the system).

Figure 2-11. ALLUSERS in the ControlEvent table
The ALLUSERS property is a Windows Installer property that you can set to three states:
- Unspecified (NULL is how the SDK documentation describes this). The installation defaults to a per-user installation.
- A value of 1. This means a per-system installation, with the caveat that the installation will fail if the user does not have administrative privileges.
- A value of 2. This causes a per-system installation if the user has administrative privileges, and a per-user installation if the user does not have administrative privileges.
For now, I’ll point out that the nondeterministic value of 2 is likely to be unwelcome in some environments. In particular, it’s unusual for applications to be installed on servers for the installing user—it’s much more likely that the product is being installed for all users of the server. Having it accidentally installed for the current user could be a disaster, so in the cases where you know how the product is intended to be installed and used, force the issue by setting the ALLUSERS property accordingly. You can do that in the ControlEvent table in Figure 2-11 by changing the Argument value to 1 using Orca.
Summary You’ve looked at building a basic installation package and seen some of the contents of the underlying database tables that are used during the installation. You’ve had a brief look at Windows Installer properties and the APIs that you can use to access database packages. One of the key points to take away from this chapter is that a Windows Installer Component is the basic unit of an installation, and you’ll come back to this idea many more times.
This is from The Definitive Guide to Windows Installer, by Phil Wilson, (Apress, ISBN: 1590592972). Check it out at your favorite bookstore today. Buy this book now. |
| DISCLAIMER: The content provided in this article is not warranted or guaranteed by Developer Shed, Inc. The content provided is intended for entertainment and/or educational purposes in order to introduce to the reader key ideas, concepts, and/or product reviews. As such it is incumbent upon the reader to employ real-world tactics for security and implementation of best practices. We are not liable for any negative consequences that may result from implementing any information covered in our articles or tutorials. If this is a hardware review, it is not recommended to open and/or modify your hardware. |