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:
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).
Using util:RemoveFolderEx tag from WixUtilExtension. I could't make this work.
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:
..\ProgramData\MyApp\Temp\..\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