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:

2 March 2007

Installing a Smart Client Software Factory client application

Filed under: .NET — Philippe Truche @ 7:28

I’ve been working on a Smart Client Software Factory development effort, and as we were nearing the end of the development, I spent some time looking at how I would deploy the client application to end users.  Though I was initially attracted to ClickOnce deployment because of the automatic update feature, I shied away from this and ended up crafting an MSI installer.  Having tinkered with both technologies, I now understand better when ClickOnce and MSIs are appropriate.  In my opinion, ClickOnce should be used only if MSIs cannot be used.  I find MSIs provide much more control over the installation process, and from my experience, are somewhat easier to put together when your application is architected around the Composite UI Application Block. 

 This being said, the installer has to be able to handle the following requirements:

  •  The same MSI should be used for development, testing, and production.  This helps to ensure that the same code base is used throughout all environments.
  • The installer should be able to encrypt app.config sections during the installation.
  • You should be able to debug into the installer during development.

Now, before I move into the meat of the installer program and the setup project, let’s talk about app.config files and the web references in Visual Studio 2005.  When you add a web reference, Visual Studio 2005 performs a number of steps to make it easy to change the web service Url without re-compiling code.   One of those steps is to create entries in the app.config file of the project.  If the project does not contain an app.config, Visual Studio will create one for you.  So what happens to the app.config when the library project get compiled?  Well, you end up with a [AssemblyName].dll.config.  So your application directory may contain one exe, one exe.config, and nothing else.  You might be tempted to copy the dll.config files into the application directory, but that will not achieve the desired result: you want the application to obtain its web service Urls from configuration so you can make changes between environments easily.  So here is the trick:

Open the app.config file(s) from the library project(s) and copy the <section name=YourProjectName.Properties.Settings child element of <sectionGroup name=applicationSettings“…> into the same place in the app.config file of the Windows Forms project.  Then, copy the entire content of the <applicationSettings> section from the library project’s app.config into the Windows Forms app.config file, taking care not to replace its contents but add to it.  As a result, the app.config of the Windows Forms project will look like this:

<configuration>

<configSections>

<sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >     

<section name="YourExeProject.Properties.Settings"… />

<section name="YourLibraryProject1.Infrastructure.Library.Properties.Settings"…/>

<section name=" YourLibraryProject2.Properties.Settings"…/>

</sectionGroup>

</configSections>

<applicationSettings>

<YourExeProject.Properties.Settings>

<setting name="Timeout" serializeAs="String">

<value>30</value>

</setting>

</YourExeProject.Properties.Settings>

<YourLibraryProject1.Properties.Settings>

<setting name="Service1" serializeAs="String">

<value>http://localhost/VirtualDir/Service1.asmx</value>

</setting>

</YourLibraryProject1.Properties.Settings>

<YourLibraryProject2.Properties.Settings>

<setting name="Service2" serializeAs="String">

<value>http://localhost/VirtualDir/Service2.asmx</value>

</setting>

</YourLibraryProject2.Settings>

</applicationSettings>

</configuration>

 

I’ll get into
the code that satisfies the installer requirements on my next post.

21 February 2007

Visual Studio 2005 Service Pack 1 Installation Issue with EFS

Filed under: .NET — Philippe Truche @ 6:06

Argh… what a struggle it was to install Visual Studio 2005 Service Pack 1 on my laptop.  I googled the issue and did not turn up any useful information.   Since I figured out what the issue was, I figured this blog might come in handy for others.

Here was the error I was getting: “Error 1321: The Installer has insufficient privileges to modify this file: C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\ItemTemplates\VisualBasic\1033\AboutBox.zip.”

It turns out this issue is caused by the Encryption File System (EFS).  To get around the issue, I performed the following steps:

  1. Ran the installer file (VS80sp1-KB926601-X86-ENU.exe) up to the point I have to click OK.  This step alone takes a few minutes.
  2. Before clicking the OK button to have the installer proceed with the installation, I snoozed the Encryption File System by clicking on Snooze on the popup menu of the Encryption File System tray icon.
  3. In the command line, I ran the following command to decrypt the files in the “C:\Program Files\Microsoft Visual Studio 8″ path: C:\>cipher /A /D /S:”C:\Program Files\Microsoft Visual Studio 8”
  4. I clicked OK on the installer to let it proceed with the installation of service pack 1.
  5. The installer ran for a while, and eventually completed the installation successfully.

Voila.  That was it.  Let this entry be useful to someone else.

16 February 2007

Web Service Software Factory – Oracle

Filed under: .NET — Philippe Truche @ 1:50

The web service software factory (December 2006 release) actually is made up of 3 guidance packages:

  • Web Service Software Factory (ASMX)
  • Web Service Software Factory (WCF)
  • Web Service Software Factory (Data Access)

Each of these guidance packages is independent from each other, and the out-of-the-box data access guidance package works with SQL Server.

I have collaborated with yogz on the development of an Oracle compatible data access guidance package.  Far from being complete, the package does provide the following functionality:

  • Ability to generate data repository classes from business entities, with selection factories compatible with Oracle.
  • Ability to handle code constructs for retrieving a single object (Get One operation), a single object with sub-objects (Get One Complex), and a generic list of objects that optionally may have sub-objects (Get Many).  Unfortunately, some manual work is still required because the guidance package UI does not show sub-objects and Oracle ref cursors are not executed for retrieval of schema information.
  • Many refinements were made on the processing of IN/OUT parameters in the “Get” operations.  These refinements were not ported to the “Update” and “Insert” operations yet.

The Oracle data access guidance package is available here: http://www.codeplex.com/servicefactory/Wiki/View.aspx?title=OracleSchemaDiscovery&referringTitle=Home.

Many improvements remain to be made, but going from the 80/20 to the 100 is going to take significant effort.  We’ve used this on our project and found that the 80 was good enough…

« Newer Posts

Blog at WordPress.com.