Posts Tagged 'WiX'

Writing Data-Driven Custom Actions

Whenever Windows Installer’s built-in actions do not suffice to perform a specific task, a Custom Action needs to be written. Needless to say, Custom Actions, can be a bit tricky — not only can they be laborious to write and cumbersome to debug, they also run the risk of interfering with Windows Installer’s declarative, transactional way of performing installs.

It is not really surprising that Windows Installer therefore more or less discourages the use of Custom Actions unless it is absolutely necessary. Moreover, as a result of its declarative nature, it is understadable that Windows Installer prefers Custom Actions to be data-driven.

What this means in practice is that a Custom Action should not perform a hard-coded task — rather, it should query one or more (custom) tables containing the necessary information (in a declarative manner) about what is to be performed and should act accordingly.

Using WiX, creating custom tables turns out to be pretty easy. Let’s assume we create a Custom Action that, based on some condition, does something with a specific file. An appropriate table could look like this:


<CustomTable Id="MyCustomTable">
  <Column Id="Id" Type="string" PrimaryKey="yes"/>
  <Column Id="Path" Type="string"/>
  <Column Id="Condition" Type="string"/>

  <Row>
    <Data Column="Id">Foo</Data>
    <Data Column="Path">[INSTALLLOCATION]foo.txt</Data>
    <Data Column="Condition"><![CDATA[ &FeatureFoo=3 ]]></Data>
  </Row>
  <Row>
    <Data Column="Id">Bar</Data>
    <Data Column="Path">[INSTALLLOCATION]bar.txt</Data>
    <Data Column="Condition"><![CDATA[ &FeatureBar=3 ]]></Data>
  </Row>
</CustomTable>

To query this table, we have to open a view and fetch the records one by one:

PMSIHANDLE Database = MsiGetActiveDatabase( InstallHandle );
ASSERT( Database != NULL );

PMSIHANDLE View;
UINT Result = MsiDatabaseOpenView(
  Database,
  L"SELECT `Condition`, `Path`, FROM `MyCustomTable`",
  &View );
if ( ERROR_SUCCESS != Result )
{
  ...
}

Result = MsiViewExecute( View, NULL );
if ( ERROR_SUCCESS != Result )
{
  ...
}

for ( ;; )
{
  PMSIHANDLE Record;
  Result = MsiViewFetch( View, &Record );
  if ( Result == ERROR_NO_MORE_ITEMS  )
  {
    break;
  }
  else if ( ERROR_SUCCESS != Result )
  {
    ...
  }

  //
  // Read condition. 
  //
  // N.B. Do not format -- this is done by 
  // MsiEvaluateCondition itself.
  //

  WCHAR Condition[ 256 ];
  DWORD Length = _countof( Condition );
  Result = MsiRecordGetString(
    Record,
    1,
    Condition,
    &Length );
  if ( ERROR_SUCCESS != Result )
  {
    ...
  }

  if ( MSICONDITION_TRUE != MsiEvaluateCondition(
    InstallHandle,
    Condition ) )
  {
    //
    // This record can be skipped.
    //
    continue;
  }

  //
  // Read remaing fields.
  //

  WCHAR Path[ MAX_PATH ];
  Length = _countof( VszPath );
  Result = GetFormattedRecord(
    InstallHandle,
    Record,
    2,
    Path,
    &Length );
  if ( ERROR_SUCCESS != Result )
  {
    ...
  }

  
  ...
}

With GetFormattedRecord being the following utility routine:


static UINT GetFormattedRecord(
  __in MSIHANDLE InstallHandle,
  __in MSIHANDLE Record,
  __in UINT Field,
  __out PWSTR Value,
  __inout PDWORD Length
  )
{
  DWORD RecLength = *Length;
  UINT Result = MsiRecordGetString(
    Record,
    Field,
    Value,
    &RecLength );
  if ( ERROR_SUCCESS != Result )
  {
    *Length = RecLength;
    return Result;
  }

  PMSIHANDLE FormattingRecord = MsiCreateRecord( 1 );
  
  Result = MsiRecordSetString( FormattingRecord, 0, Value );
  if ( ERROR_SUCCESS != Result )
  {
    return Result;
  }

  return MsiFormatRecord(
    InstallHandle,
    FormattingRecord,
    Value,
    Length );
}

Some things are worth noting:

  • I use PMSIHANDLE, which, as you probably already know, is not a typedef for MSIHANDLE* but rather a smart-pointer like class that automatically closes the handle when it goes out of scope.
  • The use of backticks in the query.
  • It must have been a Visual Basic programmer implementing MsiRecordGetString: Field Indexes start with 1, not 0. To make matters worse, reading from index 0 does not fail but returns arbitrary data. Finally, to confuse people further, indexes are 0-based for MsiRecordSetString.
  • If the field contains formatted data, you have to MsiFormatRecord it yourself. For conditions, however, MsiEvaluateCondition handles that for you.

So far, so good. There is, however, one thing to notice: To access the installer database, the custom action must be a nondeferred action:

You cannot access the current installer session or all property data from a deferred execution custom action

The problem with nondeferred actions, however, is that they execute in user context — in contrast to deferred actions, which execute in system context. On pre-Vista platforms, a per-machine installer package can be expected to always be launched by an administrator (otherwise it will fail anyway) — in this case, the differences between user and system context may not be important — both, for example, have r/w access to files in %ProgramFiles%. On Vista and later OS, however, it is common to have a regular user launch an installation which causes an elevation prompt once it reaches the main install phase. In this case, the user context is significantly less privileged than system context.

For a hypothetical custom action that is intended to edit a file installed to %ProgramFiles%, this means that (disregarding rollback considerations and assuming proper scheduling) performing this action from within the nondeferred custom action will work fine on pre-Vista OS. When run on Vista, though, it is likely to fail due to lack of write access to %ProgramFiles%. In practice, this means that all system-changing tasks usually have to be performed by a deferred action.

To sum up: To be data-driven, you have to use nondeferred actions. To be able to perform any serious, system state-changing tasks, however, you have to use deferred actions.

Great.

As it turns out, however, there is a way to escape this catch-22, and it is carefully buried in the Windows Installer documentation:

[…] Actions that update the system, such as the InstallFiles and WriteRegistryValues actions, cannot be run by calling MsiDoAction. The exception to this rule is if MsiDoAction is called from a custom action that is scheduled in the InstallExecuteSequence table between the InstallInitialize and InstallFinalize actions. […]

[From the Remarks section of MsiDoAction]

In fact, the way I came across this solution was by looking at the source code of the WiX XmlFile action, which I knew manages to both be data-driven (uses a custom table) and alter system state (edits XML files). The way it does this, and the point where the above remark comes into play, is as follows: In the nondeferred action, you do not perform any actions changing system state. Rather, you collect the information from the installer tables and stuff it (yuck) into the CustomActionData property. Then, leveraging MsiDoAction and passing said CustomActionData, you schedule another custom action — this time a deferred one — which parses the CustomActioData (yuck) and, based on this data, finally performs the actual modifications — in system context.

It really could not be easier and more intuitive, right?

Advertisements

On Setup Bootstrap Loaders

Almost two years ago, I wrote about how to create multi-language MSI packages. Although using transforms to internationalize an MSI package is a viable solution, one drawback of this approach is that it may require a bootstrap loader.

While it is easy to say that a bootstrap loader is required and many high-profile setups do indeed use bootstrap loaders, bootstrap loaders do have their issues. They not only add complexity to the setup package, there actually are several reasons why a bootstrap loader-free setup may be preferrable.

Bootstrap loaders

The idea of a bootstrap loader is to have an EXE file that does some prerequisite work and then launches the actual installer, which is usually implemented as an MSI package. While the MSI package may be a separate file, a download-friendly setup usually requires the bootstrap loader to embed the MSI package inside the EXE file and extract it when started.

