The WiX toolset's "Remember Property" pattern.

This question comes up so often, I should have written this blog entry years ago. The root issue is that the Windows Installer does not save Property values for you. That means if the user enters values in the install UI or passes them on the command-line, those values will be not be present during repair, upgrade nor uninstall. That last one, uninstall, catches people all the time. So let's solve the problem simply then solve it completely.

The Simple Solution

As is often the case, the simple solution is not the correct solution but it definitely shows us the way. In this case, what we're going to do is squirrel away the user provided Property values in registry keys and use a RegistrySearch to read them back out for repair, upgrade and uninstall. The code for this simple solution is pretty straight forward.

   1:  <?xml version='1.0'?>
   2:  <Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
   3:  <Product Id='*' Name='Demo Remember Property' Language='1033'
   4:           Version='0.1.0.0' Manufacturer='RobMensching.com LLC'
   5:           UpgradeCode='c2e7dfa4-3faf-4c41-9bcc-eabf7657a2b9'>
   6:    <Package InstallScope='perUser' InstallerVersion='200' />
   7:   
   8:    <MajorUpgrade DowngradeErrorMessage="A newer version already installed" />
   9:   
  10:    <Property Id='REMEMBERME'>
  11:      <RegistrySearch Id='RememberProperty' Root='HKCU'
  12:                      Key='SOFTWARE\Wix\DemoRememberProperty'
  13:                      Name='Remembered' Type='raw' />
  14:    </Property>
  15:   
  16:    <Feature Id='MainFeature' Level='1'>
  17:      <Component Directory='ApplicationFolder'>
  18:        <RegistryValue Root='HKCU' Key='SOFTWARE\Wix\DemoRememberProperty'
  19:                       Name='Remembered' Value='[REMEMBERME]'
  20:                       Type='string' />
  21:        <RemoveFolder Id='CleanupApplicationFolder' On='uninstall' />
  22:      </Component>
  23:    </Feature>
  24:  </Product>
  25:   
  26:  <Fragment>
  27:    <Media Id='1' />
  28:    <Directory Id='TARGETDIR' Name='SourceDir'>
  29:      <Directory Id="LocalAppDataFolder" Name="AppData">
  30:        <Directory Id='ApplicationFolder' Name='Demo Remember Property' />
  31:      </Directory>
  32:    </Directory>
  33:  </Fragment>
  34:  </Wix>

That's a fully working installation, so I'll comment on the interesting parts of the code:

  • line 6: Note that I used a per-user install for this example because it saved me UAC prompts during testing. If your install is per-machine use HKLM instead of HKCU for the various registry roots we'll discuss next.
  • line 8: I'm using the new WiX v3.5 language simplifications with the MajorUpgrade element and the absent Component/@Id,@Guid attributes. If you're using WiX v3.0 you'll have to type more stuff.
  • line 10: This is the Property that we'll remember. In this case, I have no default value but you could add one if your scenario required it. You can have as many remembered Properties as you want, you just add more registry keys.
  • line 11: This is the registry search that will reload our Property on repair, upgrade and uninstall scenarios. On the initial install the registry key will be missing so the Property will use its default value (which in my case is nothing).
  • line 18: This is the registry key that remembers our Property. Put the registry key in a place that is logical for you. Again, remember to make your Root='HKLM' when doing a per-machine install.
  • line 21: I added the RemoveFolder element to make ICE64 happy.
  • line 26: Notice that I pulled the directory tree into a separate Fragment. I did this just to keep the rest of the code a bit more tidy. I also added the Media element to make ICE71 happy even though there are no files to install.

Cool. Now let's walk through our install, repair, uninstall scenarios and see the simple solution work. I'll show you the command I executed then the important pieces of the log files that demonstrate how it works. First install and it's log file:

msiexec /l*v i.txt /i demo.msi REMEMBERME=1
MSI (c) (A0:3C) [21:31:02:897]: Command Line: REMEMBERME=1 CURRENTDIRECTORY=...
MSI (c) (A0:3C) [21:31:02:898]: PROPERTY CHANGE: Adding REMEMBERME property. Its value is '1'.
MSI (c) (A0:3C) [21:31:02:918]: Switching to server: REMEMBERME="1" TARGETDIR=...
MSI (s) (90:F0) [21:31:02:945]: Command Line: REMEMBERME=1 TARGETDIR=...
Property(S): REMEMBERME = 1
Property(C): REMEMBERME = 1

Now, repair and it's log file (Note: that I only had to pass REINSTALLMODE="u" because we only need to repair our per-user registry keys) :

msiexec /l*v r.txt /i demo.msi REINSTALL=ALL REINSTALLMODE=u
Action start 21:35:52: AppSearch.
AppSearch: Property: REMEMBERME, Signature: RememberProperty
MSI (c) (C0:A0) [21:35:52:722]: PROPERTY CHANGE: Adding REMEMBERME property. Its value is '1'.
MSI (c) (C0:A0) [21:35:52:727]: Switching to server: REMEMBERME="1" TARGETDIR=...
MSI (s) (90:90) [21:35:52:742]: Command Line: REMEMBERME=1 TARGETDIR=...
Property(S): REMEMBERME = 1
Property(C): REMEMBERME = 1

Notice how the REMEMBERME Property was set to 1 after the AppSearch. That's where the RegistrySearch element was executed and set the Property. Uninstall works exactly the same so I'll skip that and move on to where our simple solutoin breaks down. What happens if you want to change the value during a repair? It goes something like this:

