Philippe Truche’s Blog

4 March 2007

Installer code for deploying a Windows Forms client

Filed under: .NET — Philippe Truche @ 8:53

On my previous post, I discussed the requirement for an installer for an application created using the Smart Client Software Factory. I also discussed the peculiarities of the dynamic web references in library projects and how they are misleading since the entries in the app.config of the library projects need to be copied into the app.config of the Windows executable projects.

Some people have commented to me this is additional complexity brought on by working with Windows forms. I’d like to highlight this behavior
is a Visual Studio 2005 behavior. The same issue exists with ASP.NET applications. If you create a web reference in a library project instead of the web project, you will also need to copy the entries from the app.config of the library project into the web.config file.

This being said, let’s get into the code of the installer.

First, let’s use a helper class to deal with the various environments:

internal static class EnvironmentCode

{

public static string Development = “DEV”;

public static string UserAcceptanceTesting = “UAT”;

public static string SystemAcceptanceTesting = “SAT”;

public static string Production = “PROD”;

}

Let’s also create an application configuration class to handle configuration items related to the application. Notice the constructor needs to be passed an environment code and the assembly path. The assembly path is obtained from Context.Parameters[“assemblypath”] of the installer class that inherits from System.Configuration.Install.Installer. This will contain the path and file name of the installer Dll, not the application being
installed. That’s why the constructor takes this parameter and derives the target installation path of the application being installed. I removed the code comments for brevity here, but code comments are included in the code download.

internal class ApplicationConfiguration

{

private string _installationDirectory = String.Empty;

private string _exeConfigPath = String.Empty;

private string _sourceConfigPath = String.Empty;

public ApplicationConfiguration(string environmentCode, string
assemblyPath)

{

if (environmentCode == null)
throw new ArgumentNullException(“environmentCode”);

if (assemblyPath == null)
throw new ArgumentNullException(“assemblyPath”);

// (1) Set the installation directory

_installationDirectory = assemblyPath.Substring(0,
assemblyPath.LastIndexOf(@”\”));

// (2) Set the path to the executable file that
is being installed on the target

// computer.
We are expecting to find only one file that finishes in
“.exe”.

string[] applicationExecutablePath = Directory.GetFiles(_installationDirectory, “*.exe”);

if (applicationExecutablePath.Length != 1)

throw new InstallException(“Application
executable not found in installation directory.”
);

// Set the name of the config file for the exe
(i.e. the application)

_exeConfigPath = applicationExecutablePath[0].ToString() + “.config”;

// (3) Set the path to the source configuration
file. Important assumption:

// The file must follow the naming convention
[EnvironmentCode].*.app.config

// and there must be only one file with the
environment code prefix.

if (environmentCode.ToUpperInvariant() != EnvironmentCode.Development)

{

string[] configFiles = Directory.GetFiles(_installationDirectory,
environmentCode.ToUpperInvariant() + “*.app.config”);

if (configFiles.Length == 1)

{

_sourceConfigPath = configFiles[0];

}

else

{

throw new
InstallException(environmentCode + “.*.app.config not found in “

+ _installationDirectory);

}

}

}

public string InstallationDirectory

{

get { return _installationDirectory; }

}

public string ExeConfigPath

{

get { return _exeConfigPath; }

}

public string SourceConfigPath

{

get { return _sourceConfigPath; }

}

}

Finally, the ApplicationInstaller class derives from the System.Configuration.Install.Installer class and overrides the Install method. Here is a diagram of the class:

ApplicationInstaller Class

The install method is pretty lean and calls discrete private methods like ProcessCustomActionData(). All of these methods are appropriate behaviors of the ApplicationInstaller class. The end result follows:

[RunInstaller(true)]

public partial class ApplicationInstaller :
System.Configuration.Install.Installer

