Microsoft recently introduced a service specifically targeted at the installation process. For many developers, the first exposure to it might have been the Platform SDK if you installed it from the Microsoft website. In this article, were going to examine its impact on the installation process, particularly COM components and Windows NT/2000 services, and look at the Installers capabilities and features as they affect developers. Well focus on the programming hooks into the Installer.
Installing COM and System Services
If youve ever installed COM components, youre familiar with the self-registration code and the DLLRegisterServer() entry point in a COM in-process server. Traditional setup programs copy the DLL to the target system, and then the DLLRegisterServer() entry point is called. When the component is uninstalled, the DLLUnregsterServer() entry point is called. In the Windows Installer world, this installation method is discouraged. Well talk about why later, but for now the question is, how do we get the components properly registered? When you use the Window Installer, the COM registration data is part of the installation database, so installation consists of copying the file to the system and applying the registration data from the installation databases tables. Does this mean we no longer need DLLRegisterServer() and DLLUnregisterServer()? Theoretically, we no longer need them (and Microsoft could possibly integrate Installer registration technology into Visual Studio), but there are good reasons for continuing to use them. For example, tools that build Installer databases need to extract the COM registration from your components. One way to do this is to call your DLLRegisterServer() entry point and spy on the registry changes being made, incorporating them into the install project.
The same registration that applies to in-process servers also applies to out-of-process servers. These typically install with a command-line option such as -Regserver, and once again the COM information is installed from the Installer database onto the client system. The COM registration will not be installed by running it with the command-line option. If youve ever written an NT/Windows 2000 service, you may have noticed the installation code when you run it at install time with a -service command-line option. The Windows Installer has built-in support for installing, starting, and stopping services. If youre installing a service and you need to stop an existing service before yours is installed, theres support for that, too. COM servers will be unregistered by removing the database entries that were originally installed. By the way, if youve ever wondered about installing type libraries, the Windows Installer has built-in support theres no longer any need for those programs weve either written or borrowed.
By now youve probably noticed a pattern: Microsoft doesnt want you to install anything by running code. The reasons for this are to do with features that are available with the Windows Installer, as well as for robustness and reliability for the end-user. For example, if an application was installed by running a command-line program and the program has a dependent DLL that is later removed or corrupted, the program cannot be run with its uninstallation command line to remove the application. In addition, the Windows Installer has a repair mode that examines the installed application, comparing it with a cached copy of the relevant installation information and repairing missing registry entries and files. This repair mode is facilitated by updating from a database of installation information compared to re-running programs to repair themselves when the programs themselves might be missing. Also, the installation process takes place as an audited transfer of files and system information, all of which can be undone to restore the system to its original state. Again, this is more robust when no user programs have to be run.
Whats in an Installer Database
You may not be used to thinking of an installation file as a database, but thats exactly what a Windows Installer file is. There are some 80 tables that define the installation. For example, theres a File table that contains a list of the files in the database. Each row of the File table contains information about the files to be installed, including the version and the attributes (such as Hidden, and Readonly) that should be applied to the file when its installed. There is a Class table that contains the GUIDs, ProgIDs, and other COM information relating to COM components being installed. For developers, the good news is that you can examine the database and use SQL query syntax to return what youre interested in. This interface is also an automation interface, so you can use tools such as VBScript. lstfile.vbs (Listing 1) illustrates this by iterating through the File table and showing the versions of the files in the database. First, it creates an Installer object and then opens an Installer database. In this case, were opening Orca.msi. Then it uses SQL syntax to select the file names and versions of the entries in the databases File table thats what OpenView() does. Once we have the data in a View object, we can iterate through that set of records with the Fetch() method. Fetch() returns each record sequentially, and we access each field of the record with the StringData property. Note that the parameter to StringData is the number denoting the order of the items in our Select query. StringData(1) is the filename because FileName was the first item in Select, and the file version is StringData (2) because Version was the second. The filenames and versions are accumulated into a message for displaying at the end.
How the Installation Process Works
We wont go into too much detail here, but its important to have an idea of whats happening during the installation process and what we can do to customize it. The installation process itself basically consists of a number of actions in a well-defined sequence, and each of these actions processes the database tables in some way. In general, theres a sequence of user interface actions that gather information about the installation process, such as where the user wants to install it, and the features to be installed. During this time, no changes are made to the target system. When this information gathering stage is complete, the installation enters the execute sequence. In this phase, the installation actually takes place. For example, there is a StopServices action in the execute sequence that queries the database ServiceControl table for services that are to be stopped, and the installer stops them. Similarly, there is a StartServices action that queries the ServiceControl table for services that are to be started. These arent necessarily services that youre installing they can be any services installed on the system. There is also a RegisterTypeLibraries action that registers type libraries named in the TypeLib table. These sequences of actions also happen at uninstallation time. When the application is uninstalled, theres a corresponding DeleteServices action to remove services and an UnregisterTypeLibraries action to unregister type libraries. The order of these actions is important. The StopServices action occurs before the StartServices action because during the installation process its typical that services need to be stopped before they can be replaced and updated. Theres a DeleteServices action that occurs after StopServices, and the UnregisterTypeLibraries action (and others) must take place before the RemoveFiles action, which removes installed files. The reason that sequences are important is that when we write custom code to influence the installation process, we often have to choose where in the sequence we will put that code. Also, actions have conditions: you wouldnt want to install and start NT services on a Windows 9x system, so we need a way to condition installation actions based on the environment in which the installation is taking place. This leads us to installation properties.
By using installation properties we can associate state with the installation process. The Windows Installer supplies a number of properties that we can inspect at runtime. For example, the VersionNT property is set by the Installer if the target operating system is a version of Windows NT. The actions relating to services (such as StopServices) are conditioned on VersionNT so that they only occur on versions of Windows that support services. The Version9X property can be used to determine that the system is a Windows 9x system, and like VersionNT, it will tell you the actual version. Be aware that down here in version land, VersionNT extends to Windows 2000. You can also create your own properties. Although you can declare them explicitly and give them values, the code you write can create them just by setting them to a value during the installation. If you want the properties to be global to the installation, they must be in uppercase. Properties are the means by which the code we add to the installation has access to the state of the installation.
Putting Your Code in the Installation
Now that weve seen that properties and conditions are what make the installation behave in a particular way, and that our code must be placed at a particular point in the sequence, what kind of code can we write? To get the Installer to call our code during an installation we specify a particular type of custom action that calls our code. There are many types of custom actions, but were going to focus on the ones in which we can write some code, and Im going to stick to what I think are the two most interesting ones: calling VBScript and calling a DLL.
VBScript has access to properties with the PROPERTY keyword, with the name of the property in quotes. The following VBScript statement is a complete custom action script that will show the value of the built-in company name property COMPANYNAME:
If you wanted the script to set the property, youd say:
PROPERTY("COMPANYNAME") = "My Company"
Note that all the custom actions described here are immediate rather than deferred. These are Windows Installer terms used when the custom action executes, either immediately during the course of the installation or deferred until later. To use the MSIHANDLE for the installation, your code cannot be deferred until later.
One of the areas where VBScript can be useful when installing on Windows 2000 is by using the WMI object model to get system information that can influence the course of the installation. Lets assume that for some reason the install might be required to behave differently depending on the Bios version of a system. For example, manuf.vbs (Listing 2) shows a VBScript that determines the BIOS version using WMI and sets a property called NEWBIOS for a certain type of system with a certain BIOS version. I used my test machine values to illustrate this case. You can then use the value of this property to condition the installation of components that may require the new BIOS. Note that the script sets the property name in uppercase so that it is global to the installation.
You may be wondering where the code for this script is actually stored. The most usual place is in the Binary table of the installation database. If youre using a commercial installation development tool, youll find that they provide an IDE to point at the script of the code, put it into the Binary table, and generally to provide the infrastructure to build the installation database so that your script gets called properly.
You may find it more useful to call code in a DLL when VBScript wont do what youre looking for. The Windows Installer requires a specific prototype, and of course, the function must be exported from your DLL. The required prototype is as follows:
UINT __stdcall StopDriver(MSIHANDLE hInstall)
The MSIHANDLE argument is a handle to the running installation. In the VBScript sample, we accessed the properties of the in-process installation with the PROPERTY keyword. In DLL code, we use MsiSetProperty() and MsiGetProperty(), and these use the MSIHANDLE that was passed into our function. Properties are text strings, and the function we use to get the value of a particular property has the prototype:
UINT MsiGetProperty( MSIHANDLE hInstall, // installer handle LPCTSTR szName, // property identifier, // case-sensitive LPTSTR szValueBuf, // buffer for returned // property value DWORD *pchValueBuf )// in/out: length of // buffer for data.
We would get the value of our COMPANYNAME property with a code fragment like this:
TCHAR szCompName [MAX_PATH]; DWORD dwlen = MAX_PATH; UINT ur = MsiGetProperty(hInstall, "COMPANYNAME", szCompName, &dwlen);
Setting a property is very similar:
MsiSetProperty(hInstall, "COMPANYNAME", szCompName);
In our sample custom action DLL, were going to enumerate some registry entries to find a particular entry in a list. The Windows Installer has some built-in features for searching for specific registry entries, so dont think that you have to write custom action code every time you want to look for a specific file or registry key. (For the details, refer to the AppSearch action in the Windows Installer documentation and the Resources section at the end of this article.) In our case, were going to look for a program name in a series of path names that have been stored in the registry. The path names depend on the configuration of the users system, so we cant predict what the key value might be. There is no support built-in to Windows Installer for enumerating registry data, so we wrote a Visual C++ DLL with an entry point that does the search. The listing shows just the entry point, but you can get the complete project by going on-line. Well discuss the libraries and header files you need when we describe the ListObjs program later. CADll.cpp (Listing 3) shows FindShared(), part of the CADll project in this months code archive. Ive omitted some of the error handling to keep this example compact.
The custom action code enumerates the values in a particular registry entry. The values contain path names such as C:\Program Files\Common Files\MyCompany\Mydll.dll and C:\WinNt\myprog.exe, and were looking for a particular program name another.exe in any of the path names. If we find an occurrence, we set the FOUNDANOTHER property and exit the loop. This property will be set to the full path name in the registry entry. The running installation then has access to the path name and could (for example) run the program. Its a rather artificial situation, but it should illustrate the capabilities of custom actions.
Obviously, you could make changes to the target system with a custom action that runs during the installation. A well-behaved custom action will undo those changes when the application is uninstalled or the installation is aborted.
After the Installation
We have to introduce some terminology here. In the Windows Installer world, an application is referred to as a product. A product will consist of features and components. A feature is usually a product option in the way that (for example) Visual Basic and Visual C++ are features of something called Visual Studio. A feature consists of components (not necessarily in the COM sense) that are all installed when the feature is installed. A well-designed Windows Installer package will consist of features and components, and a component will almost always consist of one executable file (or sometimes a registry path well discuss this later). When a product has been installed with the Windows Installer, the operating system maintains a cache of information from the installation database and a list of installed components. If youve ever been in a situation where you needed to know whether an application was already installed on the system and youre hunting for the registry entry that will tell you, hunt no more. There are well-defined interfaces to tell you which products have been installed and when they have been installed with the Installer. The entire application will have a product code, as will each component. These codes are unique, and as you may have anticipated, they are GUIDs. If you wanted to know if a particular product was installed and you know the GUID for that product, you can use the automation interface from VBScript and ask the installer for the ProductState property. There are also C/C++ equivalents you can use. Well look at some of example code that looks for installed products and components. Well start with the automation interface and assume that we know the product and component GUIDs of the item were looking for. If we built the install package, wed know these, but in FindXL.vbs (Listing 4) Im using the Excel component of Microsoft Office 2000.
In this example, we use the Installer objects ProductState property to see if Microsoft Office is installed or not. This doesnt tell us about Excel yet, but if Microsoft Office isnt installed, then Excel definitely wont be there. We then go on to use the ComponentPath method, passing the product code GUID for Office and Excels component code. This returns us the path to the executable if its installed; otherwise, we get an empty string. Finally, we use the FileVersion method to return the version of the program. Note that the FileVersion method requires only a path, so its a useful way of finding the version of a particular program as long as you know its path.
As I mentioned earlier, a component path may be a registry entry, so we shouldnt be unconditionally using the FileVersion method if the component path is actually a registry key. In this case, we know were looking at Excel, and in our C++ program below, well discuss what a registry component path looks like and what it means.
For our last VBScript, well go through a couple of the collections that the Installer automation model supplies.
In lstoff.vbs (Listing 5), we first enumerate the products installed on the system. Each Product that we get returned has the product code GUID as the default property, and the code then asks for the InstalledProductName property of that particular product using the ProductInfo method. Again, using Microsoft Office as our example, we enumerate all the features in the product code GUID for Microsoft Office. In this case, the Feature property reports the name of the feature. Theres also a FeatureState property we can use to report the installation state of the particular feature. In most cases, youll see a FeatureState value of 3 (otherwise known as msiInstallStateLocal) meaning that the feature is installed on the local system. Sometimes you may see a value of 1 (msiInstallStateAdvertised) meaning that the product is advertised but not fully installed. An advertised feature is one that will be dynamically installed when you first try to use it. If youve ever seen a Windows Installer progress dialog start when you first use a feature, thats because its being dynamically installed.
In our final code example Ill examine a simple C++ console program to enumerate the components installed with the Microsoft Platform SDK. The most important source code is in listobjs.cpp (Listing 6).
For the complete project, you can go on-line, but Ill mention header files and libraries here. You will need to include msiquery.h and link to msi.lib to make use of the Msi* functions. Youll probably find that your Visual C++ versions of these files are older than the Platform SDK versions, so make sure to set your include and library paths to use the Platform SDK versions of these files first.
The program uses MsiQueryProductState() to see if the Platform SDK is installed. If it is installed, the code then gets the product name by calling the MsiGetProductInfo method asking for the INSTALLPROPERTY_INSTALLEDPRODUCTNAME information. (You can see that these APIs are very similar to the automation interfaces we used with VBScript, such as the Installers ProductInfo property.) I then use MsiEnumComponents() to enumerate all the components installed on the system, get the product code for the component, and see if the component is part of the Platform SDK. I do this because there is no interface that enumerates all the components for a given product, so I enumerate each component and see if its product code GUID matches the Platform SDK. MsiEnumComponents() takes an incrementing DWORD that starts at zero and will return ERROR_NO_MORE_ITEMS when all the components have been enumerated. If the component is part of the Platform SDK, use MsiGetComponentPath() (like the automation interface ComponentPath property) to report the path of that component.
In the case that the component path is really a registry entry, it starts with 00, 01, 02, or 03 representing HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, and HKEY_USERS respectively. This is followed by a colon and the key, so youll see components like 01:\Software\CompanyName, meaning HKEY_CURRENT_USER\Software\CompanyName. The code checks if it starts with a registry key string, and if so, deciphers it before printing.
Its important to repeat that a well-designed installation will have only one item per component, usually a file but sometimes a registry entry. Although an installation can be built with more than one item in a component, youll find that the tools and programs that use these programmatic interfaces will not be as useful as they could be because they can only report on one item in a component, usually referred to as the key file (or key path if its a registry path). In addition, the Windows Installer uses the components key file as a check that the installation is correct, so if you have other files in a component that get removed, you will find that the Windows Installer features that repair your application wont repair these non-key items.
Wrapping It Up
Weve focused on the developer interfaces into the Windows Installer and barely touched the surface of the installer itself. Many companies that build installation packages use tools from companies like InstallShield and Wise, and you should find the examples in this article useful in customizing your installation. You can also download Visual Studio Installer from the Microsoft website if you have a Visual Studio license, and while this is useful for small installations, it is not as fully-featured as the products from InstallShield, Wise, and others.
The Windows Installer documentation, together with sample code, is part of the Platform SDK. If youre only interested in the Windows Installer part (or youre a developer of installations) you can download the Windows Installer SDK from Microsoft. If you go to www.microsoft.com/downloads and search by keyword for installer, youll find the Windows Installer SDK and Visual Studio Installer will show up.
The Windows Installer SDK contains a tool called Orca (install it with orca.msi) that you can use to examine the table layout and content of installer databases (and edit them if you know what youre doing). When you install it, it will add itself to the right-click context menu of msi files.
Microsoft hosts a newsgroup Microsoft.public.platformsdk.msi that covers the Windows Installer.
Youll also find that www.installsite.org does an excellent job of keeping you up-to-date with new products, books, tips, and techniques.
About the Author
Phil Wilson is a developer at Unisys Corporation, building COM objects and services. Since February 2000, Phil has been architect for migrating installations to use Windows Installer technology. He has a BSc in Chemistry from the University of Aston in the UK.