InfiniTec - Henning Krauses Blog

Don't adjust your mind - it's reality that is malfunctioning

Windows Installer Xml 3.0 Extension for managed installers

The Windows Installer technology unfortunately lacks support for managed custom actions or the System.Configuration.Install.Installer class. Rob Mensching posted an article on his blog a while back why Microsoft considers custom action in general and managed custom action in particular a bad idea. While he makes some valid points (some technical and some strategic), I think managed custom installers are not a bad thing:

  • While they add a dependency on the .NET Framework at setup time, for applications using a windows service written in managed code, the Framework must be installed on the target computer at the setup time because Windows Installer will start the service during setup. And for all other managed applications you'll need the framework right after the application has been installed (to run it). Since the Framework cannot be installed using a Merge module, it must be installed before the actual setup of the application.
  • I consider myself a fairly good software developer when it comes to managed code. But my C or C++ knowledge is minimum at best. So I can either write solid managed custom actions (where I have a well-tested BCL at hand) or create spooky and unreliable custom actions in C. I prefer the former option.
  • He mentions a problem with managed custom actions using different versions of the CLR. While this may be a problem, you'll mostly write custom actions using .NET 2.0 these days. And .NET 3.0 and 3.5 both use the same CLR as 2.0.
  • What remains of his technical problems is the fact that Windows Installer on Windows 2003 will try to load the .NET Framework 1.1 into the deferred-custom action server when it tries to register assemblies into the Global Assembly Cache, which will fail if you force the .NET 2.0 runtime into the process with a managed custom action.
  • All those strategic reasons might be ok for the Windows Installer team, but I can't wait a few years until the Windows Installer team bakes all the actions I need into the core of the product. And when they do, you'll need Windows 2015 at the very least…. Not an option.

Apparently, the Visual Studio Team doesn't consider managed custom actions to be harmful - otherwise they wouldn't give you the option to run managed installers in those Visual Studio deployment projects. But these installers do lack a serious feature: The Windows Installer context. It's not that the installer context isn't propagated to the runtime (You may have wondered what the IManagedInstaller interface is meant for :-) ).

Windows Installer Xml also doesn't support managed custom actions out of the box. You have to do this yourself. One option is to decompile a Visual Studio Deployment project and see what Visual Studio does to call a managed custom action. While this will certainly work, you'll end up with a managed installer which has the same limitations as the Visual Studio deployment project: No access to the installer context. Additionally, these installer classes are always called as deferred custom actions. This means that they neither work in the immediate phase of the InstallExecuteSequence nor in the InstallUISequence.

Reinventing the wheel…

To call managed code in-process from the Windows Installer process, an intermediate unmanaged DLL must be called which in turn loads the .NET Framework into the process, spawns an AppDomain and finally runs the managed code inside this AppDomain. This is actually what the Visual Studio Deployment Project does.

The approach I'm using here is based on the article "WiX - Managed Custom Actions" by ForestWalk, which in turn is based on two other aricles: "Wrapping the Windows Installer 2.0 API" by Ian Marino and  “Hosting the CLR within a custom action” by Pablo M. Cibraro (aka Cibrax). The code in the article make it possible to call managed code in every part of the sequence. But the usage is not very intuitive to use, especially managed installer classes.

Since deferred custom actions can only access one property (CustomActionData), all information needed by be managed installer must be placed in this property. And since the CustomActionData is only an unstructured simple string property, some form of serialization is needed to put multiple properties into it.

To support all four methods of the managed installer class, you'll have to create and sequence eight custom actions: For each of the four methods (Install, Commit, Rollback, Uninstall) one action for the parameters (CustomActionData) and one action to run it.

Multiple managed installers will seriously degrade the readability of your Windows Installer XML file. That's why a took the code from the article and put it into a Windows Installer Xml Extension. I also created a small framework to simplify the development of managed installers.

Here is a simple example setup file:

    1 <?xmlversion="1.0"encoding="UTF-8"?>

    2 <Wixxmlns="http://schemas.microsoft.com/wix/2006/wi"

    3     >

    4   <ProductId="4ed3ff4f-7b33-4915-9801-a0fdd5515647"

    5     UpgradeCode="d4bacea3-a59a-4d44-b95b-1e144edfb88b"

    6     Name="Acme Sample Application"Language="1033"Version="1.0.0.0"

    7     Manufacturer="Acme Software Ltd."

    8   >

    9     <PackageInstallerVersion="300"Compressed="yes"   />  

   10     <MediaId="1"Cabinet="Sample.cab"EmbedCab="yes" />

   11     <DirectoryId="TARGETDIR"Name="SourceDir"FileSource=".\">

   12       <ComponentId="ProductComponent"Guid="865018ca-dc6f-4987-9766-cffe792cb937">

   13         <FileId="f1"Name="ManagedCustomAction.dll"Source="Include\ManagedCustomAction.dll" >

   14           <ManagedInstallerxmlns="http://schemas.infinitec.de/wix/MCAExtension">

   15             <ParameterName="TargetDir">[TARGETDIR]</Parameter>

   16             <ParameterName="AssemblyFile">Assembly is run from [#f1]</Parameter>

   17           </ManagedInstaller>

   18         </File>

   19       </Component>

   20     </Directory>

   21     <FeatureId="ProductFeature"Title="Main Feature"Level="1">

   22       <ComponentRefId="ProductComponent" />

   23     </Feature>

   24     <UIRefId="WixUI_Minimal"/>

   25     <BinaryId="ManagedCustomAction"SourceFile="Include\ManagedCustomAction.dll" />

   26     <ManagedCustomActionId="test"BinaryKey="ManagedCustomAction"Type="ManagedCustomAction.CustomAction"Execute="immediate"xmlns="http://schemas.infinitec.de/wix/MCAExtension" />

   27     <ManagedActionSequencexmlns="http://schemas.infinitec.de/wix/MCAExtension">

   28       <ManagedAction="test"After="CostFinalize"SequenceTable="InstallUISequence" />

   29     </ManagedActionSequence>

   30   </Product>

   31 </Wix>

Managed Installers

The extension makes it very easy to call managed installers or managed custom action.

Just put the tag ManagedInstaller into a File tag and the installer will be called during setup. If you need context information stored in other MSI properties, add a Parameter tag into the ManagedInstaller tag with an appropiate name and the value. From your managed installer, you can use the Parameters dictionary from the InstallContext class. Here is a sample implementation for the Install method of a System.Configuration.Install.Installer class:

    1 publicoverridevoid Install(IDictionary stateSaver)

    2 {

    3     string targetDir = Context.Parameters["TargetDir"];

    4 

    5     for (int i = 3 - 1; i >= 0; i--)

    6     {

    7         InstallerContext.Current.StartAction("ManagedCustomAction", string.Format("Install: Waiting {0} seconds...", i), "");

    8         Thread.Sleep(1000);

    9     }

   10     base.Install(stateSaver);

   11 }

In line 5, the property TargetDir is accessed. This property contains the value as specified in line 15 of the Windows Installer XML file. But far more interesting are the lines 7 and 11: These lines access the Windows Installer process and report details about what the custom action is doing. The two function wrap two flavors of the MsiProcessRecord function. The StartAction method reports the start of a major action (such as "Copying files" or "Creating registry values"). Additionally, a format string for details is specified, in this case "Waiting [1] more seconds"). The ReportDetails now just take the replacement values for the format string, in this case the number of seconds remaining).

Another important method of the InstallerContext class is the LogMessage method which writes directly to the Windows Installer log. Note that you don't have to use this method to log data. You can also use InstallContext.LogMessage or Trace.WriteLine or Console.WriteLine. The output of all those methods is captured and written to the log.

All unhandled exceptions from an Installer class are catched by the framework and cause an error message to be displayed. Unhandled exceptions in the Install, Commit and Rollback methods cause the installation to be aborted. If an exception occurs in the Uninstall method, an error dialog is displayed, but the uninstall will continue.

The four methods are sequence in the InstallExecuteSequence at the following positions:

  • Install, Commit, Rollback: Before InstallServices

  • Uninstall: Before UnpublishComponents

The installer will only be invoked if the component the file is associated with is installed.

Managed custom actions

To run a managed custom action, two things have to be done: Create a ManagedCustomAction tag under the Product tag and fill in the blanks:

  • Id: The name of the custom action
  • BinaryKey: If you want to run the custom action in the immediate sequence or in the InstallUISequence table, add the assembly to the binary table (via the Binary tag) and enter its key here.
  • FileKey: If you want to run this custom in the deferred sequence, add the assembly to the file table (via the File tag) and enter its key here.
  • Type: Name full qualified name of the type you want to run (Namespace + type name)
  • Execute: Either commit, deferred, firstSequence, immediate, oncePerProcess, rollback or secondSequence. These are the same options you have with normal Custom actions (Cutom tag)
  • Impersonate: Yes to run the custom action in the security context of the logged on user. False otherwise. The default is true. Only valid for deferred custom actions.
  • Return: asyncNoWait, asyncWait, check or ignore. These are the same options you have with normal Custom actions (Cutom tag)

Unfortunately Windows Installer XML does not allow extensions in the sequence tables, so I had to create my own: ManagedActionSequence. Add a Managed tag for each custom action you want to schedule. The Managed tag has these attributes:

  • Action: The name of the managed custom action to run.
  • After: The name of the action the managed custom action should be executed after.
  • Before: The name of the action the managed custom action should be execute before.
  • Sequence: The absolute sequence where the managed custom action should run.
  • SequenceTable: The name of the sequence table where the managed custom action should be scheduled: Either InstallUISequence, InstallExecuteSequence, AdminUISequence, AdminExecuteSequence.

The managed custom action must be a class which implements the InfiniTec.Configuration.Install.ICustomAction interface, like in this example:

    1 publicclassCustomAction: ICustomAction

    2     {

    3         publicvoid Execute()

    4         {

    5             string targetDir = InstallerContext.Current.GetProperty("TARGETDIR");

    6 

    7             InstallerContext.Current.StartAction("ManagedCustomAction", "Running custom action...", "Waiting [1] seconds...");

    8             for (int i = 3 - 1; i >= 0; i--)

    9             {

   10                 InstallerContext.Current.ReportDetails(i.ToString());

   11                 Thread.Sleep(1000);

   12             }

   13 

   14 

   15         }

   16     }

This implementation has full access to the MSI properties (see line 5, if scheduled as immediate action) and of course access to the Installer log via InstallerContext.Current.LogMessage.

Other useful classes

Since the custom actions can be executed in the immediate sequence of the install process, it has full access to all properties and tables of the installer. The rows of a view can be accessed via the View class:

    1 using (View view = InstallerContext.Current.OpenView("SELECT * FROM Binary"))

    2 using (RecordCollection records = view.Execute())

    3 {

    4     foreach (Record record in records)

    5     {

    6         string name = record[1].GetString();

    7     }

    8 }

The view returns a RecordCollection which in turn provides access to it's Record instances. Each record consists of one or more fields. Note that if you create a record with the Record.Create(int columnCount) method, the resulting record will have columnCount+1 fields - 0 to the specified value.

Modifications to the original source code

Apart from the newly added code, I made significant changes to the existing code:

To load the .NET runtime into the process I use the CLRHosting project from the article mentioned above. I have replaced all dangerous API calls (strcat, sprintf) with secure ones. But my C and C++ knowledge is VERY limited. I would appreciate it if someone with more knowledge could take a look at the code….

I have also made significant changes to the managed part of the solution. Mainly, I have encapsulated all unmanaged MSI handles in a custom SafeHandle class.

Open issues

  • Deferred custom actions with assemblies in the Binary table are not yet supported
  • Immediate custom actions with assemblies in the File table are not supported (And I don't see how this could work)
  • Managed installers do not have an immediate part
  • Managed custom actions and managed installer classes don't add ticks to the progress bar.
  • A much cleaner approach is to call the custom action in a separate process and provide access to the Windows Installer context via remoting. This approach is dicussed in more detail in the article A New Approach to Managed Custom Actions by Christopher Painter. Unfortunately he didn't release any source code and I'm lacking the necessary C and C++ skills right now.

Installation

Just copy the zip file to a directory and decompress it. In your WIX Project add a reference to the ManagedInstallerWixExtension.dll. In your setup file add the namespace http://schemas.infinitec.de/wix/MCAExtension to the list of namespace definitions.

Requirements

The extension is compiled against WIX 3.0.361 (build from December 21, 2007) using .NET 2.0.

License

The authors of the original articles haven't lost a word about the license, so I assume it's freely available. To keep this stuff freely available I publish this work under the Common Public License, the same license Windows Installer XML is published under.

Downloads

ManagedInstallerWixExtension.zip (637,678 Bytes)
Source files and binaries

Technorati:

Posted by Henning Krause on Sunday, December 30, 2007 12:00 AM, last modified on Monday, December 31, 2007 12:00 PM
Permalink | Post RSSRSS comment feed

Comments (1) -

On 5/21/2008 2:32:43 PM Christopher Painter United States wrote:

Christopher Painter

All of the work that I&amp;amp;amp;amp;amp;#39;d done is now completly useless.   See, while I was doing RPC via .NET Remoting IPC channels, I was still using CLR to host the remoting host.   This solved the communications problems but not the sticky CLR msiexec process problem.

But now DTF is released with WiX and this solves all of the problems that I&amp;amp;amp;amp;amp;#39;d ever tried to solve.  And it&amp;amp;amp;amp;amp;#39;s much more then a prototype, it&amp;amp;amp;amp;amp;#39;s a feature rich, clean, very well documented SDK that is ready to roll.   The stuff I&amp;amp;amp;amp;amp;#39;d done... just proof of concepts without much time to invest in making them better.

 +Pingbacks and trackbacks (1)