{

private ApplicationConfiguration _applicationConfiguration;

private List<string> _configSectionsToEncrypt = new List<string>();

private String _encryptionProviderName = String.Empty;

private Boolean _encryptApplicationSettings = true;

public ApplicationInstaller()

{

InitializeComponent();

}

public override void Install(System.Collections.IDictionary stateSaver)

{

base.Install(stateSaver);

#if DEBUG

System.Windows.Forms.MessageBox.Show(“You may now attach the debugger to the managed msiexec instance. “ + “Click the OK button after you have
attached the debugger.”
, “Installer Message”, System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Information);

#endif

ProcessCustomActionData();

CopyEnvironmentConfigToExeConfig();

EncryptExeConfigFile();

SecureEnvironmentConfigFiles();

return;

}

private void ProcessCustomActionData()

{

// Translate the environment code

string environmentCode = this.Context.Parameters[“ENVT”].ToUpperInvariant();

switch (environmentCode)

{

case “DEV”:

environmentCode = EnvironmentCode.Development;

break;

case “UAT”:

environmentCode = EnvironmentCode.UserAcceptanceTesting;

break;

case “SAT”:

environmentCode = EnvironmentCode.SystemAcceptanceTesting;

break;

case “PROD”:

environmentCode = EnvironmentCode.Production;

break;

default:

this.Context.LogMessage(“Unrecognized environment variable. The value passed was “ + environmentCode + “.”);

this.Context.LogMessage(“Defaulting to an installation for the development environment.”);

environmentCode = EnvironmentCode.Development;

break;

}

_applicationConfiguration = new ApplicationConfiguration(environmentCode,
this.Context.Parameters[“assemblypath”]);

// get Configuration section names from custom action parameter

// section names must be seperated by commas

String[] separator = { “,” };

String[] sectionNames = this.Context.Parameters[“sectionNames”].Split(separator, StringSplitOptions.None);

for (int i = 0; i < sectionNames.Length; i++)

{

_configSectionsToEncrypt.Add(sectionNames[i]);

}

//get Protected Configuration Provider name from custom action parameter

_encryptionProviderName = this.Context.Parameters[“provName”];

//get applicationSettings flag (decides whether or not the
//entire applicationSettings
section group will be encrypted.

// If not provided, defaults to true.

String encryptApplicationSettings = this.Context.Parameter[“encryptApplicationSettings”];

if (encryptApplicationSettings == Boolean.FalseString)
_encryptApplicationSettings = false;

return;

}

private void CopyEnvironmentConfigToExeConfig()

{

if (_applicationConfiguration == null) throw new InstallException(“_applicationConfiguration cannot be null.”);

if (!String.IsNullOrEmpty(_applicationConfiguration.SourceConfigPath))

{

File.Copy(_applicationConfiguration.SourceConfigPath,
_applicationConfiguration.ExeConfigPath, true);

this.Context.LogMessage(“Succesfully copied “ +
_applicationConfiguration.SourceConfigPath +

” to “ +
_applicationConfiguration.ExeConfigPath);

}

return;

}

private void EncryptExeConfigFile()

{

if (_applicationConfiguration == null) throw new InstallException(“_applicationConfiguration cannot be null.”);

if (String.IsNullOrEmpty(_encryptionProviderName)) throw new InstallException(“CustomActionData must contain a provName.”);

// Map to application exe.config file.

ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();

fileMap.ExeConfigFilename = _applicationConfiguration.ExeConfigPath;

Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap,

ConfigurationUserLevel.None);

if (_encryptApplicationSettings)

// Encrypt all sections in the applicationSettings section
group

{

ApplicationSettingsGroup
applicationSettings =
(ApplicationSettingsGroup)configuration.GetSectionGroup(“applicationSettings”);

foreach (ConfigurationSection applicationSettingsSection in
applicationSettings.Sections)

{

if (applicationSettingsSection != null)

{

if (!applicationSettingsSection.SectionInformation.IsProtected)

{

applicationSettingsSection.SectionInformation.ProtectSection(_encryptionProviderName);

applicationSettingsSection.SectionInformation.ForceSave
= true;

configuration.Save(ConfigurationSaveMode.Full);

}

}

}

}

foreach (string sectionName in _configSectionsToEncrypt)

{

ConfigurationSection configurationSection;

try

{

configurationSection = configuration.GetSection(sectionName);

}

catch (Exception ex)

{

throw new InstallException(“The setup program must copy custom section handlers to the system directory.”, ex);

}

if (configurationSection != null)

{

if (!configurationSection.SectionInformation.IsProtected)

{

configurationSection.SectionInformation.ProtectSection(_encryptionProviderName);

configurationSection.SectionInformation.ForceSave
= true;

configuration.Save(ConfigurationSaveMode.Full);

}

}

}

return;

}

private void SecureEnvironmentConfigFiles()

{

if (_applicationConfiguration == null) throw new InstallException(“_applicationConfiguration cannot be null.”);

String[] environmentConfigFiles = Directory.GetFiles(_applicationConfiguration.InstallationDirectory, “*.app.config”);

for (int i = 0; i < environmentConfigFiles.Length; i++)

{

if (File.Exists(environmentConfigFiles[i]))

{

using (FileStream fs = new FileStream(environmentConfigFiles[i],
FileMode.Create, FileAccess.Write, FileShare.None))

{

this.Context.LogMessage(“Blanked out “ +
environmentConfigFiles[i]);

}

File.SetAttributes(environmentConfigFiles[i],
FileAttributes.Hidden);

}

}

return;

}

}

Key features to look for in the code:

  • The use of #if DEBUG means that when running the installation program with a DEBUG build, a popup window gices you a change to attach to the msiexec process from Visual Studio. There are several instances running; you will look for the one instance that is managed and has the title “Installer Message” as set in the code.

  • The CopyEnvironmentConfigToExeConfig method simply copies the source config file and overwrites the *.exe.config file. For example, if you’re deploying to SAT, SAT.app.config overwrites YourApplication.exe.config.

  • The environment specific config files can’t simply be deleted. If you do that, when you click on the shortcut to the applicationm the installer will attempt to repair the application. Instead, I wrote the SecureEnvironmentConfigFiles method to blank out the files and set them as hidden.

  • If custom section handlers are used (e.g. Enterprise Library), then the
    section handlers must be copied to the system directory because the msiexec process executes from that directory and the CLR will be probing for the assemblies there. In the setup project, use the file system editor and include the requisite assemblies in the “System
    Folder” folder. For example, to encrypt the securityConfiguration section (Enterprise Library), Microsoft.Practices.EnterpriseLibrary.Common.dll and Microsoft.Practices.EnterpriseLibrary.Security.dll are included in the System Folder.

  • The Custom Actions editor needs to include a custom action in the Install folder. The CustomActionData property for this custom action can read the environment value from the user interface and pass the values to the install program as follows:

/ENVT=[BUTTON3]

/sectionNames=”nameOfSection1,nameOfSection2,etc…”

/provName=”configProtectedDataProviderNameInConfigFile”

/encryptApplicationSettings=”True|False”

For example:

/ENVT=[BUTTON3] /sectionNames=”securityConfiguration ”
/provName=”DPAPIProtection”
/encryptApplicationSettings=”True”

  • Don’t forget to include the installer project output to the setup project (in the File System Editor, like other project outputs).

  • The app.config includes the DPAPIProtection as follows:

<configProtectedData defaultProvider=DPAPIProtection>

<providers>

<add useMachineProtection=true

name=DPAPIProtection

type=System.Configuration.DpapiProtectedConfigurationProvider,
System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a,
processorArchitecture=MSIL
/>

</providers>

</configProtectedData>

There are some nice references out there. I’d like to recommend these two. They were a great help to me and I definitely wanted to acknowledge them:

Advertisements

6 Comments »

  1. I’ve been building an installer that modifies custom sections as well, and ran into the same problem as you – I must copy my section handlers (and any dependencies…) to my System32 directory if I want to be able to manipulate them. This feel very hackish to me, and going against the whole point of .NET and the “don’t touch system directories” ethos.
    Have you found any alternative approach? I’m thinking of starting a new appdomain, setting its base directory to my bin path and doing all my config code there, but it means my installer class balloons in size and complexity, and I would rather avoid that if I can.

    Comment by Avner Kashtan — 7 May 2007 @ 1:53

  2. You bring up a very good point. In fact, copying the EL assemblies to C:\Windows\System32 was a last resort approach, after trying hard not to do this. The issue is that the application executing when installing is msiexec.exe, located in C:\windows\system32. When I looked at the Assembly Binding Logs, is showed the AppDomain as C:\Windows\System32, which explained why my element settings to bind to EL assemblies not located in the system32 folder weren’t being applied. Let me know if you have another idea on how this could be resolved.

    Comment by Philippe Truche — 7 May 2007 @ 8:03

  3. Hey, good article. I don’t think you stated how the ENV.*.app.config file goes into the ExeConfigPath for copy to SourceConfigPath in the first place. Do you include the UAT.app.config for eg. in the setup installer so it is copied to program files?

    Comment by Graham — 13 May 2009 @ 1:16

  4. OK, well I have figured out one way, by manually including the file in the File System Option -> Application Folder in the Installer Project. Seems to work, and I imagine I could include all environments and simply Include/Exclude flag the appropriate environment at the time. Is this how you have done it Philippe?

    Thanks for your assistance.

    Comment by Graham — 13 May 2009 @ 1:30

    • I did this a few years ago, but yes, I think that is how I did it. I believe I also set the hidden attribute on files I did not want to show for a given environment.

      Comment by Philippe Truche — 13 May 2009 @ 6:18

    • Ha! I re-read my post and the answer is right at the bottom:

      “The environment specific config files can’t simply be deleted. If you do that, when you click on the shortcut to the application the installer will attempt to repair the application. Instead, I wrote the SecureEnvironmentConfigFiles method to blank out the files and set them as hidden.”

      Comment by Philippe Truche — 13 May 2009 @ 6:24


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: