I typically create a Visual Studio installer project to build install and
uninstall scripts for my applications. Although there are more powerful
solutions available for creating install scripts, I use the installer
project because it is free, simple to use, and meets my needs most of the
time. It does a decent job of installing my applications and cleaning up
whatever it installed when I uninstall my applications. The problem comes
when the application creates registry entries or creates configuration
files at run time. The installer is unaware of these items because it did
not create them. On application uninstall these registry entries and
configuration files are left behind by the uninstaller. While they don't
take up a lot of space, it is good etiquette to clean up behind your self
and leave no trace behind. I even ran into a situation where a corrupted
settings file caused an application to crash on startup. Uninstalling the
application did not clean up the settings file. So after uninstalling and
reinstalling the application it still crashed. This required removing the
corrupt file by hand. Needless to say the user was not pleased. Although
the real issue was insufficient error handling when loading the
configuration file, performing an uninstall and reinstall should have
worked.
Approach
The approach I used to clean up application settings was to use an
InstallerClass to run my cleanup code. An InstallerClass is a class you
create in the application you are installing. This class inherits from
Installer and has several methods you can override. These override-able
methods correspond to a number of events that occur during the install and
uninstall process of an application. This gives you a way to run some of
your own code during the application install and uninstall process.
The methods you can override are:
- Commit
- Install
- OnAfterInstall
- OnAfterRollback
- OnAfterUninstall
- OnBeforeInstall
- OnBeforeRollback
- OnBeforeUninstall
- OnCommitted
- OnCommitting
- Rollback
- Uninstall
Preparation
In order to use an InstallerClass, some up front work is required. Here
is how to create the InstallerClass and how to tie the pieces together.
The first step is to add an InstallerClass to the Visual Studio main
application project. Right click on the project in Solution Explorer and
select Add/New Item from the context menu. In the Search text box enter
Installer. Then select InstallerClass from the list, enter a name for your
new class, and select Add. A new class with the name you specified will be
added to your project.
Now right click on the class you just added in Solution Explorer and
select View Code. Then add the UnInstall method to the class. Note that we
call our base class UnInstall method. The base class performs the actual
uninstall. We want to be able to run our code after the uninstall is run.
We will be adding the additional code to this method shortly. For now we
need to finish setting up the basic skeleton.
public override void UnInstall(IDictionary savedState)
{
base.UnInstall(savedState);
}
Next you need create a custom action in the setup project to
call the UnInstall() method you just created. To do this, right click on
the your Visual Studio setup project in solution explorer and select
View/Custom Actions in the context menu. This will open an editing window
that will permit you to specify a custom uninstall action. Since we are
overriding the Uninstall action, right click on Uninstall and select
Custom Action from the context menu. Select Application Folder and press
OK. Then select the primary output from your application and select OK
again. Now when your application is uninstalled the installer will call
our overridden UnInstall method in the main application.
We only want our clean up code to run when the application is being
completely uninstalled and not when the application is only being
upgraded. We can add a condition to the custom uninstall action to limit
when our custom uninstall code is run. The condition we want to add in the
properties window is
REMOVE="ALL" AND NOT UPGRADINGPRODUCTCODE
Writing the Clean Up Code
Now that we have the skeleton set up, we can write the clean up code
using our overridden UnInstall method to remove any stray configuration
files. My application created the settings file under the users
AppData\Local folder. Normally you could find this folder using
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
But in this case Windows Installer is running the UnInstall() method
under the System account rather than the application user's account. So
the Environment class will return the folder path for the System account
folders rather than the folder path for the application user's account. We
need to find a way to determine which user account ran Windows Installer
so we can clean up their application data. After some research I found a
way using some Windows API calls.
First we need to define some API functions we plan to call.These
functions will allow us to retrieve the session information we need.
[DllImport("Wtsapi32.dll")]
private static extern bool WTSQuerySessionInformation(IntPtr hServer, int sessionId, WtsInfoClass wtsInfoClass, out IntPtr ppBuffer, out int pBytesReturned);
[DllImport("Wtsapi32.dll")]
private static extern void WTSFreeMemory(IntPtr pointer);
[DllImport("kernel32.dll")]
private static extern uint WTSGetActiveConsoleSessionId();
private enum WtsInfoClass
{
WTSUserName = 5,
WTSDomainName = 7,
}
private const uint INVALID_SESSION_ID = 0xFFFFFFFF;
Next we need to define some helper methods to make things a bit easier.
We need to be able to get the user name for the specified session,
retrieve the SID for that user, and then read their profile path from the
registry. These three methods accomplish that.
private string GetUsername(int sessionId, bool prependDomain = true)
{
var username = "SYSTEM";
if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WtsInfoClass.WTSUserName, out IntPtr buffer, out int strLen) && strLen > 1)
{
username = Marshal.PtrToStringAnsi(buffer);
WTSFreeMemory(buffer);
if (prependDomain)
{
if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WtsInfoClass.WTSDomainName, out buffer, out strLen) && strLen > 1)
{
username = Marshal.PtrToStringAnsi(buffer) + "\\" + username;
WTSFreeMemory(buffer);
}
}
}
return username;
}
public string GetUserSID(string userName)
{
try
{
NTAccount account = new NTAccount(userName);
SecurityIdentifier sid = (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier));
return sid.ToString();
}
catch
{
return null;
}
}
//You can either provide User name or SID
public string GetUserProfilePath(string userName, string userSID = null)
{
try
{
if (userSID == null)
{
userSID = GetUserSID(userName);
}
var keyPath = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\" + userSID;
var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(keyPath);
if (key == null)
{
//handle error
return null;
}
var profilePath = key.GetValue("ProfileImagePath") as string;
return profilePath;
}
catch
{
//handle exception
return null;
}
}
Finally, our Uninstall method now looks like this:
public override void Uninstall(IDictionary savedState)
{
base.Uninstall(savedState);
try
{
// proc is only being used so we can place a MessageBox in front of the installer dialog. Otherwise the MessageBox
// will appear behind the installer dialog and not be visible to the user. Probably best to not display one if possible.
// If you have no need to display a MessageBox you can omit the code to get the process.
//
// We make two attempts to get the process. That is because if we are debugging in Visual Studio the process will be
// the development environment rather than the windows installer
//
string windowTitle = "Your window title";
var proc = Process.GetProcessesByName("msiexec").FirstOrDefault(p => p.MainWindowTitle == windowTitle);
if (proc == null) proc = Process.GetProcessesByName("devenv").FirstOrDefault(p => p.MainWindowTitle == windowTitle);
// Retrieve the session identifier of the console session. The console session is the session that is
// currently attached to the physical console.
var activeSessionId = WTSGetActiveConsoleSessionId();
// If we got back a valid session identifier
if (activeSessionId != INVALID_SESSION_ID)
{
// get the user name associated with that session. That should be the user we are looking for
// get the profile path for that user and then build up the path to the configuration settings folder
string userName = GetUsername((int)activeSessionId, false);
string profilePath = Path.Combine(GetUserProfilePath(userName), "AppData");
// remove config files
string configFileFolder = "Local\\YourFoldername";
string localApplicationDataFolder = Path.Combine(profilePath, configFileFolder);
if (Directory.Exists(localApplicationDataFolder))
{
// TODO:delete any configuration files in this folder
}
// If local application data folder is empty then delete it as well
if (Directory.GetFiles(localApplicationDataFolder).Length == 0)
{
try
{
Directory.Delete(localApplicationDataFolder);
}
catch(Exception)
{
// TODO: is there anything we can or should do here?
}
}
}
}
catch (Exception ex)
{
MessageBox.Show("Exception in uninstall: " + ex.Message);
}
}
If you have any registry entries to delete under the user's profile, you
can use code similar to GetUserProfilePath() to find the correct path in
the registry.
This took me some time to research and debug. I hope it helps to save you
some time when developing your installation scripts. A similar technique
can be used when installing an application to add configuration files or
registry entries as well.