Tuesday, April 6, 2010

XML text replacement

A new product I was working on required a UI prompt then replace in a config file, this case using xml. In the past I have tried using the XML File Changes view in InstallShield 2009 (and before) and had limited success. The task of supplying a property instead of a static value was simple enough, but the issue I kept encountering was additional decoration that was being placed on the xml file, which in turn caused the problems when trying to load the xml file at a later time, usually by the intended application.

In the past I had found some batch util that did text replace, but I found I was not comfortable putting this in my installer because batch files are like a black hole that get no reporting back to the log file. After the success with my previous post I felt confident that I could find something in c# that would allow this and reporting back to the log, plus it allowed me to learn a little more c#.

After searching I found a suitable sample. What this offered was to treat the xml file like a flat file because all I wanted was to replace a matched phrase with the value supplied by the user during install. This used the System.Text.RegularExpressions namespace to do the matching. What this sample does is load the file at once and do a match across the entire file. Because of this, it is only recommended for small files. Larger files will cause this to fail. With that in mind, here is my code used:

using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Deployment.WindowsInstaller;

namespace TextReplaceUtil
{
public class CustomActions
{
[CustomAction]
public static ActionResult TextReplaceAll(Session session)
{
string AutoLogPrefix = "### COMPANY MESSAGE ### ";

// setup variables using properties from MSI database
// this will come from a deferred action, which will be comma delimited
// format of "What to find", "What to replace with", "absolute path + name of file"
string CustomActionData = session["CustomActionData"];
string[] MSIProperties = CustomActionData.Split(',');
string TextToReplace = MSIProperties[0];
string ReplaceWith = MSIProperties[1];
string FilePath = MSIProperties[2];

session.Log(AutoLogPrefix + "Begin TextReplaceAll action, values passed are: " + CustomActionData);

try
{
StreamReader reader = new StreamReader(FilePath);
string content = reader.ReadToEnd();
reader.Close();

content = Regex.Replace(content, TextToReplace, ReplaceWith);
StreamWriter writer = new StreamWriter(FilePath);
writer.Write(content);
writer.Close();
session.Log(AutoLogPrefix + "Successfully wrote to file " + FilePath);
}

catch(Exception e)
{
session.Log(AutoLogPrefix + "exception thrown was " + e.Message);
return ActionResult.Failure;
}
return ActionResult.Success;
}
}
}



Just like before, you use the .ca.dll after it is built. In this case you also need to create a set property action in addition to the MSI DLL action to build up the CustomActionData value that we will be using here. The comment in the code indicates the format of what is expected and the order it should arrive in. With a utility like this, you can use this for any need for text replacement in your installers.

IIS 7 with InstallShield 2009

One of the recent situations we are addressing is not having the latest InstallShield software. Doing this and relying on their functionality can create complications. One we recently encountered was a defect that occurs with IIS 7 when virtual directories are created.

If you happen to have an installer that creates a virtual directory which also contains a subfolder by the same name for the virtual directories path, it will create an additional virtual directory at the subfolder's location. This was a very odd bug which was raised by only one of our products, but it of course causes it to fail because the path is now invalid.

In our efforts to use more built in capabilities, usually for MSI tables, but on some occasions InstallShield as well, we started using the IIS view in InstallShield. Up until 2009 I used to write custom actions to handle IIS tasks. Now I am no programmer, I guess I could be called a scripter, so my custom actions were not as robust as they could be, but they were functional. The driving force to using custom actions was that InstallShield was not properly handling a dynamic site id in previous versions.

To address the defect of the second virtual directory, I decided to start using c# custom actions after years of VBScript(I know, I'm awful) use. The necessary namespace I was using was based on sample code I found that referenced System.DirectoryServices. Intially I used InstallShield's built in managed code custom actions, but I converted to using the Custom Action project type you get with WiX and then adding MSI DLL custom actions. This worked far easier as this will demonstrate because you are using the session object. My main issue was I wanted to be writing to the Windows Installer log file at run time. Here is my simple code to address the defect:

using System;
using System.Collections.Generic;
using System.Text;
using System.DirectoryServices;
using Microsoft.Deployment.WindowsInstaller;

namespace AutoIISCAUtils
{
public class CustomActions
{
[CustomAction]

public static ActionResult DeleteVDir(Session session)
{
string AutoLogPrefix = "### COMPANY MESSAGE ### ";

// setup variables using properties from MSI database
string IISSiteID = session["IISSITEID"];
string AppName = session["APPNAME"];
string VDirName = session["VIRTUALDIR"];
session.Log(AutoLogPrefix + "Properties are " + IISSiteID + "|" + AppName + "|" + VDirName);

string VDirPath = @"IIS://localhost/W3SVC/" + IISSiteID + "/Root/" + AppName + "/" + VDirName;
session.Log(AutoLogPrefix + "deletion will occur on: " + VDirPath);

try
{
DirectoryEntry VDir = new DirectoryEntry(VDirPath);
if (VDir != null)
{
DirectoryEntry parent = VDir.Parent;
if (parent != null)
{
parent.Children.Remove(VDir);
parent.CommitChanges();
session.Log(AutoLogPrefix + "changes were successfully committed to the Virtual Directory " + VDir.Name + " which has now been removed.");
}
}
}
catch (Exception e)
{
session.Log(AutoLogPrefix + "exception thrown was " + e.Message);
return ActionResult.Failure;
}
return ActionResult.Success;
}
After the project is built it will generate two binaries that are dlls. The file to reference when you create the MSI DLL custom action is the .ca.dll version. Add the entry for the function name to call, change the execution and insert in the sequence. Make sure to add .Net as a prerequisite as well.

My reference for this task can be found here: