4

I am having a problem with using a deferred CustomAction where the parameter is passed as a file id reference in the style [#file.Product.exe.config]:

<Component Id="comp.Product.exe.config" 
           Guid="{966DA007-E3F0-4BFE-94ED-63A66F82CA58}" 
           Win64="$(var.Win64)">
    <File Id="file.Product.exe.config" 
          Name="Product.exe.config" 
          Source="$(var.SourcePath)\Product.exe.config" 
          KeyPath="yes" />
</Component>

Then I have a property called ConfigurationFile which is later on passed in to the deferred custom action like so:

<Property Id="ConfigurationFile" 
          Value="[#file.Product.exe.config]" />

I previously assigned the ConfigurationFile property Value="[APPLICATIONROOTDIRECTORY]\Product.exe.config" which worked just fine, but because we need to install the configuration to a different folder (the reason why this had to be done is irrelevant to this topic), I had to change it to resolve the location of the configuration file from the installer, using the [#file.Product.exe.config] syntax.

When the custom action receives the parameter, it is still in the [#file.Product.exe.config] format (that is, unresolved), and causing loads of problems. I was confused for quite a while because based on logging it seemed it should be working. It turns out that Session.Log() resolves this file reference, which resulted in the log "lying" to me about the actual content of the argument.

I have tried different approaches to "claning" this string, including session.Format() which results in InvalidHandleException, using record (as stated below) and other ways with no luck.

using (Record formatRec = new Record(0))
{
    formatRec.FormatString = p0;
    return formatRec.GetString(0) // Or formatRec.ToString();
}

There are several custom actions provided in WiX that handles this convention. However, I have yet to find out how they do it. No luck as of yet when diving into the source code for these custom actions.

Arguments to the custom action is retrieved from Session.CustomActionData.

Is there a nice way of handling this?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
bigfoot
  • 455
  • 5
  • 12

2 Answers2

4

You need a type 51 custom action to set the property:

<CustomAction Id="SetProductConfigPath"
              Property="ConfigurationFile"
              Value="[#file.Product.exe.config]"
              Execute="deferred"/>
<InstallExecute>
    <Custom Action="SetProductConfigPath"
            Before="InstallInitialize">/>
</InstallExecute>

You may need adjust the scheduling to something other than 'InstallInitialize'.

See blog post From MSI to WiX, Part 5 - Custom actions: Introduction for a list of all CustomAction types, and blog post Property does not exist or empty when accessed from deferred custom action for a detailed explanation of deferred custom actions.

I hope this helps.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Zach Young
  • 10,137
  • 4
  • 32
  • 53
  • Sadly, it did not work. I coudln't use Execute="deferred" as it sets a property, and ended up as immediate (which still makes it Type 51). I execute the action to set the property MergeConfigFileFullPath=[ConfigurationFile], and then in the next step pass [MergeConfigFileFullPath] to set the CustomActionData of the CA I need to execute. The result is the same as when passing [ConfigurationFile] directly. The value passed to the CA is still the file reference [#file.Product.exe.config] which is in the log printed as the actual file name. – bigfoot Mar 13 '12 at 09:34
  • I was hoping I could find a way to have the MSI tell me the resolved filename of the reference, either through a method, or querying the MSI directly. The latter approach seems a bit complex and digging a bit too deep into the internal workings, and I guess that is something to avoid. – bigfoot Mar 13 '12 at 10:02
1

After a few grey hairs and quite a bit of foul language, I came up with a solution. It is based on @Zachary Youngs answer, however, some modifications were required.

Using a deferred action does not allow access to properties. Using the action as immediate type 51 action to set a property resulted in the property's [#filekey] reference remaining intact.

Using the following solution works:

<!-- Custom action to set update [ConfigurationFile] to the resolved filename -->
<CustomAction Id="SetResolvedConfigurationFile"
              BinaryKey="LocusCA"
              DllEntry='SetResolvedConfigurationFile'
              Return="check"
              Execute="immediate" />
<!-- Set CustomActionData for MergeConfiguratioFiles custom action -->
<CustomAction Id="SetMergeConfigurationFiles"
              Property="MergeConfigurationFiles"
              Execute="immediate"
              Value="[ConfigurationFile]" />

<!-- Merge settings from old configuration file to the one being installed -->
<CustomAction Id='MergeConfigurationFiles'
              BinaryKey="LocusCA"
              DllEntry='MergeConfigFiles'
              Return="check"
              Execute="commit" />

<InstallExecuteSequence>
    <Custom Action="SetResolvedConfigurationFile" Before="InstallFinalize">NOT (WIX_UPGRADE_DETECTED = "") AND NOT (ConfigurationFile = "")</Custom>
    <Custom Action="SetMergeConfigurationFiles" After="SetResolvedConfigurationFile">NOT (WIX_UPGRADE_DETECTED = "") AND NOT (ConfigurationFile = "")</Custom>
    <Custom Action="MergeConfigurationFiles" After="SetMergeConfigurationFiles">NOT (WIX_UPGRADE_DETECTED = "") AND NOT (ConfigurationFile = "")</Custom>
</InstallExecuteSequence>

The custom action SetResolvedConfigurationFile is as follows:

[CustomAction]
public static ActionResult SetResolvedConfigurationFile(Session session)
{
    // Set property ResolvedConfigurationFile
    try
    {
        session["ConfigurationFile"] = session.FormatRecord(new Record(1) { FormatString = session["ConfigurationFile"] });
    }
    catch (Exception ex)
    {
        session.Log("Unable to find configuration file for merge from property 'ConfigurationFile'): " + ex);
        return ActionResult.Failure;
    }
    return ActionResult.Success;
}

Using session.FormatRecord to resolve the current value of [ConfigurationFile], and write it back to the property, the existing setup of my custom action works.

Note: using the shortcut of changing my action MergeConfigurationFile to execute immediate does not work, as the files I need to merge do not yet exist. Using session.FormatRecord directly within MergeConfigurationFile results in a InvalidHandleException when trying to format when Execute="commit", and using Record.ToString() or Record.GetString() just returns the [#filekey] reference.

bigfoot
  • 455
  • 5
  • 12
  • Sorry I know this is an old post but I just wondered if you managed to get this working? I am also trying to merge config files but can't figure out how to access the new config file? – Mike Apr 08 '14 at 13:45
  • I know it's a long time since you posted the comment, and hope you have been able to figure it out since then. Merging worked just fine. The merging itself is "just" loading two XML documents, using attribute Name/id or key as the identifier, and copying values from the old config file. Basically iterate the original XML tree, and creating XPath expression based on the element I'm currently working on, to get the value from the configuration file being upgraded from. Remember to exclude any assembly redirects, as this might cause very annoying issues if the redirect is no longer correct :) – bigfoot Jan 20 '16 at 09:07