While the average end user probably will not care much whether he double-clicks on an .exe or an .msi file, a standalone MSI-package is a better Windows citizen for at least the following two reasons:

  • Active Directory has native support for deploying MSI packages. It is, of course, possible to deploy EXE-based setups through AD, but these tend to require more work or more elaborate software solutions like SMS.
  • msiexec supports a plethora of switches that allow customizing and automating setups. These switches usually cannot be used when a bootstrap loader is present. To be equally admin-friendly, a bootstrap loader should therefore support appropriate command line switches for things like performing quiet installations, pre-selecting features, and suppressing reboots. Needless to say, impementing these features increases complexity and implementation effort.

That said, there are good reasons to avoid using a bootstrap loader. If, for example, all the bootstrap loader has to do is determine the user’s locale to choose an appropriate MSI transform, it may well be worth considering whether it is easier to just offer 5 language-specific MSI packages for download. And in case of CD-ROM based distribution, a simple HTML Application (HTA) could serve as a bootstrap loader-surrogate.

Still, there are situations when a a true boostrap loader is required. A classic example for this is MDAC: With Windows XP, MDAC has become part of Windows, but if you still support older Windows releases, you do not have much choice but to redistribute MDAC. In all those years, however, Microsoft has not managed to provide a proper merge module for MDAC. There were several half-hearted attempts both by Microsoft and others to wrap MDAC by a MSM, and in fact, I tested at least five of them, but not a single one was robust enough to be useable in practice. Given this situation, the only sane choice was (and still is) to put the duty of installing MDAC on the bootstrap loader.

Another common reason for using bootstrap loaders used to be that the machine might not have the correct Windows Installer version installed — but unless you are crazy enough to require Windows Installer 4.0, this should not be of much concern today.

Creating a bootstrap loader

Notwithstanding these pros and cons, the question remains how to create a bootstrap loader. Several of the commercial setup authoring tools have built-in support for that and their use might be the most straighforward approach. However, after having had a horrid experience with Wise for Windows Installer, I have not used any of these Windows Installer-based authoring tools and therefore cannot say much about them.

With the upcoming 3.0 release, WiX also includes a tool for creating bootstrap loaders — although it does not quite seem ready for prime time yet.

Creating a custom bootstrap loader from scratch is certainly the most flexible approach and doing so should not actually be too hard. However, implementing a full fledged bootstrap loader is likely to require more effort than most teams are willing to put into a setup.

There is, however, another approach that may seem somewhat unorthodox at first but has served me well for a past project: Using one of the script-based setup solutions to roll your own bootstrap loader. The idea of tools such as Wise Installation System (which, btw., has absolutely nothing to do with Wise for Windows Installer) is that the entire setup is authored as a script. Now, contrary to the original intent of these authoring tools, you can leave all the wizards and UIs aside, start off with an empty script and leverage the rather powerful building blocks and constructs these scripting languages tend to offer to create a highly customized (UI-less) bootstrap loader. Taking WIS as an example, extracting an embedded file is a matter of one line of code. Similarly, invoking GetLocaleInfo to query the system’s locale is a snap. And for even more powerful bootstrap loaders, it is possible to extract a helper DLL and call its routines from within Wise Script.

As I said, this approach has served me well for a particular project that required a pretty complex bootstrap loader. And while I cannot recommend this approach unconditionally (especially because of the quirkiness of WIS and the fact that WIS is actually an obsolete technology), it is an approach that, when a bootstrap loader is absolutely necessary, may be worth considering.


Categories




About me

Johannes Passing, M.Sc., living in Berlin, Germany.

Besides his consulting work, Johannes mainly focusses on Win32, COM, and NT kernel mode development, along with Java and .Net. He also is the author of cfix, a C/C++ unit testing framework for Win32 and NT kernel mode, Visual Assert, a Visual Studio Unit Testing-AddIn, and NTrace, a dynamic function boundary tracing toolkit for Windows NT/x86 kernel/user mode code.

Contact Johannes: jpassing (at) acm org

Johannes' GPG fingerprint is BBB1 1769 B82D CD07 D90A 57E8 9FE1 D441 F7A0 1BB1.

LinkedIn Profile
Xing Profile
Github Profile
Advertisements