msiexec /l*v r.txt /i demo.msi REINSTALL=ALL REINSTALLMODE=u REMEMBERME=2
Action start 22:57:23: AppSearch.
AppSearch: Property: REMEMBERME, Signature: RememberProperty
MSI (c) (E4:A0) [22:57:23:378]: PROPERTY CHANGE: Modifying REMEMBERME property. Its current value is '2'. Its new value: '1'.
MSI (c) (E4:A0) [22:57:23:383]: Switching to server: REMEMBERME="1" TARGETDIR=...
MSI (s) (88:A8) [22:57:23:398]: Command Line: REMEMBERME=1 TARGETDIR=...
Property(S): REMEMBERME = 1
Property(C): REMEMBERME = 1

Do you see how the AppSearch that solved our problem creates a new one? It overwrites our attempt to set REMEMBERME to "2" with the remembered value "1". That brings us to our more complex but complete solution.

The Complete Solution

The complete solution builds on the simple solution. All we need to do is add a couple custom actions to save the Property value if it set from the command line. I had hoped to use the SetProperty element introduced in WiX v3.0 to make the solution pretty clean but that does not support the "firstSequence" bit for custom actions. So I've had to write out the custom actions completely.

   1:  <Fragment>
   2:    <CustomAction Id='SaveCmdLineValue' Property='CMDLINE_REMEMBERME'
   3:                  Value='[REMEMBERME]' Execute='firstSequence' />
   4:    <CustomAction Id='SetFromCmdLineValue' Property='REMEMBERME'
   5:                  Value='[CMDLINE_REMEMBERME]' Execute='firstSequence' />
   6:   
   7:    <InstallUISequence>
   8:      <Custom Action='SaveCmdLineValue' Before='AppSearch' />
   9:      <Custom Action='SetFromCmdLineValue' After='AppSearch'>
  10:        CMDLINE_REMEMBERME
  11:      </Custom>
  12:    </InstallUISequence>
  13:    <InstallExecuteSequence>
  14:      <Custom Action='SaveCmdLineValue' Before='AppSearch' />
  15:      <Custom Action='SetFromCmdLineValue' After='AppSearch'>
  16:        CMDLINE_REMEMBERME
  17:      </Custom>
  18:    </InstallExecuteSequence>
  19:  </Fragment>

There is nothing special here. We squirrel away the REMEMBERME value from the command line in the CMDLINE_REMEMBERME Property. Later we set the REMEMBERME Property to the CMDLINE_REMEMBERME value if one was provided. Since I put this in a Fragment the only thing remaining is to add a reference from the Product element. I added the following just above the close Product element.

  25:    <CustomActionRef Id='SaveCmdLineValue' />

Try it out yourself and follow the changes to the REMEMBERME Property in a verbose log file. In the future (WiX v4.0, perhaps), we might add something to the WiX language to make this much easier to write. In the meantime, tuck this solution in a Fragment for when you need it later.

 

9 Comments

Comment by Ran Davidovitz on Monday, May 3, 2010 12:14 AM

Rob, great work you even gave the solution to support command line :)

I spoke with my college (Alex K) and we discussed the option to give this contrib using Extensions (and which out of the three)?

Comment by Christopher Painter on Monday, May 3, 2010 2:20 AM

I've used this pattern for years...

http://community.flexerasoftware.com/showpost.php?p=370075&postcount=6

I've long asked InstallShield for this sort of syntactic sugar. It will be nice to see it in WiX.

Comment by Igor on Monday, May 3, 2010 4:03 AM

Hi Rob,

Great stuff, never thought about appsearch and providing command line for the same property.
But I have another question about storing properties for upgrade/uninstall, what is the best approach to store sectuiry related properties - like passwords (maybe names of users) which should be used to create, for example, COM+ application. Is storing plain text value of password to registry also prefered solution?

Igor

Comment by Tim on Monday, May 3, 2010 12:07 PM

Hi, Rob!

Thank you for the outstanding post! There's always something you could learn from.
Thanks again and hope to hear from you soon :)

Comment by David on Tuesday, May 25, 2010 8:16 AM

I'm using WiX to deploy a database. On install I can acquire the server and the login credentials from the user, but on uninstall I cannot.

So I could use this pattern to remember the login credentials and server that the database is being created on. But, this creates a huge security hole as I'm storing username/password info in clear text.

Is there someway to query the user for this info on uninstall or should I just require the DBMS admin to drop the database that the installer deploys?

Comment by Simon on Monday, November 22, 2010 3:29 AM

It seems even the fixed pattern falls apart if one specify a default value for the FIXME property and then try to do a repair.

Comment by Stobor on Tuesday, August 23, 2011 12:18 AM

A quick thought, based on the way I approached this problem:

[Property Id='REG_REMEMBERME']
[RegistrySearch Id='RememberProperty' Root='HKCU'
Key='SOFTWARE\Wix\DemoRememberProperty'
Name='Remembered' Type='raw' /]
[/Property]

[CustomAction Id="SetREMEMBERME" Property="REMEMBERME" Value="[REG_REMEMBERME]" /]

[InstallExecuteSequence]
[Custom Action="SetREMEMBERME" After="AppSearch" ]REG_REMEMBERME AND NOT REMEMBERME[/Custom]
[InstallExecuteSequence]

Seems a little simpler; are there any hidden issues with this approach?

Comment by Thomas Trias on Thursday, December 15, 2011 12:36 PM

I approached it the same way as Stobor, and have had no problems. I am potentially going to have to add additional properties for either approach because there is no way to distinguish between a desired empty property passed on the command line and the property being omitted from the command line. The pattern I really want is:

1) If the property were passed on the command line (including empty / null) use that

2) Otherwise, if it is persisted, use that

3) Otherwise use the default

Comment by Stephen Baker on Wednesday, August 22, 2012 2:02 PM

It seems that wix 3.5 does support firstSequence on SetProperty through sequence="first"; so the custom action shouldn't be necessary.

As for everyone asking why not just use a NOT condition, consider something like INSTALLDIR that has a default value.



Leave a comment
optional