Monday, September 06, 2004

MSI - Modifying the Installer Database at Runtime

A recent question posted to the Microsoft.public.platformsdk.msi newsgroup:

I'm trying to prevent some NT services installation with a VBScript custom action (type 38) that removes the corresponding rows from the ServiceInstall table.
The poster shows some example code that does not seem to work. Let's look at some approaches to try to solve this problem.

Solution 1

Here's some theoretical JScript to run in a Custom Action that modifies the installer Database:

oDatabase = Session.Database;
oView = oDatabase.OpenView("select * from ServiceInstall");
//now our view object has records we can "fetch"
oRecord = oView.Fetch(); //get the first record from the query
while (oRecord != null) //make sure we are not at the end.
//TODO: Either handle the logic on what to remove based
// on the oRecord here or in the query. Assumes all
// records matching the query are removed.
oView.Modify(6,oRecord); //6 = msiViewModifyDelete
//get the next record from the query
oRecord = oView.Fetch();
oView.Close(); //close the view
oDatabase.Commit(); //commit our changes

Looks pretty good, right? We are opening the database, fetching records, modifying the records, and committing the changes. Sad to say even though the API logic is correct, this won't solve the problem at hand! Why?
Custom actions can only add, modify, or remove temporary rows, columns, or tables from a database. Custom actions cannot modify persistent data in a database, such as data that is a part of the database stored on disk. (View.Modify documentation excerpt)
As an aside, you could substitute the following 2 lines for the first one and modify the msi (permanently) outside a CA:

oInstaller = new ActiveXObject("WindowsInstaller.Installer");
oDatabase = oInstaller.OpenDatabase("myInstall,msi",2);

Solution 2

So, with this restriction, how do we solve your problem? The answer lies (buried) in the View.Modify documentation. The action type msiViewModifyInsertTemporary is our friend. Instead of solving the problem by deleting the services you don't want to install, we can add the services you DO want to install at runtime! Remember to delete the lines from the ServiceInstall table if you use this method

Lets look at an example of this:

oDatabase = Session.Database;
oView = oDatabase.OpenView("select * from ServiceInstall");
oRecord = Installer.CreateRecord(13);

//TODO: continue for the rest of the columns in this table. See
// the ServiceInstall table docs for more information

//msiViewModifyInsertTemporary the record.

oView.Close(); //NOTE: no need to call commit..

You do need to deal with the uninstall issue using another CA. That is not covered here, but the concept is similar.

As an aside, I use the above technique to adjust the ReserveCost table when I need to create a large database. If the DB exists, I do nothing, otherwise, I add the size of the database to the table for proper costing.

Solution 3

Although Solution 2 will solve this problem, why would you want to go through that complexity when there is a much better (and simpler) method built into the MSI engine? In the ServiceInstall table, there is a Foreign Key into the Component table. If the component is to be installed, so is the service.

Simply create a hidden feature, assign the components necessary for the service to that feature, and conditionally install (or not install) that feature.

No comments: