Scripting Solutions with WSH and COM: Customizing Windows Installer Applications


Windows Script Host

Last month, I introduced you to the Windows Installer technology. This month, I continue the discussion about this key technology by showing you how to modify applications that have been installed with the Windows Installer service. I could show you how to use a script to add files to a Windows Installer installation database or change a host of properties. However, you can easily accomplish these tasks with the Windows Installer tools, such as msiexec.exe. Instead, I show you how to accomplish several tasks that you can't accomplish with these tools, such as how to add and replace a source path (i.e., a path to a product's installation directory).

In these examples, Microsoft Office XP beta 2 (formerly code-named Office 10) and Office 2000 Premium Service Release 1 (SR1) are the products I installed with Windows Installer. Writing scripts for products still in beta isn't daft. With such preparation, you'll be ready for deployment as soon as the software ships. Plus, you can use the scripts I provide this month not only for Office XP beta 2 and Office 2000 Premium SR1 but also for the final version of Office XP, other versions of Office 2000, or any Windows Installer application, as long as you use the correct globally unique ID (GUID) for the product.

Windows Installer Automation Problem
The key property that I discuss this month is InstallSource, which you can use to obtain a source path. You can adapt the script RegisteredApps.vbs I discussed last month to display this path. In RegisteredApps.vbs, you can replace the code in Listing 1 with the code in Listing 2. Now if you run the script, you'll receive results similar to those that Figure 1 shows.

However, while writing this article, I discovered a problem with the source path that InstallSource returns. As I mentioned previously, a product's source path leads to its installation directory. This directory contains the files that Windows Installer uses to install, update, repair, or uninstall that product. In the registry, the information about the uninstall files is in a different part of the registry than the information about the install, repair, and update files.

For example, in my Windows 2000 machine, the subkey related to the uninstallation of Office 2000 Premium SR1 is HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\\{00000409-78E1-11D2-B60F-006097C998E7\}\InstallSource. However, the subkeys related to the installation, update, and repair of this application are, respectively,

  • HKEY_CLASSES_ROOT\Installer\Products\904000001E872D116BF00006799C897E\SourceList\Net\1
  • HKEY_CLASSES_ROOT\Installer\Products\904000001E872D116BF00006799C897E\SourceList\Net\2
  • HKEY_CLASSES_ROOT\Installer\Products\904000001E872D116BF00006799C897E\SourceList\Net\3

