When writing a script, you don't always get it right the first time. That was a worthwhile lesson that our engineering team recently learned. Every month when Microsoft releases security updates, our engineering team must automate their installation. Generally, this is a simple task of running the patch executable with switches for silent remote deployment through HP's ProLiant Essentials Rapid Deployment Package (RDP), our deployment standard for Windows 2000 servers. However, there are times when a security update isn't a simple install. For example, that was the case with the security update commonly known as the graphics device interface (GDI) update associated with Microsoft Security Bulletin MS04-028 "Buffer Overrun in JPEG Processing (GDI+) Could Allow Code Execution" ( The GDI security update affected various versions and editions of Windows and its components, including the Windows .NET Framework 1.1 and Framework 1.0 Service Pack 2 (SP2).

In the side-by-side execution model, Framework 1.1 and 1.0 coexist on a system, which lets developers use either or both versions in their applications. However, in most cases, it's simpler to upgrade to version 1.1 and not support side-by-side execution, provided the applications compiled for version 1.0 continue to function as expected. (In most cases, applications compiled for version 1.0 will continue to work properly on version 1.1. For more information about the versions, see "Versioning, Compatibility, and Side-by-Side Execution in the .NET Framework,"

Because the GDI security update affected both Framework versions and we support side-by-side execution, it was essential for us to detect each server's Framework version and service pack level to ensure that the correct update would be applied. Moreover, because the Framework is an optional install in our Windows 2000 build, we also had to ensure that servers with no Framework installed didn't receive a GDI update. Thus, we decided to create a script that queries a server for its installed products and identifies the installed Framework versions and service pack levels if found. Although it took a few attempts to get it right, the result was worth it.

The First Attempt
There are multiple ways to confirm that a product is installed on a system. For example, you can read the registry, read an .ini file, or look for a specific file. Because both Framework versions were installed with Windows Installer (i.e., .msi packages) we found it easiest to use Windows Management Instrumentation's (WMI's) Win32_Product class and its properties. Using WMI's ExecQuery method to query the Win32_Product class returns instances of the Win32_Product class that represents products installed with Windows Installer. We then enumerated and compared the names of the products installed on a system with the display names of Framework 1.1 and Framework 1.0.

As Listing 1 shows, the script begins by declaring the computer name on which the script is to run. The strComputer variable stores this value. Because the script is to connect to the local computer, strComputer is set to a period. Next, the script calls the GetObject method to connect to WMI's root\cimv2 namespace, which contains the Win32_Product class.

Callout A in Listing 1 shows the heart of the script. This code stores the Windows Query Language (WQL) query in the wqlQuery variable, then calls WMI's ExecQuery method to run that query. Running the query returns a collection of objects, which the code assigns to the colProducts variable. Using a For Each...Next statement, the script iterates through the collection. The Select Case statement in the For Each...Next loop compares the name of the product (exposed by the Win32_Product class's Name property) with Microsoft .NET Framework 1.1 and Microsoft .NET Framework (English) to confirm if any or both Framework products are installed on the server.

The Second Attempt
The WQL query in callout A in Listing 1 is a general query that will return a collection of all installed .msi packages. Thus, the For Each...Next loop must iterate through the entire collection. Because we were interested in only querying for the Framework, we decided to optimize the query by modifying it to include a Where clause that limited the search to only the installed Framework products. Listing 2 shows the script with the optimized query.

We also optimized the ExecQuery method by including the wbemFlagReturnImmediately and wbemFlagForwardOnly flags, as callout A in Listing 2 shows. The wbemFlagReturnImmediately flag ensures that the WMI call doesn't wait for the query to complete before returning the result. The wbemFlagForwardOnly flag ensures that forward-only enumerators are returned. Forward-only enumerators are generally faster than bidirectional enumerators. (To learn more about the ExecQuery method and its parameters and flags, go to

The Third Attempt
Easier is not always better, as we learned after running the script in Listing 2. Although it was easiest to write a script using Win32_Product class, the class took up to 20 seconds to return the results, even with the optimized query. Because we were to run this script on more than 200 servers, the long wait wasn't acceptable. As an alternative, we decided to try a different approach: check the registry to confirm whether the Framework was installed on each server. The result is the script that Listing 3 shows.

Like the scripts in Listing 1 and Listing 2, the script in Listing 3 begins by setting the strComputerName variable to the local computer, but this is where the similarity ends. The differences begin with the WMI moniker statement in the GetObject call, which callout A in Listing 3 shows. In the other two scripts, the WMI moniker included the root\cimv2 namespace. For registry management, WMI provides the StdRegProv class. All WMI versions include and register the StdRegProv class, so WMI places this class in the root\default namespace by default. Thus, you have to connect to the root\default namespace (and not the root\cimv2 namespace) to use the StdRegProv provider.

StdRegProv exposes the GetStringValue method, which you can use to read the data from a registry entry whose value is of type REG_SZ. When you use the GetStringValue method, you must include four parameters in the following order:

Key tree root. The key tree root parameter specifies the target hive in the registry. Web Table 1 shows the UInt32 values (i.e., numeric constants) that represent the hives. The default hive is HKEY_LOCAL_MACHINE, which has a value of &H80000002.

Subkey. The subkey parameter specifies the registry path (not including the hive) to the registry entry that contains the value you want to retrieve.

Entry. The entry parameter specifies the name of the entry from which you're retrieving the value.

Out variable. The GetStringValue method reads the specified entry's value into a variable. You use the out variable parameter to specify the name of that variable.

All applications that can be uninstalled create a subkey under the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall key. Thus, the Uninstall key is the base key under which the script looks for one of three subkeys: Microsoft .NET Framework (English), Microsoft .NET Framework Full v1.0.3705 (1033), or \{CB2F7EDD-9D1F-43C1-90FC-4F52EAE172A1\}. Microsoft .NET Framework (English) and Microsoft .NET Framework Full v1.0.3705 (1033) both represent Framework 1.0. When I first downloaded Framework 1.0 a while back, the subkey created was named Microsoft .NET Framework (English). If you download and install Framework 1.0 now, the subkey name is Microsoft .NET Framework Full v1.0.3705 (1033). (Microsoft sometimes changes names to make them more meaningful.) The \{CB2F7EDD-9D1F-43C1-90FC-4F52EAE172A1\} subkey represents Framework 1.1.

As callout B in Listing 3 shows, the script uses a constant and two variables to provide the registry information. First, the script sets the HKEY_LOCAL_MACHINE constant to &H80000002. That constant is used for the GetStringValue method's key tree root parameter. Because the script is checking three subkeys under the Uninstall key, the script uses two variables—strKey and arrKeysToCheck—to specify the subkey parameter. The strKey variable holds the string SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall. The arrKeysToCheck variable contains an array that includes three elements: the strings Microsoft .NET Framework (English), Microsoft .NET Framework Full v1.0.3705 (1033), and \{CB2F7EDD-9D1F-43C1-90FC-4F52EAE172A1\}.

In Listing 3, the GetStringValue method's entry parameter is DisplayName. Remember that the Win32_Product class's Name property exposes the name of the products installed by Windows Installer. In the registry, the DisplayName entry stores the Name property's value. GetStringValue's last parameter is the strDisplayName variable.

When the script runs, it iterates through the arrKeysToCheck array and calls the StdRegProv's GetStringValue method for each element in the array, as callout C in Listing 3 shows. After GetStringValue passes the DisplayName entry's value to the strDisplayName variable, the script displays the value.

On completion, the GetStringValue method returns a 0 if successful or some other value if an error occurred. Because the Framework was an optional install in our environment, there are instances in which none of the three subkeys exist in the registry. Such instances could cause the GetStringValue method to fail and throw an error, which in turn could cause the script to fail. To avoid this problem, the On Error Resume Next statement appears before calling the For Each...Next loop to ensure that the script continues to run, even if GetStringValue throws an error.

