6

So I'm stuck in wix hell. Come point and laugh at me.

I'm trying to create a wix installer that does some moderately complex things AND clean up after itself properly when uninstalled. This is apparently too much to hope for, as...

I can ether choose between executing my cleanup custom actions, or to remove the install folder, but not both.

The installer has five custom actions--two to bind an SSL cert to a port and add a url ACL for that port, two to remove these, and one to run the application after install.

If I do NOT execute the two custom actions that remove the cert binding and url ACL (by removing them or even if they fail), then the install folder is removed as expected. If I DO execute the two custom bindings, the install folder is NOT removed. Because I'm in wix hell.

The custom actions are pretty straight forward. Look, here they are.

<CustomAction Id="LaunchApplication"
              BinaryKey="WixCA"
              DllEntry="WixShellExec"
              Impersonate="yes" />
<CustomAction Id="AddHttpsReservation"
              Directory="INSTALLFOLDER"
              ExeCommand="[SystemFolder]netsh http add urlacl url=https://127.0.0.1:9001/ sddl=D:(A;;GX;;;BU)"
              Execute="deferred"
              Impersonate="no"
              Return="asyncWait" />
<CustomAction Id="AddHttpsBinding"
              Directory="INSTALLFOLDER"
              ExeCommand="[SystemFolder]netsh http add sslcert ipport=0.0.0.0:9001 certhash=S0M3C3RTHA5H appid={8feef448-1a4f-408f-b6d4-8a90300517ed}"
              Execute="deferred"
              Impersonate="no"
              Return="asyncWait" />
<CustomAction Id="DeleteHttpsReservation"
              Directory="INSTALLFOLDER"
              ExeCommand="[SystemFolder]netsh http delete urlacl url=https://127.0.0.1:9001/"
              Execute="deferred"
              Impersonate="no"
              Return="asyncWait" />
<CustomAction Id="DeleteHttpsBinding"
              Directory="INSTALLFOLDER"
              ExeCommand="[SystemFolder]netsh http delete sslcert ipport=0.0.0.0:9001"
              Execute="deferred"
              Impersonate="no"
              Return="asyncWait" />

Here's my execute sequence. It might look a little strange to the experienced eye; I am definitely NOT experienced using wix (I've only built and maintained eight or so relatively complex installers, which would make me an enterprise architect if we were talking about C# applications, but because these are wix installers, I'm still considered a moron of the nth degree), but these (ALMOST) give me the cargo that I want, so that's why the bamboo planes:

<InstallExecuteSequence>
  <Custom Action="LaunchApplication"
          After="InstallFinalize">NOT REMOVE~="ALL"</Custom>
  <Custom Action="AddHttpsBinding"
          Before="InstallFinalize">Not Installed</Custom>
  <Custom Action="AddHttpsReservation"
          Before="InstallFinalize">Not Installed</Custom>
  <Custom Action="DeleteHttpsReservation"
          After="InstallInitialize">REMOVE~="ALL"</Custom>
  <Custom Action="DeleteHttpsBinding"
          After="InstallInitialize">REMOVE~="ALL"</Custom>
</InstallExecuteSequence>

(Side note, if I try to do the removes Before="InstallFinalize" or any other random place I've tried the damn things fail with an error code 1631, which I think is an elevation issue but who knows because wix hell)

Again, if I comment out the last two, the install folder gets deleted on uninstall, like I want (and like Microsoft wants, and will complain about if you try to get your application certified). If I try to pick another install execute sequence for scheduling, the custom actions fail (1631 mentioned before) but the directory gets removed. I've tried a few random ones and some logical ones as well (like RemoveFolders, wouldn't that be effing nice if it worked).

Something about successfully executing the removal actions (and something about wix hell) prevents that directory from being removed!

If I don't comment out the last two lines, everything gets uninstalled except the install folder, which throws an error in the log like to see it here it goes (slight language warning):

