5
votes

Before I get started, let me note, that I read almost all related questions here, on SO, and found no solution. So the question aims the same problem as many before did:

How to properly delete temp folders and misc files that are being created while an application works using WiX facilities?

So far I came up with the following ways to do this:

  1. Using CustomAction (written, for example, in C#) that will simply delete all files and folders (this works great, but this workaround seems to me not valid as there is no support for Rollback from MSI side).

  2. Using util:RemoveFolderEx tag from WixUtilExtension. I could't make this work.

  3. Using CustomAction (written, again, in C#) that will populate MSI DB's tables (Directory and RemoveFile) just before UnInstall happens. This will force MSI to properly and in its own way uninstall all enumerated files and folders (in theory it should do this) via RemoveFiles default action.

I have focused on the third way and asking you a bit of a help here.

Few notes about application layout MSI that I built works great with ProgramFiles, Shortcuts, Registry and all that stuff. But during installation I put some configuration files into C:\ProgramData\MyApp\ folder (they are being removed too w/o any issues).

But while the application works it produces additional files and dirs in C:\ProgramData\MyApp\, that is used to update the app once new release is available. Let's assume the user closes the application in the middle of the update process and wants to uninstall the application. Here is what we have at the moment in the C:\ProgramData\MyApp folder:

C:\ProgramData\MyApp\
C:\ProgramData\MyApp\Temp\
C:\ProgramData\MyApp\Temp\tool.exe
C:\ProgramData\MyApp\Temp\somelib.dll
C:\ProgramData\MyApp\Temp\<UniqueFolderNameBasedOnGUID>\someliba.dll
C:\ProgramData\MyApp\Temp\<UniqueFolderNameBasedOnGUID<\somelibb.dll

At the end of the uninstalation procedure I want to see no C:\ProgramData\MyApp\ folder. And I can see this if no temp dirs/files are being created.

Please note, I do not know the name of folders and files being put in C:\ProgramData\MyApp\Temp\ folder as the final folder name is generated automatically using GUID.

Let me focus on the most important parts of the project and show you what I have done so far in order to accomplish the task (please, remember, I picked the 3rd way: via CustomAction):

Main MyApp.wxs file

<Product Id=...>
  ...
  <!-- Defines a DLL contains the RemoveUpdatesAction function -->
  <Binary Id="RemoveUpdatesAction.CA.dll" src="RemoveUpdatesAction.CA.dll" />
  <CustomAction Id="RemoveUpdatesAction" 
                Return="check" 
                Execute="immediate" 
                BinaryKey="RemoveUpdatesAction.CA.dll" 
                DllEntry="RemoveUpdatesAction" />
  ...
  <InstallExecuteSequence>
    <!-- Perform custom CleanUp only on 'UnInstall' action - see condition -->
    <Custom Action='RemoveUpdatesAction' Before='RemoveFiles'>
        (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")    
    </Custom>
  </InstallExecuteSequence>
  ...
</Product>

And later in the file, I define the Folder Layout for ProgramData directory:

<Fragment>
  <Directory Id="TARGETDIR" Name="SourceDir">
    ....
    <!-- ...\Program Files\MyApp\ -->
    <Directory Id="ProgramFilesFolder">
      <Directory Id="APPINSTALLFOLDER" Name="MyApp" />
    </Directory>

    ....

    <!-- This is it! ...\ProgramData\MyApp\ -->
    <Directory Id="CommonAppDataFolder">
      <Directory Id="SETTINGSINSTALLFOLDER" Name="MyApp" />
    </Directory>
    ....
  </Directory>
</Fragment>

... a lot of different stuff here ...

<Fragment>
  <Component Id="AddConfigurationFilesFolder" ...>
  ...
  <!-- This component just serves as a bound point - see below -->
  ...
</Fragment>

So far so good.

Now, RemoveUpdatesAction.cs file that contains a custom action:

public class CustomActions
{
    [CustomAction]
    public static ActionResult RemoveUpdatesAction(Session session)
    {
        try
        {
            // Provide unique IDs
            int indexFile = 1;
            int indexDir = 1;

            // Begin work
            session.Log("Begin RemoveUpdatesAction");

            // Bind to the component that for sure will be uninstalled during UnInstall action
            // You can see this component mentioned above
            const string componentId = "AddConfigurationFilesFolder";

            // Get '..\{ProgramData}\MyApp' folder
            // This property (SETTINGSINSTALLFOLDER) is mentioned too
            string appDataFolder = session["SETTINGSINSTALLFOLDER"];

            // Populate RemoveFile table in MSI database with all files 
            // created in '..\{ProgramData}\MyApp\*.*' folder - pls see notes at the beginning
            if (!Directory.Exists(appDataFolder))
            {
              session.Log("End RemoveUpdatesAction");
              return ActionResult.Success;
            }

            foreach (var directory in Directory.GetDirectories(appDataFolder, "*", SearchOption.AllDirectories))
            {
                session.Log("Processing Subdirectory {0}", directory);
                foreach (var file in Directory.EnumerateFiles(directory))
                {
                    session.Log("Processing file {0}", file);

                    string keyFile = string.Format("CLEANFILE_{0}", indexFile);

                    // Set values for columns in RemoveFile table:
                    // {1}: FileKey => just unique ID for the row
                    // {2}: Component_ => reference to a component existed in Component table
                    //      In our case it is already mentioned 'AddConfigurationFilesFolder' 
                    // {3}: FileName => localizable name of the file to be removed (with ext.)
                    // {4}: DirProperty => reference to a full dir path
                    // {5}: InstallMode => 3 means remove on Install/Remove stage
                    var fieldsForFiles = new object[] { keyFile, componentId, Path.GetFileName(file), directory, 3 };

                    // The following files will be processed:
                    // 1. '..\ProgramData\MyApp\Temp\tool.exe'
                    // 2. '..\ProgramData\MyApp\Temp\somelib.dll'
                    // 3. '..\ProgramData\MyApp\Temp\<UniqueFolderNameBasedOnGUID>\someliba.dll'
                    // 4. '..\ProgramData\MyApp\Temp\<UniqueFolderNameBasedOnGUID>\somelibb.dll'
                    InsertRecord(session, "RemoveFile", fieldsForFiles);

                    indexFile++;
                }

                string keyDir = string.Format("CLEANDIR_{0}", indexDir);

                // Empty quotes mean we we want to delete the folder itself
                var fieldsForDir = new object[] { keyDir, componentId, "", directory, 3 };

                // The following paths will be processed:
                // 1. '..\ProgramData\MyApp\Temp\'
                // 2. '..\ProgramData\MyApp\Temp\<UniqueFolderNameBasedOnGUID>\'
                InsertRecord(session, "RemoveFile", fieldsForDir);

                indexDir++;
            }

            session.Log("End RemoveUpdatesAction");
            return ActionResult.Success;
        }
        catch (Exception exception)
        {
            session.Log("RemoveUpdatesAction EXCEPTION:" + exception.Message);
            return ActionResult.Failure;
        }
    }

    // Took completely from another SO question, but is accoring to MSDN docs
    private static void InsertRecord(Session session, string tableName, Object[] objects)
    {
        Database db = session.Database;
        string sqlInsertSring = db.Tables[tableName].SqlInsertString + " TEMPORARY";
        View view = db.OpenView(sqlInsertSring);
        view.Execute(new Record(objects));
        view.Close();
    }
}

Note: Condition for uninstall ((NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")) I took from here.

This is pretty much all I done and after Install/Uninstall cycle I see in msi log file and in RemoveFile table (via enumerating the records) that all this entries are indeed inserted. But the files and folders in the ..\ProgramData\MyApp\Temp\... remains no matter what I do.

Can anyone clarify what I'm doing wrong?

I believe the problem might be in how I defined DirProperty in the Custom Action class. I put a directory path there, but I know (used Orca) that in Directory table there is no record with the temp folder I discovered via Directory.GetDirectories.

So RemoveFile table is being populated with records that has invalid reference to Directory table (is that right?). I tried to add those discovered folders in Directory table manually but failed - each time I try to get reference to Directory table I got an exception. What is the right way to populate Directory table in MSI? Does it make sense to populate Directory table at all?

For example, if I need to put there the following paths:

  1. ..\ProgramData\MyApp\Temp\
  2. ..\ProgramData\MyApp\Temp\<UniqueFolderNameBasedOnGUID>\

How can I do this?

Anyway, please, suggest anything - any advice, tips or comments will be much appreciated!

Thanks a lot!

MSI uninstall log:

MSI (s) (B4:28) [05:28:39:427]: Doing action: RemoveUpdatesAction
....
MSI (s) (B4:4C) [05:28:39:450]: Invoking remote custom action. DLL: C:\WINDOWS\Installer\MSI7643.tmp,    Entrypoint:     RemoveUpdatesAction
....  
Action start 5:28:39: RemoveUpdatesAction.  
SFXCA: Extracting custom action to temporary directory: C:\WINDOWS\Installer\MSI7643.tmp-\  
SFXCA: Binding to CLR version v4.0.30319  
Calling custom action RemoveUpdatesAction!RemoveUpdatesAction.CustomActions.RemoveUpdatesAction  
Begin RemoveUpdatesAction  
Processing Subdirectory C:\ProgramData\MyApp\Temp  
Processing file C:\ProgramData\MyApp\Temp\somelib.dll  
Processing file C:\ProgramData\MyApp\Temp\tool.exe  
Processing Subdirectory C:\ProgramData\MyApp\Temp\48574917-4351-4d4c-a36c-381f3ceb2e56  
Processing file C:\ProgramData\MyApp\Temp\48574917-4351-4d4c-a36c-381f3ceb2e56\someliba.dll  
Processing file C:\ProgramData\MyApp\Temp\48574917-4351-4d4c-a36c-381f3ceb2e56\somelibb.dll  
End RemoveUpdatesAction  
MSI (s) (B4:28) [05:28:39:602]: Doing action: RemoveFiles  
MSI (s) (B4:28) [05:28:39:602]: Note: 1: 2205 2:  3: ActionText   
Action ended 5:28:39: RemoveUpdatesAction. Return value 1.  
Action start 5:28:39: RemoveFiles.  
MSI (s) (B4:28) [05:28:39:607]: Note: 1: 2727 2: C:\ProgramData\MyApp\Temp   
MSI (s) (B4:28) [05:28:39:607]: Note: 1: 2727 2: C:\ProgramData\MyApp\Temp\48574917-4351-4d4c-a36c-381f3ceb2e56   
MSI (s) (B4:28) [05:28:39:607]: Counted 2 foreign folders to be removed.  
MSI (s) (B4:28) [05:28:39:607]: Removing foreign folder: C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Compass Mobile\  
MSI (s) (B4:28) [05:28:39:607]: Removing foreign folder: C:\ProgramData\MyApp\  
MSI (s) (B4:28) [05:28:39:607]: Doing action: RemoveFolders  
MSI (s) (B4:28) [05:28:39:607]: Note: 1: 2205 2:  3: ActionText   
Action ended 5:28:39: RemoveFiles. Return value 1.  
Action start 5:28:39: RemoveFolders.  

As you can see I got 2727 error code. That means for the following folders there are no records in Directory table:

C:\ProgramData\MyApp\Temp
C:\ProgramData\MyApp\Temp\48574917-4351-4d4c-a36c-381f3ceb2e56

So, maybe my suggestion is correct?

References for mentioned tags, MSI tables, etc.:

Creating WiX Custom Actions in C# and Passing Parameters
MSI DB's RemoveFile table description
MSI DB's Directory table description
RemoveFiles Action
WiX CustomAction Element

1

1 Answers

0
votes

As far as I can tell, you're not populating the RemoveFile table correctly. It would help if you logged that exact SQL insert string to see what's really there. I think you're getting error 2727 because the 4th thing in your insertion is supposed to be a directory property that refers to the directory table in the MSI file. It's not literally a directory name - it's supposed to be a key into the directory table of the MSI file - and as far as I can tell from your code it is an actual directory, not a value from the Directory table.