Semi-custom actions

I was working on my current double-secret project at work and got a requirement that I knew would need a custom action. The requirement is to add entries to a configuration file without overwriting existing entries.

The configuration file (scenery.cfg, for FlightSim fans) is actually in .ini format. Naturally, I looked into using the standard IniFile table but it wasn’t sufficient for my needs. Scenery.cfg has entries that point to scenery data; each entry is numbered to control the priority of the scenery in rendering.

So, for example, given the following tail entries:

[Area.114]
Title=Propeller Objects
Local=Scenery\Props
Layer=114
Active=TRUE
Required=TRUE

[Area.115]
Title=Addon Scenery
Local=Addon Scenery
Layer=115
Active=TRUE
Required=FALSE

New entries would start out like this:

[Area.116]
Title=Test1
Local=MyScenery\Test1
Layer=116
Active=TRUE
Required=FALSE

Unfortunately, scenery.cfg is different among the various editions and languages of FlightSim, so the entry numbers aren’t static. (Also, users can add third-party scenery and we can’t overwrite those entries.) Though the IniFile table supports formatted strings, adding scenery entries would require string manipulation and math operators. While those might make for interesting MSI features, they don’t exist today.

So I started planning to write a deferred custom action and its matching rollback CA. Neither is too difficult — Win32 still supports API functions to add and remove .ini file entries. (Amusingly, the SDK doc groups them together with Registry Functions.)

Still, I’m a strong believer in not doing more work than necessary. (I prefer to think of it as efficiency rather than laziness.) MSI already handles the deferred and rollback aspects of modifying .ini files via the IniFile table and WriteIniValues and RemoveIniValues standard actions. If I could plug in dynamic data, I’d be able to write a simpler immediate CA rather than two more complicated deferred/rollback CAs.

Efficiency through transience

As you probably suspect by now, MSI has just such support. Immediate CAs can add temporary tables, rows, and columns to the active database. By adding temporary rows to standard tables, immediate CAs can determine at install time what data should be installed.

It’s important to note that temporary rows are…well…temporary. There’s not a whole lot of doc in the MSI SDK about temporary rows in general but one mention points out their temporary nature:

A custom action can be used to add rows to the Registry table during an installation, uninstallation, or repair transaction. These rows do not persist in the Registry table and the information is only available during the current transaction. The custom action must therefore be run in every installation, uninstallation, or repair transaction that requires the information in these additional rows. The custom action must come before the RemoveRegistryValues and WriteRegistryValues actions in the action sequence.

So that’s the recipe: An immediate CA runs, reads whatever data it needs from a custom table, using component states to decide what data should be written to temporary rows, and writes the rows (to the Registry or IniFile tables, for example). No deferred or rollback CAs are required, because MSI handles that.

WiX’s wcautil library makes this kind of CA easy to write. The WcaOpenExecuteView function executes a query and returns a view handle. WcaFetchRecord fetches the next record from the view. The WcaGetRecordInteger, WcaGetRecordString, and WcaGetRecordFormattedString functions get column values from the record. The best is WcaAddTempRecord which does all the work of adding a temporary row. It’s a fairly straightforward function but as no other CA in WiX uses it, here’s an example:

hr = WcaAddTempRecord(&hIniTableView, &hIniColumns,
// the table
L”IniFile”,
// the column number of the key we want “uniquified”
1,
// the number of columns we’re adding
8,
// primary key
L”AcesSceneryConfig”,
// FileName — always scenery.cfg
L”scenery.cfg”,
// DirProperty — set by AppSearch in extension .wixlib
L”ACESSCENERYCFGDIR”,
// Section — [Area.<n>]
pwzArea,
// Key
L”Title”,
// Value
wzTitle,
// Action
msidbIniFileActionAddLine,
// Component_
wzComponent);