MSI (s) (0C:C0) [13:25:59:184]: Executing op: FileRemove(,FileName=readme.txt,,ComponentId={F3E98E05-E32A-5DC0-A551-64807B816AC2})
MSI (s) (0C:C0) [13:25:59:185]: Verifying accessibility of file: readme.txt
MSI (s) (0C:C0) [13:25:59:187]: Note: 1: 2318 2:  
MSI (s) (0C:C0) [13:25:59:187]: Note: 1: 2327 2: 32 3: C:\Program Files (x86)\DealingWithThisWixBullshitInstaller\ 
MSI (s) (0C:C0) [13:25:59:188]: Note: 1: 2205 2:  3: Error 
MSI (s) (0C:C0) [13:25:59:188]: Note: 1: 2228 2:  3: Error 4: SELECT `Message` FROM `Error` WHERE `Error` = 2911 
DEBUG: Error 2911:  Could not remove the folder C:\Program Files (x86)\DealingWithThisWixBullshitInstaller\.
MSI (s) (0C:C0) [13:25:59:190]: Note: 1: 2318 2:  
MSI (s) (0C:C0) [13:25:59:191]: Note: 1: 2327 2: 32 3: C:\Program Files (x86)\DealingWithThisWixBullshitInstaller\ 
MSI (s) (0C:C0) [13:25:59:191]: Note: 1: 2205 2:  3: Error 
MSI (s) (0C:C0) [13:25:59:191]: Note: 1: 2228 2:  3: Error 4: SELECT `Message` FROM `Error` WHERE `Error` = 2911 
The installer has encountered an unexpected error installing this package. This may indicate a problem with this package. The error code is 2911. The arguments are: C:\Program Files (x86)\DealingWithThisWixBullshitInstaller\, , 
DEBUG: Error 2911:  Could not remove the folder C:\Program Files (x86)\DealingWithThisWixBullshitInstaller\.

None of the bamboo planes I've been constructing have resulted in neither glorious bountiful cargo nor a clean uninstall. I'm at a loss. Any ideas?

For the completists out there, here's the whole wxs from my MCVE (again, slight language warning):

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
     xmlns:util="http://schemas.microsoft.com/wix/UtilExtension"
     xmlns:iis="http://schemas.microsoft.com/wix/IIsExtension">
  <Product Id="*"
           Name="DealingWithThisWixBullshitInstaller"
           Language="1033"
           Version="1.0.0.0"
           Manufacturer="Derp"
           UpgradeCode="1408102b-2ffd-41ff-ad57-6319b7fbfa58">
    <Package InstallerVersion="200"
             Compressed="yes"
             InstallScope="perMachine"
             InstallPrivileges="elevated" />

    <MajorUpgrade DowngradeErrorMessage="A newer version of this product is already installed." />
    <MediaTemplate />

    <Feature Id="ProductFeature"
             Title="DealingWithThisWixBullshitInstaller"
             Level="1">
      <ComponentGroupRef Id="ProductComponents" />
    </Feature>

    <Property Id="WixShellExecTarget"
              Value="[#DealingWithThisWixBullshitExecutable]" />

    <CustomAction Id="LaunchApplication"
                  BinaryKey="WixCA"
                  DllEntry="WixShellExec"
                  Impersonate="yes" />
    <CustomAction Id="AddHttpsReservation"
                  Directory="INSTALLFOLDER"
                  ExeCommand="[SystemFolder]netsh http add urlacl url=https://127.0.0.1:9998/ sddl=D:(A;;GX;;;BU)"
                  Execute="deferred"
                  Impersonate="no"
                  Return="asyncWait" />
    <CustomAction Id="AddHttpsBinding"
                  Directory="INSTALLFOLDER"
                  ExeCommand="[SystemFolder]netsh http add sslcert ipport=0.0.0.0:9998 certhash=E68AA1985440FDAD0BB07547BE2C9957F416709B appid={8feef448-1a4f-408f-b6d4-8a90300517ed}"
                  Execute="deferred"
                  Impersonate="no"
                  Return="asyncWait" />
    <CustomAction Id="DeleteHttpsReservation"
                  Directory="INSTALLFOLDER"
                  ExeCommand="[SystemFolder]netsh http delete urlacl url=https://127.0.0.1:9998/"
                  Execute="deferred"
                  Impersonate="no"
                  Return="asyncWait" />
    <CustomAction Id="DeleteHttpsBinding"
                  Directory="INSTALLFOLDER"
                  ExeCommand="[SystemFolder]netsh http delete sslcert ipport=0.0.0.0:9998"
                  Execute="deferred"
                  Impersonate="no"
                  Return="asyncWait" />

    <InstallExecuteSequence>
      <Custom Action="LaunchApplication"
              After="InstallFinalize">NOT REMOVE~="ALL"</Custom>
      <Custom Action="AddHttpsBinding"
              Before="InstallFinalize">Not Installed</Custom>
      <Custom Action="AddHttpsReservation"
              Before="InstallFinalize">Not Installed</Custom>
      <Custom Action="DeleteHttpsReservation"
              After="InstallInitialize">REMOVE~="ALL"</Custom>
      <Custom Action="DeleteHttpsBinding"
              After="InstallInitialize">REMOVE~="ALL"</Custom>
    </InstallExecuteSequence>
  </Product>

  <Fragment>
    <Directory Id="TARGETDIR"
               Name="SourceDir">
      <Directory Id="ProgramFilesFolder">
        <Directory Id="INSTALLFOLDER"
                   Name="DealingWithThisWixBullshitInstaller" />
      </Directory>
      <Directory Id="ProgramMenuFolder">
        <Directory Id="ApplicationProgramsFolder"
                   Name="DealingWithThisWixBullshit"/>
      </Directory>
    </Directory>
  </Fragment>

  <Fragment>
    <DirectoryRef Id="ApplicationProgramsFolder">
      <Component Id="ApplicationShortcut"
                 Guid="*">
        <Shortcut Id="ApplicationStartMenuShortcut"
                  Name="DealingWithThisWixBullshit"
                  Description="Dealing with this wix bullshit"
                  Target="[#DealingWithThisWixBullshitExecutable]"
                  WorkingDirectory="APPLICATIONROOTDIRECTORY"/>
        <RemoveFolder Id="CleanUpShortCut"
                      Directory="ApplicationProgramsFolder"
                      On="uninstall"/>
        <RegistryValue Root="HKCU"
                       Key="Software\Microsoft\DealingWithThisWixBullshit"
                       Name="installed"
                       Type="integer"
                       Value="1"
                       KeyPath="yes"/>
      </Component>
    </DirectoryRef>
  </Fragment>

  <Fragment>
    <Binary Id="CACert"
            SourceFile="muhfakecert.pfx"/>
  </Fragment>

  <Fragment>
    <ComponentGroup Id="ProductComponents"
                    Directory="INSTALLFOLDER">
      <Component Id="ProductComponent">
        <File Id="DealingWithThisWixBullshitExecutable"
              Vital="yes"
              Source="$(var.DealingWithThisWixBullshit.TargetDir)DealingWithThisWixBullshit.exe"
              Checksum="yes" >
        </File>
        <File Id="readme"
              Source="$(var.DealingWithThisWixBullshit.TargetDir)readme.txt"
              Checksum="yes" >
        </File>
        <iis:Certificate Id="CACert"
                         Name="CACert"
                         Overwrite="yes"
                         StoreLocation="localMachine"
                         StoreName="my"
                         PFXPassword="I HATE YOUR FACE AND I HOPE YOU DIE!"
                         BinaryKey="CACert"/>
      </Component>
      <ComponentRef Id="ApplicationShortcut"/>
    </ComponentGroup>
  </Fragment>
</Wix>
  • 1
    Have you tried changing your Return to "ignore" or "check" on these last two custom actions? My guess is that the netsh command finds some mapping between your cert name and the install folder (??) and holds the directory open so the installer can't delete it because it's open somewhere. The asyncWait is keeping some process going until the end of the install and then checks the return code at the end. If you use "ignore" or "check" it will run synchronously and when you get to the point of removing the install folder, nothing is holding the path open so it can be removed. – Brian Sutherland May 12 '17 at 18:06
  • @BrianSutherland you magnificent bastard. Changing them to "Check" fixed it! Submit an answer below and I'll accept. I'd also like to read any additional info you have about *"the netsh command finds some mapping between your cert name and the actual file and holds the directory open"* –  May 12 '17 at 18:12
  • 1
    Honestly it was a guess because you mentioned it doesn't work then they DO run but does work when they DONT run. And that sounded to me like trying to delete a folder when it was 'in use' or 'opened'. It reminded me a lot of trying to delete a folder where I had a command prompt using that folder I was trying to delete. – Brian Sutherland May 12 '17 at 18:20
  • Aaah, yes, but you *did* know the magic incantation I was missing... I wouldn't have ever figured out the answer was in the Return value of the action, which I had set to "asyncWait" *because I'm literally cargo-cult programming here.* –  May 12 '17 at 18:33
  • Guys, we've been though this before. https://stackoverflow.com/questions/20691482/catch-22-prevents-streamed-tcp-wcf-service-securable-by-wif-ruining-my-christma and I can't be arsed to find the meta question about it where the big whigs said to leave it be. –  Oct 03 '17 at 18:58

1 Answers1

3

Have you tried changing your Return to "ignore" or "check" on these last two custom actions? My guess is that the netsh command finds some mapping between your cert name and the install folder (??) and holds the directory open so the installer can't delete it because it's open somewhere. The asyncWait is keeping some process going until the end of the install and then checks the return code at the end. If you use "ignore" or "check" it will run synchronously and when you get to the point of removing the install folder, nothing is holding the path open so it can be removed.

Just moving comment to answer. Not entirely sure of the actual mechanism being used here but it seems through some method netsh ends up opening a handle on the directory so it could not be deleted during the uninstall.

Brian Sutherland
  • 4,698
  • 14
  • 16
  • 4
    The custom actions specify `Directory="INSTALLFOLDER"` which defines the working directory for [custom action type 34](https://msdn.microsoft.com/de-de/library/windows/desktop/aa368092(v=vs.85).aspx). So as long as the custom action is running, it will have a handle open to that working directory and thus the directory can't be deleted. Changing that to `Directory="SystemFolder"` propably would have done the trick too. – zett42 May 13 '17 at 16:31