Saturday, May 10, 2008

Custom Installer and MSIs

When deploying software into a Test or Production environment, it’s worth spending the time developing an automated deployment. In the long run it will save time and will hopefully avoid mistakes if a file or a configuration setting is forgotten.
For me, automated deployment means developing an MSI. You could try zipping files that need to be deployed and writing batch files to install them but you really can’t beat MSIs. An MSI has two outstanding features: they are easy to install and include the ability to uninstall. We are fallible creatures and make mistakes so the ability to uninstall and try again is a necessity.

Easy installation is important for a Production environment where access may be restricted and you need to provide installation instructions for someone else. Frankly, if I was responsible for a Production environment, I wouldn’t let someone like me anywhere near it! Processes and change management are crucial in a Production environment and this is a different mind set to when you’re developing.

Anyway, back to MSIs. The point about an MSI is that it will perform checking for disk space and check if the correct software is installed (like .NET Framework 2.0) and will install your files to a target location. It can add files to the GAC too and then when you uninstall it will delete all trace of the files. Terrific, but I want to do more. I’ve got a web site and it's got a WEB.CONFIG file that has a number of ‘appsetings’. I need to amend these ‘appsettings’ to be suitable for the target environment. I also need to create a web site and configure the IIS attributes on it (e.g. Integrated Windows authentication and anonymous access disabled). You can get the MSI to do all of this for you by using a Custom Installer. When you run the MSI wizard you can prompt for the values you want to set in the WEB.CONFIG and then the Custom Installer can complete all the other steps for you. And yes, it can also reverse these steps when you uninstall.

Start by creating the Custom Installer first. Create a New Project that is a Class Library. Set the namespace and rename the Class to say CustomWebSiteInstaller. Add the following references :

Add the using declarations at the top of the class and add the following attribute just above the class declaration:

Make the class inherit the Installer class by adding after the class name:
: System.Configuration.Install.Installer