The first two parameters are pointers to MSIHANDLEs. You initialize them to NULL and the first time you call WcaAddTempRecord, it initializes them. The first handle is to a view on the table you’re inserting into. The second is a handle to information about the columns in the table. By passing them in and out as arguments, you can let WcaAddTempRecord initialize them once then re-use the same handles. WcaAddTempRecord doesn’t know when you’re done adding records, so you’re responsible for calling MsiCloseHandle on both handles.

The fourth parameter is named uiUniquifyColumn and is the column number of a column that must have a unique value, like the primary key of a table. Yes, somebody made uniquify a verb; as far as I can tell, Rob‘s to blame. Anyway, what uiUniquifyColumn does is add a semi-random value to the value you pass in as the value for the specified column. (Remember that column numbers start at one, not zero.) That ensures the temporary rows you’re adding won’t conflict with existing rows. As the rows are temporary, there’s no harm in using non-deterministic IDs.

The other parameters are straightforward: the table name, the number of columns, and the value of each column. Note that by default the number of columns you pass in must match the number of columns in the table. If you want to pass in fewer than the actual number of columns, you need to pass in your own view of the table, via a query that selects just the columns you’re interested in.

Risks

Because the data you’re adding is processed during the install transaction, then dropped, a bug in the CA could orphan the data those rows represent during uninstallation. The easiest way to mitigate this risk is to run the immediate CA during every transaction and use the install state and action state of a component to determine whether to write the temporary rows. You can write the temporary rows only when the component state is changing (i.e., being installed, being removed, being repaired). For example:

er = ::MsiGetComponentStateW(WcaGetInstallHandle(), wzComponent, &isInstalled, &isAction);
if (WcaIsInstalling(isInstalled, isAction) || WcaIsReInstalling(isInstalled, isAction) || WcaIsUninstalling(isInstalled, isAction))
{ … }

By triggering off any change in component state, the CA supports installation, uninstallation, repair, and also is smart enough to not do anything when the component isn’t being installed. That lets you avoid adding any conditions to the CA scheduling itself.

Extra credit

Naturally, a WiX compiler extension lets users easily author data into the custom table. Also, there’s no reason you have to limit yourself to standard tables: You can just easily add temporary rows to the XmlConfig table to have the WiX XmlConfig custom action modify XML files in ways you couldn’t with just XPath and formatted properties alone.

Apple Safari setup built with WiX

Apple’s Safari browser is now available in public beta on Windows. A little spelunking shows that it uses Windows Installer packages and that they’re built with WiX. Sadly, they didn’t use WixUI.<g>

On a more serious-but-sad note, the packages have ICE validation errors (other than the typical ones), contain VBScript custom actions, and the main Safari package uses a custom action to install the Apple Software Updater package (instead of using a chainer). Already there’s a forum report of a 2738 error with the VBScript CA. And I guess the only way to report bugs is via forum posts…? It’s not clear. The “Report Bugs to Apple” command on the Help menu seems focused on rendering problems. Too bad. After all, Setup Development Is Just Development.

When VBScript and JScript custom actions are even more evil than usual

As everyone knows, script custom actions are inherently evil. A security addition to Windows Installer 4.0 in Windows Vista means that script CAs are even more likely to fail; see Heath’s blog entry on the issue and Aaron’s follow-up.

But did you know that script CAs, evil that they are, nonetheless ship in Orca, MsiVal2, and even the WiX toolset? Shocking but true. The Internal Consistency Evaluators (ICEs) are implemented as custom actions in .cub files that are MSI databases with a vastly different schema you’re used to seeing. A couple of ICEs are written in VBScript so a misregistered VBScript engine will cause those ICEs to fail. As WiX v3 runs validation by default during linking (and on-demand using the Smoke tool), failed ICEs fail your setup build.

Of 98 ICEs in the version of Darice.cub in the Windows Vista SDK, only four are written in VBScript — yet that’s all it takes to fail your build.

If you run into the problem, check out the workaround Aaron offers before you take the drastic step of disabling validation.