The script in Listing 3 provided our engineering team with a viable, fast solution that determined whether Framework 1.1, Framework 1.0, or both were installed on a machine. However, we also needed to determine the Framework service pack levels. So, we decided to build on Listing 3 and add code to check for service pack information.

After some investigation, we discovered that Microsoft doesn't provide a direct means to detect Framework service pack levels. However, there are indirect means. For Framework 1.1, you can check the SP registry entry under the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v1.1.4322 subkey. A value of 1 means that Framework 1.1 SP1 is installed.

Checking the registry won't work for Framework 1.0. Instead, Microsoft suggests that you check the version of the mscorcfg.dll file. Web Table 2 shows the correlation between the file versions and the service pack levels.

With the service pack information in hand, we started writing the DetectFramework.vbs script that Listing 4 shows. The first part of DetectFramework.vbs should look familiar. It uses the StdRegProv class's GetStringValue method to read the DisplayName entry's value into the strDisplayName variable. However, rather than display strDisplayName's value, the script starts to go through a series of embedded If...Then...Else and Select Case statements. If the GetStringValue method returns a value of 0 (i.e., the method was successful and thus a Framework version is installed), the script proceeds to a Select Case statement that determines whether strDisplayName's value is the string Microsoft .NET Framework 1.1 or the string Microsoft .NET Framework (English).

Microsoft .NET Framework 1.1. When strDisplayName contains the string Microsoft .NET Framework 1.1, the script checks the SP registry entry for the value of 1. To do so, it uses StdRegProv's GetDWORDValue method, which reads data from a registry entry whose value is of type REG_DWORD. Like GetStringValue, GetDWORDValue requires four parameters specifying the hive, the subkey name, the entry name, and the name of the variable that will store the entry's value.

After GetDWORDValue reads the SP entry's value, the Select Case statement at callout A in Listing 4 compares that value against the value of 1. When a match occurs, the script displays the message Microsoft .NET Framework 1.1 SP1 found. When a match doesn't occur, the script displays the message Microsoft .NET Framework 1.1 found. In other words, although Framework 1.1 was installed, SP1 wasn't.

Microsoft .NET Framework (English). When strDisplayName contains the string Microsoft .NET Framework (English), the script checks the version of the mscorcfg.dll file. The Microsoft Scripting Runtime Library's FileSystemObject object provides the GetFileVersion method, which returns the version of a given file. This method requires only one parameter: the path to the target file.

Before using GetFileVersion, though, the script checks for the file's existence. Although mscorcfg.dll must exist on the system if Framework 1.0 is installed, it's good scripting practice to check for a file's existence before attempting to get its version number. This practice ensures that the script won't stop with a File Not Found error at runtime.

To check for the mscorcfg.dll file's existence, the script uses the FileSystemObject object's FileExists method, as the code at callout B in Listing 4 shows. Like the GetFileVersion method, the FileExists method's only parameter is the path to the target file. If the file exists, the script calls the GetFileVersion method.

The Select Case statement at callout C in Listing 4 compares the version number that GetFileVersion returns with the four possible version numbers shown in Web Table 2. When a match occurs, the script displays the corresponding message that identifies the service pack level.

To confirm that DetectFramework.vbs correctly detects the installed Framework version and service pack level, we tested the script by running it on a server installed with Framework 1.0 only, a server installed with Framework 1.1 only, a server installed with both versions, and a server that didn't have Framework installed. Further, after installing the latest service pack for each Framework version, we tested the script to ensure that we received the expected results. And expected results are what we received.

Challenging But Worth It
Although it took a lot of trial and error to create DetectFramework.vbs, it was worth the effort. And, as we state in engineering, a world without challenges would be a very boring world.