Then you need to override four methods:
public override void Install(System.Collections.IDictionary stateSaver)
{ base.Install(stateSaver);
public override void Uninstall(System.Collections.IDictionary savedState)
public override void Rollback(System.Collections.IDictionary savedState)
public override void Commit(System.Collections.IDictionary savedState)

The main method you are going to add code into is the Install but don’t neglect Uninstall and Rollback because if you want to reverse any actions that you add to the Install method you will need to enter them here. The Rollback method is executed if you hit the Cancel button when running the MSI wizard. The stateSaver is a collection of name/value pairs allowing you to save and restore settings (its how the MSI knows where you’re files are when you want to uninstall because it’s saved the target directory).

You should be able to compile your Custom Installer now. We’ll come back to add the code in a while because next you need to add a new Setup Project to the solution (you will find it under Other Project Types and ‘Setup and Deployment’. The Setup Project will create the MSI for you so give it a name like WebSiteInstaller. In Solution Explorer right click on ‘WebSiteInstaller’ and select Custom Actions, User Interface and then File System.

The File System shows 3 default folders:

The Application Folder is by default
Where Manufacturer and Product name are properties of the Setup Project. It means that the files you add to the Application Folder will be copied into \Program Files\MyCompany\MyProduct but of course you can change this during the installation.

You might want to add subfolders to the Application Folder, for example a bin directory.

In the right pane, use right click to add files and assemblies and project output.

First let’s add the Custom Installer. Select Project Output. In the dialog, select the CustomWebSiteInstaller project and then Primary output and click OK.

This will add the DLL into the Application Folder directory as part of the installation and this is mandatory so that you can run your custom actions.

To the Solution you will need to add the Project you need to deploy and then you use this same dialog to add files, assemblies or Project Output. It is aware of dependencies so will add your dependent DLLs automatically. So add the other files that will make up your application.

Select the User Interface tab. Note that the user interface displays a list of the default screens which are displayed when you run the MSI wizard. Right click on Start and select Add Dialog. You’ve now got a list of custom screens that you can add into the wizard. The ones we are interested in are labelled ‘Textboxes (A) , (B) and (C).

If you add Textboxes (A) you will see under properties you have four textboxes referred to as Edit1 through to Edit4 where each has four properties
The Label is the prompt you will see to the left of the text box. The Property is how you can reference the textbox in code (it’s always uppercase and without spaces). Value is a default value that will be shown. Visible is obvious and you select it as true if you don’t want to show one of the text boxes. All three dialogs are the same so you can prompt for a maximum of 12 parameters during installation.

If you want more or you have more sophisticated requirements then just simple text boxes then you can create your own form in the Custom Installer and refer to the controls on that. Its more effort because all the controls are dynamically created and you have to set size and position etc. So I try and use these 3 built-in dialogs whenever possible.
So for one of the textboxes set the Label as ‘K2 Server: ’ then set the Property as ‘K2SERVER’.

Now go to the Custom Actions screen where you will see you have four items each corresponding to the Actions of the Installer: Install, Commit, Rollback, Uninstall. On the Install item, right click and select Custom Action.

In the Dialog box double click on the Application Folder.

There you will see the Primary output of the Custom Installer. Select it so that it adds it under the Install item in the left pane.
When you select this item on the left, the Properties window will show the all important ‘CustomActionData’. In here you will need to add the parameters you want to pass to the Custom Installer. So far we just have one: the K2SERVER so you want to enter:
The syntax is important; get it wrong and you won’t be able to access the parameter. The quotes too are important, without them it won’t allow spaces in your parameters.
UPDATE: I've been getting an error when running the MSI "System.IO.FileNotFound" and it references a file in C:\Windows\System32\Files\...
I finally found the cause. It's because one of the values of the parameters entered ends with a backslash. It doesn't occur if the backslash is anywhere else in the text, only at the end does it cause a problem. Presumably "\" is acting like an escape character. I've not found a way round this yet - just avoid the need to have a final backslash. END OF UPDATE
Add other parameters in the same way using a space to separate. If you are wondering what the /k2server= is for its because these are also used as command line switches for the MSI so you have to be able to identify the parameter by name.

The MSI is just about done. Go back to the Custom Installer. To refer to the parameters in the Custom Installer you can use the Context class and within that there is a Parameters collection. The syntax is:

string myk2server = Context.Parameters[“K2SERVER”].ToString() ;

I use a function to access the Parameters collection because you need to trap for null values.
There are some built-in parameters in this collection and the most important of these is ‘assemblypath’ . This gives the full path to your custom installer including its name so it you strip off this last part what you have is the target directory. From there you can access the files you want.

The last part is then dealing with the actions that you want to perform during the install. I have a bunch of utilities in a DLL that will do the business from modifying the WEB.CONFIG, adding Identity Impersonate=true, adding registry entries, and running an executable.
I’ve added the code for the ExecuteProcess below. The example below will create a web site by using a batch file and pass a parameter to it. It takes a bit of effort to get the syntax right – try writing the message out to the event log and checking it before you try executing it.

utils.ExecuteProcess(TARGETDIR + @"\CreateIISWebSite.cmd", @" WebSite " + "\"" + strRootWeb + "\"");

Now you can run most executables with this method but note that the process you call needs to return that all important ExitCode. If it doesn’t then your MSI will sit there 95% complete but never actually finish. The main culprit of no ExitCode is MSBUILD.EXE. The solution is to write a batch file, passing the parameters you need and just to be certain add the Exit command as the last line. Works a treat. It’s a long time since I wrote batch files so I have to brush up on them.

And that brings me to my last point, I litter the event log with Information Messages from the Installer. I write out every member of the Parameters collection. Believe me it’s worth it. If the person doing the install claims there is a problem either during the install or post-install then you are going to want to know what values were entered. Get them to send you the Event Log and that will help you to figure out what they did wrong. You will also find that the Event Log includes the output from the batch file so you should ALWAYS check the event log to ensure it all worked. Your batch file might not actually do what it’s supposed to and if you don’t raise an error the MSI is going to report a successful install when in fact it wasn’t.

Good luck with your install!

// you need to add using System.Diagnostics;
public void ExecuteProcess(string fileNameString, string argumentsString)
Process process1 = new Process();
process1.StartInfo.FileName = fileNameString;
process1.StartInfo.Arguments = argumentsString;
process1.StartInfo.UseShellExecute = false;
process1.StartInfo.CreateNoWindow = true;
process1.StartInfo.RedirectStandardOutput = true;
process1.StartInfo.RedirectStandardError = true;
process1.StartInfo.RedirectStandardInput = true;
if (process1.ExitCode != 0)
// Log(string.Concat(new object[] { fileNameString, " failed - ExitCode=", process1.ExitCode, " Output: \n", process1.StandardOutput.ReadToEnd() }));
// Log(fileNameString + " succeeded - Output: \n" + process1.StandardOutput.ReadToEnd());


Brian Storrar said...

Great article, Charles. I don't suppose you've ever managed to deploy an Oracle SQL Package as part of your installation without using an external application to do it?

I always find building custom installation tasks frustrating and useful in equal measure.

Immanuel said...

it is great post. it is nice and wonderful artcle. I don’t think this is really necessary but will leave it. installerex

Immanuel said...

It isn't necessary to place the image files in the root directory however it makes finding the images easier. it is nice and great post.
installerex custom installers