Sunday, May 18, 2008

Deployment Tools Foundation (DTF) Managed Custom Actions

Background

Note: For the purpose of brevity, this blog post is going to assume that the reader already has a strong understanding of the Windows Installer architecture and philosophy. I will not attempt to fully cover the declarative and transactional design goals of MSI or the sordid details of the pro’s and con’s of different custom action types. If you do not have this knowledge, I highly advise that you obtain it prior to writing custom actions for Windows Installer packages.

Despite Windows Installer rich feature set of Standard Actions, there often comes a time when a custom action is required to accomplish the deployment goals for an application. Windows Installer originally wanted to be 100% declarative, but it came to be understood that this was not achievable. Thus the Windows Installer team created 3 primary mechanisms for developers to inject code into the installation process: Win32 DLL, ActiveScript (VBS/JS) and EXE. As the years went by, there came to be an understanding that the Script and EXE hosting models had some serious design limitations and points of failure. These CA types were then largely discouraged. The sole remaining technique of C++ Win32 exported DLL functions made perfect sense back in the mid 1990’s when MSI was created but with today’s generation of developers and platform capabilities exposed solely in .NET base class libraries, it simply no longer could meet application deployment needs in the 2000’s. At first the community struggled with this because there was still a strong desire to return back to a 100% declarative design and there was some technical problems with consuming managed code from within the Windows Installer native unmanaged engine.

Deployment Tools Foundation: A Strong Solution

First made available to the public in WiX weekly release 3.0.4116.0 (http://wix.sourceforge.net/releases/3.0.4116.0/ ), DTF provides a framework for easily and reliably writing managed code custom actions for the Windows Installer. In a nutshell, it provides a robust set of interop classes to simplify communicating with MSI and a hosting model to abstract the CLR code from the MSI process. At runtime, MSI thinks it’s calling a Win32 DLL in it’s own sandbox but in reality the CLR is being fired up out of process and communicated with through a named pipe. From your managed codes perspective, you are simply communicating with MSI through the interop classes with no need (generally) to be concerned with all of the nightmares of unmanaged code.

Simple Example:

Consider this code snippet that assigns a random number to a public property.

Notice the Microsoft.Deployment.WindowsInstaller reference, the [CustomAction] attribute and the Session class. Let's build the assembly and look at it in Dependency Walker:

There's a couple problems. The first is that there aren't any exported functions that MSI could understand, the second is the reference to MSCOREE.dll reference that would cause MSI to be stuck on a particular CLR version if it had been ran. We can fix this though by running a postbuild step with a DTF utility called MakeSfxCA.exe. This will package will parse the assembly for methods with the [CustomAction] attribute, package it and the dependencies into a near self extracting wrapper for consumption by MSI. Let's look at it again in Depends:

Notice the exported stdcall function now exists, there is a dependency on MSI and MSCOREE is no longer required (in this context).

Debugging

DTF supports two ways of debugging your managed code custom action. One is to attach a debugger to the process via a MessageBox and the later is a new environment variable called MMsiBreak ( not to be confused with MsiBreak ). I tried it and it worked the first time.

Things To Come

In DTFSDK has an expirementalnamespace that provides LINQ capabilities to Custom Actions. This promises to greatly simplfy the process of querying the MSI database and should encourage developers in creating data driven CA's. For example, in my simple example I merely assign a random number to a single property. An improved custom action would have a RandomNumbers table with n number of rows specifying properties to be assigned a random number and perhaps a Condition column for evaluating if the assignment should occur.

Summary

If you don't mind adding the .NET framework as a dependency to your install and you want to write custom actions in managed code, DTF rocks. I really don't know any other way to put it. There were some bugs that I encountered ( mostly release defects with missing files and mismatches of strong name keys ) but nothing that I couldn't easily overcome to create this sample project. The resulting custom action can then be consumed in virtually any MSI editing tool. This is clearly a best in class solution for managed code custom actions that is long overdo.

On the other hand, if you still aren't sold on .NET and also don't want to resort to C++, then my best of class reccomendation for unmanaged code custom actions continues to be InstallScript from Acresso.

DTF CA's ( and InstallScript CA's ) both get packaged inside a Win32 PE DLL with StdCall Function exports. In otherwords, they appear to Windows Installer as just another C++ CA and can thereby be consumed by any installation authoring tool that can author a DLL CA.

Ok ... it is very clear to me that InstallScript CA's and DFT CA's will appear to Windows Installer as just a plain ole CA but with InstallAware's CallDLL function ... does the MSIcode add an entry into the CA table or do they have a CA in the middle that handles calling external DLLs? Do you follow?

MakeSfxCa will package everything ( including the `man in the middle` ) into a single DLL which the MSI authoring tool needs to consume and eventually get into the CA table. Nothing special beyond that needs to be done because when the function is called (whether streamed from the binary table or invoked from an installed component ) the function will then self extract it's resources, fire it up, start the IPC channel, fire up the CLR, fire the CA host, establish IPC communication and then use reflection to call into your code making the handle / session object available. It also handles exceptions and cleans up after itself when it's done.

As I understand, the MMsiBreak variable is used in the same manner as the MsiBreak variable, i.e. adding it as an environmental variable with a value of the name of the CA.I used the MessageBox method, and when attaching (via VS2005), the name of the executable was not what I expected, so how do I know what value to use?

I guess you could always tweak the resultant MSI to wire in your CA but I suppose the whole concept of InstallAware with there `MSICode` is that you aren't supposed to have to understand anything about MSI.