This registry setup can lead to problems if you use scripts to change or read Windows Installer source paths. According to the Microsoft Developer Network (MSDN) Online Library (, you use the Installer::AddSource method to change a source path and the Installer::ProductInfo property to read a source path. However, AddSource and ProductInfo use different APIs that talk to different parts of the registry. As a result, if you use AddSource to change a source path, the system writes the changes to only the HKEY_CLASSES_ROOT subtree's install, update, and repair subkeys and not the HKEY_LOCAL_MACHINE subtree's uninstall subkey. Similarly, ProductInfo reads data from only the HKEY_LOCAL_MACHINE subtree's uninstall subkey. So, if you use AddSource to change a source path, then use ProductInfo to check whether that source path changed, it appears as if it didn't. In this case, you'd need to use a registry editor to manually check the HKEY_CLASSES_ROOT subkeys for the new source path.

So, if you adapt RegisteredApps.vbs, you should use the script to only read the source path prior to making any changes with Installer::AddSource. If you want to check the path after you've made changes, you should manually check the registry.

This problem is annoying but easily resolved. However, a few more unresolved problems exist. For example, in the registry paths I just mentioned, you might have noticed that the GUIDs in the HKEY_LOCAL_MACHINE and HKEY_CLASSES_ROOT differ. The GUID in the HKEY_LOCAL_MACHINE subtree is the same as the product's GUID. I'm currently working with Microsoft to discover why the GUID in the HKEY_CLASSES_ROOT subtree differs and how to solve this problem. When I have the answers, I'll share them with you. In the meantime, let's look into how to use Installer::AddSource and some other Installer object methods to modify the install, update, and repair paths in the registry.

Adding Paths
Let's say you've used Windows Installer to roll out Office XP beta 2 to all your users worldwide. You used installation points for the product on the \software\officexp\beta2\cd1\ share on two servers in the UK, a server in the United States, a server in France, and a central server. However, since you performed this global rollout, you've installed secondary servers to back up the five primary servers and created installation points for Office XP beta 2 on each secondary server as well.

You now want to set up a failover system. This setup will ensure minimal interruption to users if the primary server goes down. This setup will also let users take advantage of install-on-demand technology to download extra components automatically.

The task at hand isn't particularly difficult. On every client PC, you need to run a script that connects to the product you're updating (in this case, Office XP beta 2 on the client PC) and adds a backup installation source path to the installation source on the secondary server. Because you're using one script for all the clients but the clients connect to different primary and secondary servers, the script needs to

  • Include all possible secondary source paths
  • Include a mechanism that lets each client select the appropriate secondary source path

Listing 3 contains a sample script, AddBackupPathForOneProduct.vbs, that you can create. You begin the script by defining the product's GUID as a constant. You then create an instance of the Windows Installer object that represents the product. You use the Installer::ProductInfo method, which I discussed last month, to query the object. However, this time, you pass in InstallSource as a parameter to retrieve the source path for the product you installed on the primary server (i.e., the primary source path). You assign the retrieved primary source path to the strInstallSource variable.

Next, you use a Select Case statement to select the correct secondary source path based on the primary source path in the strInstallSource variable. The Select Case statement compares the variable's primary source path against a list of possible primary source paths. If the statement finds a match, it assigns the strServerPath variable the appropriate secondary source path. For example, if the matched primary source path is \\frserver01\software\officexp\beta2\cd1\, the statement assigns the secondary source path \\fritserver01\software\officexp\beta2\cd1\ to strServerPath. If the statement doesn't find a match (i.e., you installed the client from the central server), the statement's Else clause assigns the central server's secondary source path to strServerPath.

Now that strServerPath contains the secondary source path, you can use the Installer::AddSource method to add this path to the product. The method takes three parameters, the first of which is the GUID constant. The second parameter is the name of the user for whom you installed the product. If the user is part of a Win2K or Windows NT domain, you use the format domain\username to specify the name. If the user is on Windows Millennium Edition (Windows Me), Windows 9x, or another machine that's not part of a domain, you specify only the username. If any logged-on user can use the product (i.e., the product is machine specific rather than user specific), you put an empty string as the second parameter, as callout A in Listing 3 shows. The third parameter is the secondary source path.

AddBackupPathForOneProduct.vbs and the other scripts I discuss this month work on Win2K systems or on NT 4.0, Windows Me, or Win9x systems in which you've installed the Windows Installer technology. You can place these scripts on any machine (e.g., a local PC, the central server), as long as the client can run them.

Replacing Paths
Suppose you now need to replace the primary and backup servers in the United States and France with one single new server. Thus, you need to wipe out all paths and put a single new path in their place. As the script ReplacePathForOneProduct.vbs in Listing 4, page 13, shows, replacing source paths is simple. You begin ReplacePatForOneProduct.vbs the same way you began AddBackupPathForOneProduct.vbs. You define the GUID constant, create an instance of the Installer object, use the Installer::ProductInfo method to retrieve the primary source path, and assign that path to the strInstallSource variable. However, you use the Select Case statement differently in ReplacePathForOneProduct.vbs. This time, the statement includes only two possible primary source paths: a path for the US primary server and a path for the French primary server. If the path in strInstallSource doesn't match one of these two paths, the Else clause uses the WScript::Quit method to stop the script so that no inadvertent replacements take place. If the path in strInstallSource matches one of these two paths, the statement assigns the new primary source path to the strServerPath variable.

Before you can add the new primary source path, you need to delete the old one. As callout A in Listing 4 shows, you use the Installer::ClearSourceList method. This method takes two parameters: the product's GUID and the name of the person who's using the product (or an empty string if the product is machine specific). After you delete the old path, you use the Installer::AddSource method to add the new primary source path.

Replacing the Paths of All Products
You might have noticed a problem with the last scenario. You put in a new server yet replaced the path of only one product. A more likely scenario is that you'll need to replace the source paths of many different products. If each product has a separate install path on the installation source server, the script would get very long.

A better approach exists. If you want to replace the source paths of all the products installed on a machine, you can incorporate a For Each...Next loop similar to the loop I used in RegisteredApps.vbs. The script ReplacePathForAllInstalledProducts.vbs on the Windows Scripting Solutions Web site uses such a loop. First, the script creates an instance of the Installer object. Then, it uses a For Each...Next loop to iterate through all the products that the Installer::Products property enumerates. In each iteration, the loop places the product's GUID in the msiProduct variable. The loop uses the GUID to retrieve the old primary source path, then uses the Select Case statement to assign the new primary source path to strServerPath. After the Installer::ClearSourceList method deletes the old path, the Installer::AddSource method adds the new path to the product.

To use ReplacePathForAllInstalledProducts.vbs, you must have all the products on one share on each server and all the .msi setup files in the same directory. If you have multiple shares, you can use one Select Case statement per product, letting you set the path for that specific product.

Time Well Spent
As these examples show, you can easily customize the installation options of the products you use Windows Installer to install. Eventually, Windows Installer will make setup.exe installations obsolete, so learning how to use and customize Windows Installer applications is time well spent.

Hide comments


  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.