Feature conditions and UI

A question that’s come up on the wix-users mailing list several times lately has been about how to use feature conditions with properties set in the UI. It doesn’t work as most people expect so I thought I’d dig a little deeper than I would in a bunch of replies on the mailing list.

The basic idea is that you have some kind of optional functionality — like a Web site or Visual Studio integration — that you want the user to be able to enable or disable from the installer UI. (I’m assuming here that the functionality is in reality just a set of discrete components organized into a feature.)

The easiest way to get this working is to use MSI’s built-in SelectionTree control — the normal, boring, not-quite-easy-to-use feature tree that you see in most installers. That’s the approach we used with the WiX installer; in addition to features for plain sets of files, there are features for Visual Studio, Votive, and MSBuild integration.

The thrust of this question, though, is generally about using some other UI, like checkboxes or groups of radio buttons, to offer a better user experience during setup. But if you replace the SelectionTree control, you need to also replace how it manages feature states for you. Feature conditions seem like a fairly cheap way of accomplishing that, assuming you use public (i.e., all UPPERCASE) and secure (Property/@Secure=”yes”) properties to ensure they’re always passed to the MSI server for the installation execution sequence.

But it doesn’t work and as is usual with MSI, reading the SDK doc and examining a verbose log tell us why — almost.

Let’s start with the doc on the Condition table, which is the table where feature conditions are stored. A promising blurb:

The Level may be set based on any conditional statement, such as a test for platform, operating system, or a particular property setting.

A nice feature of the MSI SDK doc is that each table lists the actions that refer to the table. For the Condition table, it says:

This table is referred to when the CostFinalize action is executed.

Follow that link and you get another couple of interesting blurbs:

The CostFinalize action must be executed before starting any user interface sequence which allows the user to view or modify Feature table selections or directories.

and

The CostFinalize action queries the Condition table to determine which features are scheduled to be installed.

So far, everything sounds like it will work. It doesn’t, though, so clearly the docs aren’t telling the whole story. Verbose logs are our next step. Starting with MSI 3.1, they include entries when properties are added, changed, or deleted, so you can verify that the properties are being set correctly during the UI sequence. You can also verify that MSI is passing those properties to the execution sequence. Search the log for “Switching to server” to see the list of properties being passed. For example, from a complete install of the Windows Vista SDK version of Orca:

MSI (c) (6C:24) [14:54:54:015]: Switching to server: CUBDIR=”C:\Program Files (x86)\Orca\” ORCADIRECTORY=”C:\Program Files (x86)\Orca\” TARGETDIR=”V:\” MS.51D569E0_8A28_11D2_B962_006097C4DE24=”C:\WINNT\SysWOW64\” MS.51D569E2_8A28_11D2_B962_006097C4DE24=”C:\WINNT\SysWOW64\” MS.7EBEDD6A_AA66_11D2_B980_006097C4DE24=”C:\WINNT\SysWOW64\” MS.7EBEDD3E_AA66_11D2_B980_006097C4DE24=”C:\WINNT\SysWOW64\” INSTALLLEVEL=”1000″ COMPANYNAME=”MS” USERNAME=”Bob Arnson” CURRENTDIRECTORY=”X:\” CLIENTUILEVEL=”0″ CLIENTPROCESSID=”3180″ SOURCEDIR=”X:\” ACTION=”INSTALL” EXECUTEACTION=”INSTALL” ROOTDRIVE=”V:\” SECONDSEQUENCE=”1″ ADDLOCAL=OrcaHelp,Orca,EvalComServer,MergeModServer,CUBFiles,FullCUBFile,LogoCUBFile,XPLogoCUBFile,MMCUBFile

Notice the ADDLOCAL property setting. It’s a comma-delimited list of feature names that are to be installed. I didn’t pick them individually; instead, I used the “complete” button to say I wanted everything. The orca.msi package publishes a SetInstallLevel control event with a high install level to let MSI decide to install all features rather than using an AddLocal control event to manually list them.

