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

5 Responses to “Writing Data-Driven Custom Actions”


  1. 1 Bill Bartmann September 19, 2009 at 3:18 am

    Hey good stuff…keep up the good work! I read a lot of blogs on a daily basis and for the most part, people lack substance but, I just wanted to make a quick comment to say I’m glad I found your blog. Thanks,)

    A definite great read.. :)

    -Bill-Bartmann

  2. 2 fashion games December 24, 2009 at 1:00 pm

    That looks very intresting,
    We thought about moving to MSI and that gives us more info.
    thanks.

  3. 3 Sean Farrow January 19, 2011 at 6:10 am

    Hi:
    do you know whether it’s possible to reference one column from another in a data row?
    Fantastic read and helped me a lot–thanks!
    Sean

  4. 4 Kiran Hegde July 31, 2012 at 9:52 am

    Hello,

    I am reaching out to you as there is very little documentation on Lux available and i am at loss to understand the potential benefits of Lux.

    I have just started evaluating Lux in the Wix toolset, in the hope that we could implement a custom action test framework for our installers. We have huge number of installers(100+) and i would like to implement this for these in a phased manner.

    Most of our custom actions are C++ based.

    However, the documentation for Lux doesn’t make it clear as to how Lux would help improve the quality of custom actions. All that Lux does is to verify if the values of PROPERTIES are set as expected.

    Now, with just this being done, how would the custom action quality improve?

    Can someone give me specific real world examples as to where Lux could help?

    I have to evaluate this and brief the management about the pros and cons of Lux.

    Can you provide me some practical examples where Lux has helped you with your installation development?

    What are the general scenarios for which you make use of Lux?

    Any help would be very much appreciated.

    Thanks,

    Kiran Hegde


  1. 1 Writing Data-Driven Custom Actions keynote link for different customs Trackback on August 26, 2009 at 6:03 pm
Comments are currently closed.



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

%d bloggers like this: