Application Resiliency: Unlock the Hidden Features of Windows Installer
Summary: Windows Installer has several features that have largely gone unnoticed by the development community. These features allow an application to repair itself at runtime, or to install optional components based on the user's interaction with the application. (10 printed pages)
Download MSI Integration Sample Code.msi.
As developers, we really tend to think of our applications running in ideal environments, on ideal systems, and by ideal users who are using the application after a successful installation. The reality is that after our applications have been successfully installed, their lifetime for that user has only just begun. The challenges our application can face in remaining stable and functional are many, but most applications aren't prepared to deal with changes in the operating environment that could render the application inoperable.
Windows Installer provides resiliency features that have made significant strides in keeping our applications stable, but this capability is based on certain actions the user takes while interacting with the shell to provide "entry points" through which Windows Installer can detect problems with the configuration of the application and take steps to repair it.
Here's a short list of Windows Installer "entry points":
- Shortcuts. Windows Installer introduces a special type of shortcut which, while transparent to the user, contains additional metadata that Windows Installer uses through its shell integration to verify the state of the specified application's installation prior to launching the application.
- File Associations. Windows Installer provides a mechanism for intercepting calls for a document or file's associated application so that when a user opens a document or file using the shell, Windows Installer can verify the application before launching the associated application.
- COM Advertising. Windows Installer provides a mechanism that is hooked into the COM subsystem, so that any application that creates an instance of a COM component installed by Windows Installer (and configured to use this feature) will receive an instance of that component after Windows Installer has verified the state of that component's installation.
Under certain circumstances, the built-in resiliency features of Windows Installer may not be able to detect all the problems with our application's configuration, or our application may function in such a way that the required entry points are not being activated. Fortunately, the smart guys on the Windows Installer team understood this challenge and made additional resiliency features available to us through the rich Windows Installer API.
Before we move on to the advanced resiliency features that Windows Installer API provides, let's take a look at a typical resiliency scenario that we usually get for free when we deploy our apps with Windows Installer.
In this scenario, we are deploying a simple text editing application we'll call SimplePad. To create the installation, we are going to use Microsoft's Open Source WiX Toolkit (for more info see http://sourceforge.net/projects/wix.), but you can accomplish the same with any tool of your choosing.
<Directory Id="TARGETDIR" Name="SourceDir"> <Component Id="SimplePad" Guid="BDDFA5DC-BD69-4232-998E-5167814C21B9" KeyPath="no"> <File Id="SimplePadConfig" Name="SP.cfg" src="$(var.SrcFilesPath)SimplePad.exe.config" LongName="SimplePad.exe.config" Vital="yes" KeyPath="no" DiskId="1" /> <File Id="SimplePad" Name="Simple~1.exe" src="$(var.SrcFilesPath)SimplePad.EXE" LongName="SimplePad.exe" Vital="yes" KeyPath="yes" DiskId="1" > <Shortcut Id="SC1" Advertise="yes" Directory="ProgramMenuFolder" Name="SimpPad" LongName="Run SimplePad" /> </File> </Component> <Directory Id="ProgramMenuFolder" Name="ProgMenu"></Directory> </Directory>
As you can see in the XML fragment above, we've created a very simple installation with one file (SimplePad.exe) and a single shortcut located in the user's Start menu. It is important to note that in this example, the shortcut we are creating is the entry point that Windows Installer will use to detect the state of our application and repair it as needed.
At this point, we can build our installer, install the application, and use the newly created Start menu shortcut to run it. As expected, the application will function exactly as intended. To test the built-in resiliency features of Windows Installer, we can delete the SimplePad.exe file and try running the application from the Start menu shortcut. Again, as expected, Windows Installer detects that SimplePad.exe is missing, and a repair is launched. During the repair operation, Windows Installer reads the required configuration information from its internally cached copy of the installation package, and finally replaces the missing file, prompting the user for the source installation media if it is not present in the original location from which it was installed. Once the repair operation has completed, the application is launched as normal.
Figure 1. Repair operation in progress
Application resiliency is also provided by Windows Installer through a couple of other mechanism worth mentioning here as well. The second most common method of ensuring applications remain highly available is through Windows Installer file associations. This mechanism operates very much the same way as Windows Installer shortcuts, but instead of directly linking to an application's executable file, the association is made by a registered file type. As you can see in Figure 2, Windows Installer file associations are defined using the same mechanism that standard file associations use, with one exception. Notice in Figure 2 that an extra value is listed under the typical "shell\Open\command" registry key. The extra value (also named "command") is where Windows Installer looks any time you double-click on a file from within the Windows shell. This cryptic-looking string, sometimes referred to as a "Darwin Descriptor," is actually an encoded representation of a specific product, component, and feature. If this extra value exists, Windows Installer will decode the data, and use it to perform checks against that product and component. If the component is found to be missing or corrupt, Windows Installer will launch a repair to restore the missing file or data, and finally launch the referenced application as normal, passing the appropriate command-line options to it.
Figure 2. Viewing a "Darwin Descriptor" for a file association
The final resiliency mechanism we'll discuss today is commonly known as COM Advertising. Before we look at the mechanics of COM Advertising, it is important to understand the use case behind it. Let's say you were a component vendor who provides a COM-based shared library that provides real-time postal rates. Since this component may be used by many different products, it is installed to a single shared location on the end-user's system. To ensure the component always installs to the same location, and to ensure the component remains highly available, you ship it to your customers in a Merge Module properly configured to take advantage of COM Advertising. Of course, since your solution ships as a single .dll file with no user interface, the other mechanisms of resiliency simply won't suffice. In this case, we can rely on COM Advertising to ensure our component remains properly installed and registered on the user's system. When an application creates an instance of this component through normal COM mechanisms, Windows Installer "hooks" into the process in just the same way we saw it do with file associations. Notice in Figure 3 that this time a "Darwin Descriptor" is stored in the InprocServer32 registry value for our component's COM registration. Again, this information is decoded and used by Windows Installer to ensure that our component is properly installed and configured, performing any repairs as needed, before finally returning an instance of your component to the calling application.
It's worth pointing out that this unique feature works completely independently of the application using the component. In other words, even if the application using the component was not installed using Windows Installer, the COM Advertising employed by the component will continue to function properly, even if the calling application is merely a VBScript.
Figure 3. Viewing a "Darwin Descriptor" for a COM Server
So far, everything we've talked about and demonstrated has taken advantage of the capabilities of Windows Installer without needing to write a single line of code, but now it's time to move on to a richer, more robust implementation.
Windows Installer's default behavior worked well for us in the previous scenario, but often, in the real world, we have slightly more sophisticated applications. Let's expand our sample scenario to deal with a more challenging scenario.
Often an application is comprised of more than one executable file. One example might be an application that uses a bootstrapper executable to check for and install updates to an application as seen in the Updater Application Block. In this case, the first executable is the one that is invoked when the user clicks a shortcut on the Start menu. It in turn launches the second executable that contains the main user interface of your application. Depending on how your installation was configured, there's a good chance that problems with the main application executable will go undetected by the Windows Installer engine. While one option might be to write a bunch of code that runs at start-up that inspects the runtime environment, this simply won't work if the executable itself is missing or corrupted and, moreover, would not be able to easily repair the problem. A much more effective solution is to leverage Windows Installer's knowledge of your application's configuration that is already defined in your deployment package.
The Windows Installer API exposes the same mechanisms for verifying the integrity of an application that it uses when the user interacts with the shell. By using these API calls from within our application, we can be sure of still achieving the same benefits without the reliance on shell "entry points" discussed earlier.
Here is a list of scenarios not covered by the shell-integration resiliency features of Windows Installer:
- Apps that start with the OS (run or run-once registry keys)
- System services
- Scheduled tasks
- Apps executed by other apps
- Command-line apps
I'm sure there are many more scenarios that we could add to the list above, but I think you get the idea. In the following example, I will demonstrate how we can gain the benefits of Windows Installer resiliency without the reliance on the shell integration features we discussed earlier. When we are done, you'll be able to take these concepts and easily apply them to just about any scenario that involves running executable code.
Before we dig into some example scenarios, let's take a look at some of the key Windows Installer APIs that you can use from within your applications. For specific information on the usage of each of these APIs, please refer to the Windows Installer Finction Reference in the Platform SDK.
|Key Windows Installer Functions||Description|
|MsiProvideComponent||Retrieves the installed location of a component, installing or repairing as needed to ensure the component is available.|
|MsiQueryFeatureState||Returns the installation state of a given feature. For example, this function tells you if the feature is installed, not installed, or advertised.|
|MsiQueryProductState||Returns the installation state of a product. For example, this function tells you if the product is installed, advertised, installed for another user, or not installed at all.|
|These two functions allow you to programmatically install or uninstall an application. MsiConfigureProductEx provides greater control by allowing you to specify options similar to what you would normally do on the command line.|
|MsiConfigureFeature||This function allows you to install, uninstall, or advertise a specific feature of an application.|
|MsiGetUserInfo||This function returns the name of the user, the organization, and the product serial number collected during the product's installation sequence.|
|These two functions assist you in determining the physical location of a component file or registry key on the target system. MsiGetComponentPath returns the path of the component's instance installed by a specific product, while MsiLocateComponent returns the first instance of a component installed by ANY product.|
Previously we talked about a very basic scenario in which we could actually delete our application's executable from the system and use a shortcut to cause Windows Installer to detect and repair the problem by reinstalling the missing file. While that scenario worked well for demonstrating the shell integration that Windows Installer leverages, to take these concepts deeper, we are going to take a look at a slightly more sophisticated scenario.
In this scenario, our application is comprised of a single .exe file and several text files that provide critical configuration information to the application.
The technical support staff at our hypothetical software company has been receiving a lot of support requests that reveal that application configuration problems are not being solved by Windows Installer due to the fact that users are running the application directly by double-clicking on the executable in Windows Explorer instead of using the Start menu shortcut created by our installation.
After consulting with the team's resident deployment expert, our team of engineers decides that the application would benefit greatly by performing its own resiliency check at start-up to ensure it is properly configured. To accomplish this, the team simply adds a call to the MsiProvideComponent API to ensure that the critical components defined in the application's installation package are properly installed and configured.
<DllImport("msi.dll")> _ Private Shared Function MsiProvideComponent(ByVal szProduct As String, ByVal _ szFeature As String, ByVal szComponent As String, ByVal dwInstallMode As _ MSI_REINSTALLMODE, ByVal lpPathBuf As System.Text.StringBuilder, ByRef _ pcchPathBuf As IntPtr) As Integer End Function Public Shared Function ProvideComponent(ByVal productCode As String, ByVal _ featureName As String, ByVal compID As String) As String Dim iRet As Integer Dim cbBuffer As Integer = 255 Dim buffer1 As New System.text.StringBuilder(cbBuffer) Dim pSize As New IntPtr(cbBuffer) iRet = MsiProvideComponent(productCode, featureName, compID, _ MSI_INSTALLMODE.INSTALLMODE_DEFAULT, buffer1, pSize) Return buffer1.ToString End Function
To better encapsulate this code, a new Class called WIHelper is added to the project to house Windows Installer API method declarations and wrapper methods. Calling this code was a simple matter of adding a few lines to our main form's Load event handler.
Private CONST PRODUCTID As String = "PRODUCT_GUID_HERE" Private CONST MAIN_FEATUREID As String = "DefaultFeatureKey" Private CONST COMPID_1 As String = "COMP1_GUID_HERE" Private CONST COMPID_2 As String = "COMP2_GUID_HERE" Private Sub MainForm_Load() Handles MyBase.Load If WIHelper.IsProductInstalled(PRODUCTID) Then WIHelper.ProvideComponent(PRODUCTID, MAIN_FEATUREID, COMPID_1) WIHelper.ProvideComponent(PRODUCTID, MAIN_FEATUREID, COMPID_2) End If End Sub
In the sample code above, we are first testing to see if our application has actually been installed via its installation package or not. This is an important concept since we want to be sure that even if we are debugging in the development environment, our application will still function properly. To accomplish this, we call a method in our helper class called IsProductInstalled. This method, in turn, simply calls MsiQueryProductState to determine if the product has been installed on the system. If our call to IsProductInstalled reveals that our product has been installed, then we make a series of calls to the ProvideComponent method in our helper class. This method is, again, a simple wrapper around the MsiProvideComponent API, which returns the full path to the specified component and ensures that the component is properly installed and ready for use. Depending on the needs of your specific products, you can call the ProvideComponent method as many times as you like to ensure your application is fully available for the user.
Our hypothetical companies sales executives have been hearing a lot of feedback from customers that they would like to see a set of standard templates delivered with SimplePad. While some customers have expressed a strong desire for this feature, others have voiced concern about installing extraneous data that most of their users may not need.
After overhearing the engineers discussing how to deal with this new requirement, our intrepid Installation Engineer jumps in and points out that Windows Installer can easily handle this with just a small amount of additional coding.
After some quick planning, the team decides that they will implement a new Templates menu item in the application's File menu. If the templates feature is installed locally on the user's system, the user will see a fly-out menu listing each of the available templates, and an option to uninstall the templates feature. If the templates feature has not been installed, the templates fly-out menu will have a single entry, enabling the user to install the additional templates.
Private Sub mnuFile_Popup(ByVal sender As Object, ByVal e As System.EventArgs) _ Handles mnuFile.Popup Dim newItem As MenuItem With mnuTemplates.MenuItems .Clear() If WIHelper.IsFeatureInstalled(PRODUCTID, TEMPLATES_FEATUREID) Then Dim dirInfo As New DirectoryInfo(Application.ExecutablePath) For Each dirFile As FileInfo In dirInfo.Parent.GetFiles("*.tpl") Dim mi As New MenuItem(Path.GetFileNameWithoutExtension(dirFile.Name)) AddHandler mi.Click, AddressOf OpenTemplate .Add(mi) Next .Add("-") newItem = New MenuItem("Uninstall Templates") AddHandler newItem.Click, AddressOf UninstallTemplates .Add(newItem) Else newItem = New MenuItem("Install Templates") AddHandler newItem.Click, AddressOf InstallTemplates .Add(newItem) End If End With End Sub
As you can see, we first check to see if the templates feature is installed. If it is, then we enumerate through the files in our applications folder that have the "tpl" extension and add the name of each template to the menu. If it is not, we simply add an option for the user to install the templates. Before we look at that, let's first look at how we determine whether the templates feature is installed.
<DllImport("msi.dll")> _ Private Shared Function MsiQueryFeatureState(ByVal szProduct As String, ByVal szFeature As String) As MSI_INSTALLSTATE End Function Public Shared Function IsFeatureInstalled(ByVal pid As String, ByVal fid As String) As Boolean Return MsiQueryFeatureState(pid, fid) = MSI_INSTALLSTATE.INSTALLSTATE_LOCAL End Function
In this simple function, we simply call the Windows Installer MsiQueryFeatureState function, passing in our application's ProductCode and the name of the feature we are inquiring about. If Windows Installer returns INSTALLSTATE_LOCAL then we return true as this means that the feature is installed locally.
Installing and uninstalling our templates feature is accomplished just as easily.
<DllImport("msi.dll")> _ Private Shared Function MsiConfigureFeature(ByVal szProduct As String, ByVal szFeature As String, ByVal eInstallState As MSI_INSTALLSTATE) As Integer End Function Public Shared Function InstallFeature(ByVal pid As String, ByVal fid As String) As Boolean Return MsiConfigureFeature(pid, fid, MSI_INSTALLSTATE.INSTALLSTATE_LOCAL) = ERROR_SUCCESS End Function Public Shared Function UninstallFeature(ByVal pid As String, ByVal fid As String) As Boolean Return MsiConfigureFeature(pid, fid, MSI_INSTALLSTATE.INSTALLSTATE_ABSENT) = ERROR_SUCCESS End Function
When the user clicks the "Install Templates" menu item, a call is made to MsiConfigureFeature with the ProductCode, the name of the feature we want to configure, and an enumeration value indicating we want to install the feature locally. The user will see a Windows Installer progress dialog appear briefly while the templates feature is installed. When the dialog disappears, the templates will be installed and ready for use. When the user goes back to the File menu, the templates submenu will populate with the names of the templates as described above.
Leveraging the "free" features and API exposed by Windows Installer provides us with some cool capabilities that go a long way towards reducing support costs, increasing application stability, and enhancing the user experience. The examples demonstrated here are somewhat trivial in nature, but hopefully will form a great starting point for implementing your own unique solutions. We've looked at some of the APIs available, but we certainly haven't covered them all. Take some time to explore all the features of the Windows Installer API and I know that you'll be pleasantly surprised at how easily you can leverage these relatively untapped features of Windows Installer.
About the author
Michael Sanford is President and Chief Software Architect for 701 Software (http://www.701software.com). Prior to forming 701, Michael was President and CEO of ActiveInstall Corporation, which was acquired by Zero G Software. ActiveInstall achieved notoriety for its Windows Installer Authoring solutions. Michael is a Microsoft Certified Solution Developer (MCSD), Microsoft Certified Systems Engineer (MCSE), and a Windows Installer MVP. You can read Michael's Blog at http://msmvps.com/michael.