Let’s assume that MSI uses the ADDLOCAL property and its friends the REMOVE and ADDSOURCE properties (among other friends) to communicate to the MSI server the feature choices the user made during the UI sequence. If you think about the many different ways the UI can implicitly and explicitly set which features get installed — from “typical” and “complete” buttons publishing SetInstallLevel control events and SelectionTree controls letting users pick and choose individual features — it makes sense that MSI would need a rich way of controlling feature installation and it makes sense for MSI to use its own properties mechanism rather than invent a new one. (And note that the same thing applies to uninstallation and reinstallation — those are all handled on a per-feature basis via properties.)

OK, so what does that have to do with feature conditions? (I’m getting there, really.) All the feature-selection property topics have this blurb:

The installer sets the Preselected Property to a value of “1” during the resumption of a suspended installation, or when any of the above properties are specified on the command line.

Follow the Preselected link and you get this blurb:

The Preselected property indicates that features have already been selected and that the selection dialog need not be shown.

Now it’s time to enter the murky world of speculation. There’s no doc to indicate that the feature-selection properties or the Preselected property have any affect on feature-condition evaluation in CostFinalize. But I think it’s reasonable to infer that an explicit feature selection (via ADDLOCAL, for example) would override the more implicit feature-selection options available, including the Condition table and INSTALLLEVEL property.

And, to make a long story short — too late! — Carolyn, MSI Team Dev Lead, confirmed that’s the case. Feature conditions are evaluated only if no feature-selection properties are set. And, as the UI sequence converts SetInstallLevel control events, feature-selection control events, and SelectionTree control settings into the corresponding feature-selection properties, feature conditions that include properties set during the UI sequence won’t work as expected.

Note that an installation that doesn’t run the UI sequence (e.g., using the /qb command-line switch to run a basic-UI installation) can use properties set during the execute sequence in feature conditions. But the same caveat applies: If any feature-selection properties are set, feature conditions aren’t evaluated.

OK, fine, what now?

So how do you get this working? As I said near the beginning of this much-longer-than-expected tome, “if you replace the SelectionTree control, you need to also replace how it manages feature states for you.” As SelectionTree controls end up setting the feature-selection properties, you need to do the same thing, directly or indirectly. The easiest way is to publish feature-selection control events from your feature-selection dialog.

For example, the WixUI dialog set WixUI_Mondo uses SetInstallLevel published from SetupTypeDlg:

<Control Id=”CompleteButton” Type=”PushButton” X=”40″ Y=”171″ Width=”80″ Height=”17″ ToolTip=”!(loc.SetupTypeDlgCompleteButtonTooltip)” Text=”!(loc.SetupTypeDlgCompleteButton)”>
<Publish Property=”WixUI_InstallMode” Value=”InstallComplete”>1</Publish>
<Publish Event=”SetInstallLevel” Value=”1000″>1</Publish>
</Control>

If you wanted to explicitly list features, the easiest way to do so is to use an AddLocal control event with an argument of ALL to install all features locally, then use individual Remove control events to remove the features that don’t apply. Doing so covers lets the user both install and uninstall features. For example:

<Control Id=”Next” Type=”PushButton” X=”235″ Y=”243″ Width=”57″ Height=”18″ Default=”yes” Text=”&Next >”>
<Publish Event=”AddLocal” Value=”ALL”>1</Publish>
<Publish Event=”Remove” Value=”FeatureX”>NOT FEATUREX_CHECKBOX</Publish>
<Publish Event=”Remove” Value=”FeatureY”>NOT FEATUREY_CHECKBOX</Publish>
<Publish Event=”Remove” Value=”FeatureZ”>NOT FEATUREZ_CHECKBOX</Publish>
</Control>

Hope this helps understand why things aren’t always as simple as a first glance might indicate.