What to do when there's nothing to do?

I'm in one of this situations where the product is in Beta, stable but not yet actually released. Therefore the codebase is locked down such that only the most pressing code changes are allowed to get checked in. In the ideal situation we'd have a branch of the code and a set of tasks to work on for the *next* release. For various reasons we don't have that at the moment. So.. it means unless a critical bug comes in in my code, then I'm locked out from doing any coding work at the moment.

 *twiddle thumbs*

Here's my recommendation on what to do in this situation: Document!

I know most programmers hate writing any sort of documentation, but someone working on installers and configuration management tasks is dealing with an area that even more urgently requires documentation. Most sorts of code you can write comments into it to give some explanation of why it is done that way. Some new programmer walking in off the street has a reasonable opportunity to grasp the basic flow and policies that went into how the code works.

Because MSI installers are table-driven, there isn't any good way to comment like this. Not only does a new install developer have to fumble around blindly, but I even forget why I did things the way I did if I'm away from the installer for a couple months.

The solution here is to document, at least in a basic form, the thinking behind why the install does what it does. I do this by making the document feature-based. For example, I have a feature where I want to validate the path a user has entered on a dialog. In order to support this feature there are entries in about 5 different tables (ControlEvent, Property, CustomAction, AppSearch, Etc.)

In my document I make a heading for this feature, explain what the feature is supposed to accomplish (in plain English), then list the 5 table entries that are required to provide this feature and explain in simple english how those entries relate to each other. For most features this takes a small paragraph and a 4 to 8 item bulleted list. Simple. I keep the document going, adding features to it as I add them into the installer. I guess you might call this a technical spec - I don't know the official term for it. I also go ahead and check this document into source control right alongside the installer project so that the two are easily located together.

A great benefit of writing this kind of documentation is that if you ever need to add this feature to another installer, you already have the pattern for how to do it. Similarly, if you ever need to remove that feature, you know all the little pieces that go with it and can remove it cleanly.

This is the type of documentation that often gets left out, but can really make a difference in providing long-term quality of code. Also, managers love it!

Hope you are enjoying living the Lifestyle of the MSIdle!

Susan

Vacation's Over!

Vacation was longer than I expected, but now it's over and it's back to work!

Posted by SusanGorman with no comments

On Vacation

School's out and it's officially summer! 

I'm on vacation for 2 weeks. I'll be back and posting more MSI, InstallShield and Configuration Management related items in mid-June.

Enjoy your summer!

Susan

Posted by SusanGorman with no comments
Filed under:

Customizing the PathEdit and DirectoryCombo controls to validate path entries

My last post Common Tasks: Validating a Path received this comment from HookEm:

 Two of your 5 conditions can be handled without a custom action using the native MSI controls:

"Must be less than 200 characters in length": see the Text property of the PathEdit control (msdn.microsoft.com/.../aa370749(VS.85).aspx)

"must be on a fixed (local, non-removable) drive": see the available hexadecimal bit flags assignable to the Attributes property of the DirectoryCombo control (msdn.microsoft.com/.../aa368290(VS.85).aspx)

 -----------------------------------------

This was great information! Thanks, HookEm! Please keep the comments coming Smile

I checked out the PathEdit control.

You can put {200} (for example) in the Text field and that will prevent the user from manually typing in more than 200 characters. For most purposes this will work perfectly fine.

However, there are some aspects about how the dialog works that you may want to be aware of. These are mostly corner case situations, but your QA team may run across them in their testing, so just wanted you to be aware of them.

  • When you click OK, the InstallShield SetTargetPath function will then append a backslash to the path - giving a length of 201. 
  • If you then click the Change.. button to go back into the dialog, you will get an error that the path is too long, because now it is 201, not 200.
  • Also, from the dialog itself, if you use the new folder button it makes a folder called "New Folder". If that puts your path over the limit, you get an error, BUT it goes ahead and creates the folder anyway. The path showing in the PathEdit field is the truncated path, but if you click OK from the dialog it actually sets the path to the long path including the New Folder. SO.. it's possible to leave the dialog having set the path to a length longer that your desired limit.
  • It's possible to get into a state on the dialog where the INSTALLDIR value is out of synch with the path in the PathEdit field.

 

I also checked out the Directory Combo control.

Very good to know! I hadn't looked at this control's extra settings before. From the Dialog editor UI in InstallShield you can set each type of drive, Fixed, Removeable, RAMDisk, Remote, etc. to TRUE or FALSE in that combo box. Making that change will immediately reduce the likelihood of a person selecting a disalowed drive type.

Note, however, that the user is still free to type into the PathEdit box a path that uses a drive of that type. So this does help, but doesn't completely cover the situation.

 

One thing to note is that using just these settings you can't validate the path unless the user is using the UI. If you use a custom action and table to validate the path length, then you can validate the path during the Execution phase, even if the installation is being done without UI. So, I'd recommend using these settings in conjunction with a path validation custom action. I think they complement each other. 

 

Posted by SusanGorman with no comments

Common Task: Validate a Path

The built-in dialog for selecting the Destination Folder will automatically do a certain amount of validation on the selected path. For example, if the user types in a path using a character that isn't allowed by Windows (such as a question mark ? ), then the dialog won't allow it. How do you handle it, though, when a user enters a technically valid, but undesirable path, such as C:\Windows. That's "valid" but definitely undesirable. Another similar problem for us is path length.

 So what is a good method of validating the path?

Here is the basic setup:

In the Custom Actions I make a custom action of some sort (InstallScript, VB Script, DLL, doesn't matter what...) called MyCustomAction. This custom actions performs my path validation using the specific criteria for MY product. If the path is a valid path, then it sets the property PATH_VALID to TRUE. If the path is not valid, it sets PATH_VALID to FALSE. The custom action also displays an error message alerting the user as to why the path isn't valid and tells them to try again.

On the InstallChangeFolder dialog, OK button I have these events:

  1. SetTargetPath -> [_BrowseProperty]. Always. (This sets INSTALLDIR to the entered path and does some brief validation on it.)
  2. DoAction ->MyCustomAction. Always. 
  3. EndDialog -> Return. If PATH_VALID (This returns to the Destination Folder dialog when the path is valid).
  4. Reset. If Not PATH_VALID. (This resets the InstallChangeFolder dialog back to its original values and redisplays it when the path is not valid)

 

Cool! This works great.

Up until now, I used a VB script custom action and my own custom DLL to do this. Doing it this way puts part of the 'definition' of the installation rules into a script or DLL and actually obfuscates (hides) the functionality so it isn't easy to see. The intended overall design of an MSI installation is to use a data-driven architecture, where the definition of the installer is in tables and an engine operates on the data in those tables. Today's goal is to switch my path validation to an InstallScript custom action that reads data from a table.

Step One:
Evaluate what types of path validation rules may be needed and work out a custom table format that will allow these types of rules to be entered.

In my current installation program here is the set of path validation rules that I have to apply:

  • must be less than 200 characters in length
  • must be on a fixed (local, non-removable) drive
  • cannot be on the root of the drive
  • cannot be in the Windows directory OR any of its subdirectories
  • cannot contain any of the following characters $ # ; %

At first I tried to have a column for Drive Type, and a column for Length, etc. I realized though, that these columns were too specific. They'd only be valid for certain conditions/rules. The table needs to be more flexible than that. So after several stops and starts on different types of columns to use. I decided that I would need to define my own condition syntax and use some sort of parser to evaluate the syntax. This simplified the table format so that it could look like this:

Table Name - "ValidPathConditions"

  • Column 1 - "ConditionID" Type: String, String Length: 31, Primary Key.
  • Column 2 - "Condition"    Type: String, String Length: 255
  • Column 3 - "ErrorMessage"  Type: String, String Length: 255, Localizable

So each of your path validation rules will have its own descriptive name (ID) and its own error message to be displayed if this rule isn't followed. Because the Error Message column is marked as Localizable, when you type text into this field it will automatically create a String Table entry for that string (such as ID_STRING1). However, you won't be able to modify WHICH string table entry is used in that column from within the IDE. You'd have to edit the .ism file in a text editor in order to do that. Minor issue.

The condition syntax that I decided on goes like this [operator][expression]. Note there is no space between the operator and the expression.

Each condition must begin with one (and only one) of these operators.

  • ! - meaning "not". Whatever expression follows this operator describes a condition that a valid path does NOT meet.
  • = - meaning "equals". Whatever expression follows this operator describes a condition that a valid path DOES meet.
  • > or < - meaning greater than or less than. The expression that follows must be a number indicating path length.
  • % - meaning "does not contain". The expression that follows must be a single character.

The expressions following a ! or = operator can be:

  • A string literal path surrounded by double quotes. Such as "c:\program files\bad path"
  • A Directory table ID surrounded by brackets. Such as [WindowsFolder].
  • A string literal or Directory table ID followed by the special token CHILD. Such as [WindowsFolder]CHILD. This token indicates that the operator applies to ANY child directory of the given path. For example, =[ProgramFiles]CHILD specifies that a valid path must be a child directory of the Program Files folder.
  • A special token for drive type, such as FIXED.
  • The special token ROOT. For example, !ROOT specifies that a valid path must not be on the root of the path's drive.

Other than a few special tokens, I think that the syntax is pretty self-explanatory and is flexible enough that it could easily be expanded. A person reading the condition column in the table would probably be able to divine what the condition was intended to do without having to look at the InstallScript code. That's my goal.

Step Two:

Create the actual table and fill it with data. So.. I've created the above table using the Direct Editor. Then I added 8 rows to this table representing each of the path validation rules that I have to support. The Condition rows are:

ConditionID01

<400 The path must be less than %s characters long.
ConditionID02 =FIXED The path must be on a fixed (local) drive.
ConditionID03 !ROOT The path must not be on the root of the drive.
ConditionID04 ![WindowsFolder]CHILD The path may not be in the Windows directory nor any of its subdirectories.
ConditionID05 %$ The path may not contain the characters $ # ; or %.
ConditionID06 %# The path may not contain the characters $ # ; or %.
ConditionID07 %; The path may not contain the characters $ # ; or %.
ConditionID08 %% The path may not contain the characters $ # ; or %.

I gave the Condition ID's the way that I did because they will be processed in alphabetical order according to ConditionID. This allows me to control the order pretty effectively.

This is a good start for now. Next time I'll provide the InstallScript code that I'm using to evaluate this table. Let me know if you have comments or suggestions. This is all a learning process for me!  

Til next time .. Hope you are living the lifestyle of the MSIdle!

NOTE ABOUT PATH LENGTH: The InstallChangeFolder dialog automatically truncates any given path to a length of 240 (241 if you include the last slash that it automatically adds). (Actually it may be the SetTargetPath action that does this truncation, but regardless..) This truncation is all well and good, but sometimes 240 is still too long. The reason is that if you install any files to subdirectories of the given path, that's going to increase the path length. Your user could enter a valid 238 character long path, and all will appear to be well until it actually tries to copy the file to the subdirectory (the too long path), and at that point the installer generates an ugly error message and aborts. Yuk!

 

What are the common install tasks?

Out-of-the-box (meaning the default Basic MSI project) your Basic MSI installer project is already capable of handling most of the standard installation actions. But what are some of the common features that you might want to add to the installer project -- and how might you do it?

Here are some of the things I've run into. If you have suggestions of common tasks like this, please leave a comment about it.

  • Path validation. The user enters a path in the destination dialog and the built-in dialog will automatically prevent them from entering an invalid path. That's good, but what if a user choose a path like C:\, or C:\Windows? These probably are not smart places to install the product.Wink Another problem is path length. If your have subdirectories that get created under the install directory, then what looks like a valid path length may actually turn out to be too long once the full subdirectory and filenames get put together. Bottom line - I find it useful to be able to validate the path that the user enters on the Destination Dialog and then prompt them for a better path if it fails validation.
  • Cleanup user data on uninstall. The uninstall is automatically capable of removing any items that it originally installed. What if your product creates some sort of user data over time (history, configuration data, etc.)? In that case, the uninstall will not know about those items and won't remove them. Best practice would be to totally clean up at uninstall any items that your product put on the machine. (I like to refer to leftover items after uninstall as 'hanging chads'). So you will need a method to cleanup those hanging chads at uninstall time.
  • Advanced Cleanup user data on uninstall. Once you've added the cleanup feature, I guarantee that somebody is going to come up with a reason why under some conditions the user will NOT want to cleanup all the user data. So the very next request that will come your way is... Make the cleanup of user data an option for the user to decide at uninstall time. NOTE: You will want to make the default be that it leaves the custom data so that you can support the next feature in the list.
  • Port user data on upgrade. This is a common scenario and it is related to the above two items. When doing a major upgrade it will uninstall the old version and it does so silently (with no UI). Often the user may have customized some portion of the product and it is desirable for that customized information to get carried forward to the new version. Because uninstall may very well remove some of those customized items, you will need a method of backing up that data before uninstall, then restoring it after installation of the new version.
  • Perform different upgrade tasks depending on which version you are upgrading from. Sometimes it seems that users tend to hold on to old versions of the software as long as they can. This can create a situation where you end up having to support upgrading from several different versions of the product, perhaps going back a couple years. Oftentimes you will need to run a certain upgrade task if you are upgrading from version 1 and a different task if upgrading from version 2.
  • Require user to install to same directory on upgrade. This is fairly common. Even though a major upgrade uninstalls the old version, there may be good reasons why it is wise to reinstall to the same directory. One good reason: customized user data gets left behind in the old install directory and it saves having to copy it to a new location. So.. you want a reliable method of detecting the old install path and reusing it.
  • Use the same install project to produce differently 'branded' versions of the same product. This happens often with my product line. We have a product that is almost identical as far as installation needs go. The only real differences are in the graphics, the product names, and perhaps a few components. It's valuable to work out a consistent and reliable method for being able to design the installer in such a way that you can fairly easily 'swap out' one brand for another without having to maintain two separate install projects.

If you have any other suggestions for common install tasks let me know. I hope to write up quick tutorials on each of these tasks.  

Over time, your company may come up with a few other tweaks and minor adjustments to the default installer project that you will want to routinely add to your project. This includes things like "Make all the dialog titles say the product name only and remove "InstallShield Wizard" from the title." or "Always create an MSI log when the install runs." As features like this become known -- do yourself a favor and write a quick little document that lists the feature, why it is wanted, and a quick list of what items to change in the installer in order to provide that feature. This gives you a quick template to follow whenever you start a new install project. And... should you ever win the lottery and run off to Bimini with your significant other Big Smile, then the new install developer will thank you in his prayers for leaving that kind of documentation behind.

That's all for now!

Basic Introduction to InstallShield and Windows Installer

The scenario here is that the previous install developer won the lottery and ran off to Bimini with his girlfriend, leaving YOU the job of taking over a dozen different InstallShield 12 install projects. Let's assume you know very little about installer technology other than the fact that you've installed software a jillion times and watched that little progress bar for longer than you care to admit. Let's also assume you are fairly computer-savvy. You understand software and computers, in general, but you need not be a hard-core code writer. Ready? Action!

The Players 

First issue... who's who? InstallShield? Acresso? Windows Installer? MSI? Who are all these people?

Windows Installer (WI) is the name of the technology (from Microsoft) that has become the new standard for installation and deployment. 

MSI is a the default file extension for Windows Installer databases. People have gotten used to using "Windows Installer" and "MSI" pretty interchangeably. 

InstallShield (IS) is the name of an installation software that's been around for years (before Microsoft Windows Installer ever existed). Before MSI's existed, InstallShield used a script-based technology (based on their own proprietary scripting language called InstallScript) to define installation and deployment packages. There's also a Wiki article on InstallShield, though it isn't much... http://en.wikipedia.org/wiki/InstallShield

Acresso is the name of the company that makes InstallShield (as of 2008). InstallShield used to be produced by a company called InstallShield. In 2004 the InstallShield product line (company?) was sold to MacroVision. I wasn't real excited about that and didn't find that it improved the product or service much. But hey.. that's my opinion. In 2008 the InstallShield product line was sold again to a company called Acresso. It remains to be seen if this helps or hinders the product and its quality and reputation.

The Technology

Nowadays it seems like the only smart thing to do is to use an MSI-based installer. It integrates so completely and consistently with the Windows operating system that it only makes sense to use it if you are installing on Windows. So... what is the overall concept for WI (Windows Installer)?

The key here is to realize that the installation instructions are essentially stored in a database, made up of tables with columns and rows. The The Wikipedia article on it gives a pretty good overview. Check it out... http://en.wikipedia.org/wiki/Windows_installer

When you run the program, you call the WI engine -- msiexec.exe. The engine then acts on the instructions found in a database (an MSI) -- myapp.msi.

The information in the various tables of the database tell WI what UI to display, what to do with the information the user enters into any dialogs, what files to install, what registry keys to create, any extra actions to take and what order to do this all in, etc.

There are several different applications that you can use to create an MSI. InstallShield is only one of them, but it does seem to be one of the most common ones. The benefit of using some sort of graphical IDE for creating MSI's is that it takes care of all the cross-table interactions for you. For example, if searching for a registry value actually requires entry in 3 different tables - you get to define your search in one window and InstallShield takes care of creating the entries in all 3 tables for you and you don't even have to know about it. This is great for a new user! I couldn't have started using WI without the InstallShield IDE.

As you become more familiar with the product, and as your needs become more complex, you will eventually get familiar with what is actually going on in the tables 'behind-the-scenes' when you design something in the InstallShield UI. At some point you will find that you need to do something that you can't actually do from one of the InstallShield wizards and you have to edit a table and it's rows directly. That's ok. InstallShield gives you a Direct Editor so you can do just that.  No problem.

Three flavors to choose from 

So what is the difference between a Basic MSI, an InstallScript MSI and an InstallScript installer? Simple...

An InstallScript installation is old-school. It doesn't use the Windows Installer engine at all. Instead you define the installation package's actions using the InstallScript scripting language.  You will find that the end result quite similar. The major benefit in my mind is flexibility.

A Basic MSI package uses the built-in functionality of the Windows Installer, which is quite robust. You get the benefit of the InstallShield UI with which to create the MSI. You are, however, limited to the database structure and if you need to do something that exceeds the base functionality, now you have a choice (or a challenge?) in choosing a method to extend the MSI functionality. Options include vbscript, javascript, windows API dlls, write your own (C++) dll. The major benefit in my mind is reliability. If you are *only* using WI functionality then you know what what you are using has been well-tested and extensively exercised by a huge programmer and user base. The more you limit yourself to the basic WI functionality, the less opportunity for programmer error and the more reliable your installer is likely to be.

An InstallScript MSI is a half-breed. Essentially it is an MSI, with an added layer of execution on top driven by InstallScript. This allows the UI to be driven by the InstallScript engine, which then calls the MSI engine to make the actual changes to the system. Theoretically this should be the best of both worlds. You get the flexible and robust extensability available from InstallScript functions, but the reliability and safe structure of an MSI.

I use a Basic MSI wherever possible because I don't want the added layer of the InstallScript engine to create potential problems. But I'm also overly cautious ;-) I will probably start using InstallScript MSI's over the course of the next year because for security reasons (read: Vista) I need to move away from vbscript in my custom actions and InstallScript is a good alternative.

 

Part Four - A simple C# app to modify an ISM

In the last post on this topic we defined an attribute for a <script> task. Note: You can find the file, as we last left it, attached to the previous post if you want to go grab a copy of it for reference. The finished file for this post is attached at the bottom of this post.

ImportStrings parameters

The ImportStrings method has 4 parameters, so let's get each of these declared and configured within the ExecuteTask function code.

1. The path to the file containing the strings to import - a string

To create a file containing strings to import just open a plain text file. At the beginning of each line write the string ID, put a tab, then put the string. Do not surround the string in quotes. Make sure to use a tab and not a space. The string must all be on the same line in the file. The end of the file should be at the end of the last line. If there is an extra CRLF in the file ImportStrings won't like it.

We've already declared a task attribute to hold this value: importfile.
And in our ExecuteTask function, the importfile attribute's value gets placed into the _importfile string that we've declared.

2. The langauge ID of the particular string table (e.g. are these English, French or Italian strings?) - a string

If you only have an English string table in your project, then the language ID will be "1033". If you open the ISM in the IDE and look in the Direct Editor at the ISString table, the ISLanguage_ column is the langauge ID. Remember that this value is a string even though the content is all digits.

I didn't create a task attribute for this value because I'm always using the English string table. However you could easily declare another task attribute just the same way you did for the importfile.
Instead, in the ExecuteTask function I declared and set the _langaugeID string to "1033" right from the beginning.

3. Whether or not to overwrite existing strings of the same ID - a reference to an object

Now things get a little weird. There is a special enumerator defined just for this ieoverwrite value. If you were in Visual Studio you could see this enumerator by the title "SAAuto12.ImportStringTypes". The two possible values are eiIgnore or eioverwrite. One means to overwrite strings with the same ID, and the other means to ignore duplicate string IDs and skip them.

The task attribute for this value is declared as a string: overwrite.
I also declared a string in the ExecuteTask function called _overwrite.

The problem is that somehow this string needs to get converted to an object reference for the specific ImportStringTypes object. I tried several ways of doing this (including trying to declare the task attribute itself as an object instead of a string) and wasn't able to find a simpler method. So this may be a bit of a kluge, but it works.

In the ExecuteTask declare an object of the ImportStringTypes type and set it to a default value of overwrite. I'd do this before the project.OpenProject method is called.

object _eiType = ImportStringTypes.eioverwrite;

Now make a switch block that takes the string "true" or "false" value for the _overwrite variable and converts it to the corresponding enum value. Make the default value = overwrite.

switch (_overwrite)
{
  case "true":
    _eiType = ImportStringTypes.eioverwrite;
    break;
  case "false":
    _eiType = ImportStringTypes.eiIgnore;
    break;
  default:
    _eiType = ImportStringTypes.eioverwrite;
    break;
}

When we actually use this variable in the ImportStrings method, we will use it as "ref _overwrite" so that we get a reference to it, as required.

Note: If you look at the IS help for this method it says that the overwrite parameter is optional. This works when I use a simple VBscript to run it. Unfortunately, I tried leaving this parameter off the ImportStrings function call when calling it from this C# code and it always failed with a message that the parameter was required. So... I always provide it. *shrug*

4. The path to write out the log file - a reference to an object

This is simply the path to which you want a log file created, along with the log file name. However this parameter has some of the same funkiness as the overwrite does -- it needs to be an object.

The task attribute is already declared as a string: logfile
The ExecuteTask variable is also a string as: _log

Again, to get our conversion to an object, declare an object inside the ExecuteTask method and set it equal to the _log string. I'd do this before the project.OpenProject method is called.

object _logfile = _log;

When we actally use this variable in the ImportStrings method we will use it as "ref _logfile" so that we get a reference to it, as required.

Note: If you look at the IS help for this method it says that the log file parameter is optional. Just as with the overwrite parameter, I got errors unless I provided it.

 

Basic Error Handling

Before we attempt to open the project, let's cover a few error conditions and handle them.

Let me state right now that I've been told this is not 'proper' C# coding - it's not best practice to use try/catch blocks in this way if you are doing full on C# classes and whatnot. Alright, so be it. I'll clean it up and make it pretty later :-) But for now, it works and seems adequate for this usage. So here we go.

Right after declaring our project object, start a try block. Inside that block we will test for a few different error conditions. Any error will throw us into the catch block where we log an error to nAnt providing it with the specific error condition, then issue a single task-level nAnt task error to indicate that our custom task has "failed".

ISWiProject project = new ISWiProject();
try
{
  ...set of error handlers described below goes here...
  project.SaveProject();
  project.CloseProject();
}
catch (Exception ex)
{
  project.CloseProject();
  Log(Level.Error, "ERROR: Cannot import strings. {0}", ex.Message);
  throw new BuildException("Cannot import strings");
}

 

In these error checks I've used the String.Format method in order to be able to insert the relevant variable's value into the error message. You'll see that String.Format lets you give a string, then put placeholders in for each of the items you want to insert into the message, such as {0} {1} {2}. Then just give each of the items to replace separated by commas.

1. Error: The import file doesn't exist

Make sure that the file exists with a simple call to the System.IO.File.Exists method. If it fails, throw an error.

if ( !System.IO.File.Exists(_importfile) )
{ throw new Exception(String.Format("Cannot locate import file {0}." , _importfile )); }

2. Error: The installer project file doesn't exist

Make sure that the ISM file exists, same as with the import file.

if ( !System.IO.File.Exists(_ismpath) )
{ throw new Exception(String.Format("Cannot locate ISM {0}." , _ismpath)); }

3. Error: The installer project exists, but it is locked or otherwise unable to be written to

In this case the OpenProject method will not return an error, so unless we check for it we won't realize that the project is successfully opened, but can't be written to. Open the project and check the return value. Any value other than zero is an error for us.

if ( project.OpenProject(_ismpath, false) != 0)
{ throw new Exception(String.Format("Failed to open ISM {0}." , _ismpath)); }

4. Error: The ImportStrings method fails in some way

We actually call the ImportStrings method now, but check to make sure it worked. We use the 4 parameters that we defined and configured above.

if ( !project.ImportStrings(_importfile, languageID, ref _eiType, ref _logfile) )
{ throw new Exception("Cannot perform import"); }

 

And that's all she wrote!

This should be a functioning custom task for importing strings. Hope this little exercise is useful to you in some way.

Tip: Editing nAnt files in Visual Studio

I used to think that opening nAnt files in Visual Studio was a lot of wasted overhead and not worth it. But I've realized that the auto-completion and IntelliSense features make it very worthwhile... so... here is how you setup Visual Studio 2005 to edit nAnt files. 

  1. Go to the directory where you installed nAnt. In the schema subdirectory is nant.xsd.
  2. Copy this file into the Visual Studio 8\XML\Schemas folder.
  3. Open Visual Studio. Open a project.
  4. From the menu Project > Add New Item…
  5. Select the template for an XML File. Give the file a .build extension.
  6. If the new file isn't already opened, go to the Solution Explorer and open it.
  7. Click somewhere in the opened .build file.
  8. Look at the pane that shows the properties for the file. One of the properties is Schemas. Click the … browse button and locate the nant.xsd file and check the box next to it.

Now you will be able to use the IntelliSense features of Visual Studio to see schema errors, syntax errors, and get auto-completion of your elements.

Special note... there is a bit of odd behavior that you might notice.  I discovered that if I clicked on the .build file in the Solution Explorer, the properties tab would change and I would no longer see the Schemas field, BUT if you then click inside the editing window for the .build file, the Schemas field reappears. Just some weird behavior from VS... Wink

Posted by SusanGorman with no comments
Filed under: ,

Part Three - A simple C# app to modify an ISM

Note: The finished file is attached to this post. Scroll down to the bottom of the post for the link. 

In my previous post I wrote a simple C# <script> task that could open an InstallShield ISM from an nAnt script. The eventual goal is to call the Import Strings function to import a string table into the ISM. I typically use a function like this when I have a master installer project from which I can build 2 different branded versions of the same product. I create a string table text file that contains only the strings that are different between one brand an another, then import the correct version of the string table depending on which brand I'm building. To do that we are going to need to have a few pieces of data that we can pass to the custom task whenever we call it.

Where we last left our hero<ic> nAnt script it could open an ISM project but didn't actually modify it. Here is what it looked like:

<target name="customtask">
  <script language="C#">
    <references>
      <include name="c:\prgram files\nant\bin\interop.isappserviceslib.dll"/>
      <include name="c:\prgram files\nant\bin\interop.ismautolib.dll"/>
      <include name="c:\prgram files\nant\bin\interop.ismupdaterlib.dll"/>
      <include name="c:\prgram files\nant\bin\interop.isupgradelib.dll"/>
      <include name="c:\prgram files\nant\bin\interop.iswibuildlib.dll"/>
      <include name="c:\prgram files\nant\bin\interop.saauto12.dll"/>
      <include name="c:\prgram files\nant\bin\interop.vba.dll"/>
    </references>
    <imports>
      <import namespace="SAAuto12"/>
    </imports>
    <code>
      <![CDATA[
        [TaskName("modifyISM")]
        public class modifyISM : Task
        {
          protected override void ExecuteTask()
          {
            ISWiProject project = new ISWiProject();
            project.OpenProject(@"c:\projects\myism.ism",false);
            project.SaveProject();
            project.CloseProject();
          }
        }
      ]]>
    </code>
  </script>
</target>
<target name="runCustomTask">
  <modifyISM/>
</target>

First, let's create an attribute for our task which we can set to the path to the ISM. Locate the line between the last curly brace and the ending square brackets. Insert a new line at this point and insert a declaration for an attribute like this:

}
[TaskAttribute("ismpath",Required=true)]
public string ISMPath
{ get {return _ismpath;} set {_ismpath = value;} }
]]>

The instead of "ismpath" you can use whatever you want the attribute to be called. And obviously you can set Required to true or false. I defined this attribute as a string. The difference between _ismpath and ISMPath is that ISMPath is the publicly visible string name, wherease _ismpath is only visible internally. This little set of code exists to allow the nAnt script to 'set' the internal variable to whatever value is passed in via the attribute. It ensures that the value of _ismpath only changes in a 'safe' way, by OUR function. This would be much more relevant in a larger task or code that was actually going to get called by other code. For our simple code it isn't really critical, but it's good practice to get used to the idea.

Now modify the line where we used to have a hard-coded path and replace it with this variable:

project.OpenProject(_ismpath,false);

In addition, modify the target from which we actually call the custom task and provide the attribute for ismpath:

<modifyISM>
  ismpath="c:\projects\myism.ism"
</modifyISM>

Try this out now. You could even go a step further and create a property to hold this path so that you could provide the ismpath on the nAnt command line for ultimate flexibility!

To prepare us for the next phase of this custom task, let's go ahead and declare a few more attributes that we are going to need: the path to a string table text file to import, the desired location for the import strings log, and an optional flag saying whether to overwrite existing strings of the same name. Insert these lines declaring the new internal variables right ABOVE the line starting the ExecuteTask():

private string _ismpath;
private string _importfile;
private string _log;
private string _overwrite; 

Then, go down to the first TaskAttribute add these lines immediately BELOW the TaskAttribute:

[TaskAttribute("importfile",Required=true)]
public string ImportFile
{ get {return _importfile;} set {_importfile= value;} }
[TaskAttribute("logfile",Required=true)]
public string LogFile
{ get {return _logfile;} set {_logfile= value;} }
[TaskAttribute("overwrite",Required=false)]
public string Overwrite
{ get {return _overwrite;} set {_overwrite= value;} }

Go ahead and add these attributes to the <modifyISM> call in the runCustomTask target.

<modifyISM
  ismpath="${ISM}"
  importfile="c:\projects\myStringTable.txt"
  logfile="c:\projects\import.log"
  overwrite="true"
/>

There you go. You have the framework in place. Next time we'll tackle some error handling and actually making the call to ImportStrings.

Part Two - A simple C# app to modify an ISM

So in my previous post I got a simple C# app written that modifies the ISM.

Alright, now to put this into the nAnt script. I'm going to assume a basic familiarity with nAnt here...  Note: I'm using nAnt .085 build.

Of course, start with a <target> that will contain the new task definintion. Inside the target start a <script> task. The language we are writing in is C#.

<target name="customtask">
<script language="C#" >

Now the first thing we must do is provide all the references so that our code can interact with the InstallShield Standalone Automation. These are going to be all the same references that we saw added in Visual Studio. For my task this is the list:

<references>
    <include name="C:\program files\nant\bin\interop.isappserviceslib.dll" />
    <include name="C:\program files\nant\bin\interop.ismautolib.dll" />
    <include name="C:\program files\nant\bin\interop.ismmupdaterlib.dll" />
    <include name="C:\program files\nant\bin\interop.isupgradelib.dll" />
    <include name="C:\program files\nant\bin\interop.iswibuildlib.dll" />
    <include name="C:\program files\nant\bin\interop.saauto12.dll" />
    <include name="C:\program files\nant\bin\interop.vba.dll" />
</references>

When we wrote the C# console application in the previous blog, the app worked because it created all the above DLLs to allow the C# managed code to be able to talk with the COM interface. These dlls were all on disk and accessible to your C# console app when it ran (as I understand it). So we need to accomplish the same thing for our C# nAnt task -- it needs to have access to all these same DLLs. In my case, I copied these DLLs to my nAnt program directory so that they would be available anytime this nAnt script runs. You might want to put yours in a different location -- as long as they are reliably going to be in that location when your script runs.

The next item is the <imports> section. The imports section corresponds to the "using" statement that we added in the original source code. So again, it is optional, but adding it allows us to shorten our code a bit since we can leave off the "SAAuto12" each time.

<imports>
    <import namespace="SAAuto12"/>
</imports>
 

Now we get to the code itself: The <code> section. I'm sure somebody could explain why, but all I know is that your code section will all be inserted between some sort of CDATA and brackets characters, like this:

<code>
    <![CDATA[
--- your code goes here ---
    ]] >
</code>

Again, I haven't looked up the meaning of this, I just know it works. Confused

In order to run the code as an nAnt task, we need to format it as a task. That requires a few extra sections of code from what we wrote previously. As a reminder, the original code was this:

    ISWiProject project = new ISWiProject();
    project.OpenProject(@"C:\projects\myism.ism",false);
    project.SaveProject();
    project.CloseProject();

Our new code starts by declaring this as a new type of Task (a class we inherit from nAnt)and implementing the "ExecuteTask" method in this class. It looks like this:

    <code>
    <![CDATA[

    [TaskName("modifyISM")]
        public class modifyISM : Task
        {
            protected override void ExecuteTask()
            {
                ISWiProject project = new ISWiProject();
                project.OpenProject(@"C:\projects\myism.ism",false);
                project.SaveProject();
                project.CloseProject();
            }
        }

    ]] >
    </code>

Now.. close up your <script> and <target> sections and you have finished coding this new custom nAnt task!

    </script>
    </target>

Let's do a quick check and make sure our new task 'customtask' can be run without error. Now when you run the 'customtask' target itself, all you are doing is compiling the C# code -- it does NOT actually execute it. 

Go to a command prompt and run your nAnt script calling the customtask target. For example I'd go to the dir where my nAnt script is located and run: nant customtask.

You should see a portion of the output that looks like this:

customtask:
     [script] Scanning assembly "cytjfj4d" for extensions.

BUILD SUCCEEDED

This is good! This means that it was able to successfully compile the code and create an assembly out of it. The assembly name is a randomly generated name and will be different every time. If you get an error -- scroll up through the build output and locate the first error. It should give you some sort of hint as to what needs fixing -- and the error should be very similar to what you would see if you were compiling this in the Visual Studio IDE.

Now that we know it compiles -- let's get wild and add an actual call to execute that code. Devil

<target name="runCustomTask" >
    <modifyISM/>
</target>

Run your nAnt script again, this time calling the new target after calling the customtask. Such as, nant customtask runCustomTask.

If all has gone well your nAnt script will complete without error and you get BUILD SUCCEEDED!

Next post I'll tackle defining parameters that you want to pass to your custom task and making this custom task actually modify the ISM. The example will be calling the ImportStrings function.

Part One - A simple C# app to modify an ISM

As part of my nAnt build process, I discovered I needed to modify a setting in my installer at build time.

InstallShield's Standalone Automation Interface provides a pretty simple way to interact with the install project using COM.
 
Note: I use the standalone version of the automation interface because it doesn't require having the full IDE installed on the box. The 'normal' automation interface can be used to perform the same tasks, but it will only exist on a machine with the full IDE installed. I'm assuming in these instructions that you have the standalone engine installed on your box.
 
In the past, I would have written a quick VB script to perform this task. But it's a new modern world Sarcastic and I'm trying to mend my ways so I took off down the path of trying to write a C# task to do this, then I'm going to call this task from my nAnt build script.
 
Here's what I learned:
 
I started by using the Visual Studio IDE to create a C# console application. I wrote the code in there and got it to where it compiled and ran. Then I moved on to porting the code over into nAnt. You should be able to make a simple C# app like this in about 15 minutes.
 

1. Start the project. Open the IDE (I'm using Visual Studio 2005 Professional Team Edition). File > New > Project. Find Visual C# in the list. Choose Windows > Console Application. Fill in the desired project name and OK.

2. Make the project aware of the Standalone Automation Interface. Project > Add Reference.... Select the COM tab. Scroll down to InstallShield 12.0 Standalone Automation Interface. Click OK. Notice that in the Solution Explorer you now have about 10 different references listed. (This list will be important later when we need to repro these references in the nAnt script.)

3. Import the namespace reference for ease of use. At the top of the code file (program.cs) add a new using statement. This statement makes the project aware of the namespace so that as we use objects within the SAAuto12 namespace we don't have to keep prefixing them with "SAAuto12." every time. It's just a convenience thing.

using SAAuto12;

4. Open the ISM project. Declare a variable to hold the project object, then open it. Provide the full path to the ISM file you want to test this on.

ISWiProject project = new ISWiProject();
project.OpenProject(@"C:\projects\myISM.ism",false);

Note: Because my path string has slashes in it I preceded the string with the @ symbol so that the compiler won't think they are escape symbols. Also, there is no error handling in this yet, so if it can't open the file or if it opens it but it is not writable, too bad. Tongue out

5. For now, save and close project. Just to get something working quickly...

project.SaveProject();
project.CloseProject();
 

6. Build the solution and test it. Build the solution and make sure this compiles. Go out to the command prompt and run the executable. It should run without error.
 
WHEE!! We now have a simple C# app that is capable of using the InstallShield automation interface to modify an ISM. Now all you have to do is add some error handling and put the code to actually make the changes between OpenProject and CloseProject. I'll blog on an example of this later.
 
Next blog I'll cover how to take this basic code and put it into an nAnt script.

RunOnce Key needed for limited user

Most installations should be per-machine installations at this point. So with a per-machine installation say that you have a need to run something on the next reboot. Typically you'd put it in the HKLM RunOnce key. On Windows XP and Vista, limited or standard users don't have rights to delete from the HKLM RunOnce key.
Therefore, Windows won't even try to run whatever is in the HKLM RunOnce key.
 
So how do you effect a RunOnce in that situation??
 
Well, you could put the key into the HKCU, but this has problems, too.
1. Especially with the advent of Vista, it is HIGHLY recommended that you do not put user-specific stuff into your installer, for several reasons. Robert Flaming (former PM for the Windows Installer group at Microsoft has some valuable discussions about UAC and Vista and per-user problems on his blog.)
2. If you use the Registry table to create the HKCU key, beware! Keep in mind who the 'current user' is going to be at the time when the registry keys get added. If you are on Vista, and the user had to provide an admin's login in order to run the installer, then the 'current user' will be the admin, NOT the real user.
3. This process falls down if it so happens that the person who installs is not the next person who logs on.
 
Here are some options to try for a solution to this problem:
1. Don't do it. Find some way to refactor the installation so that the thing you are trying to put into RunOnce can be initiated by your product when it is first launched. It is really best practice to put this type of configuration into the execution of the product rather than in the installer.
2. Put the item in the HKLM Run key, but then find some way to either delete it from the Run key after it runs, or modify the item you are running so that once it's been run it just immediately exits if it gets run again.
3. Create a custom action that adds an HKCU RunOnce reg key but author the action so that it runs Deferred WITH the Impersonate bit set. This should cause it to treat the Current User as the *real* user and not the admin. Note, however, that this option has drawbacks! For one thing, it would be hard to clean this up on uninstall because you can never be sure who 'current user' will be on uninstall. In most cases the key should be gone by the time uninstallation happens, but... For another thing, this just illustrates why it is best practice to keep all your installation actions and data at the per-machine level.
 
 
Posted by SusanGorman with 1 comment(s)
Filed under: , , ,

Build and test a simple DLL

Continuing with the walkthrough of making a DLL from my previous blog...  
As a reminder, I'm following along with Chapter 19 (Dynamic Link Libraries) from Programming Windows 95 by Charles Petzold. 
 
I have a header file with a macro defined for exporting the function. Now I need to declare my function. The DLL from this book isn’t something I’ll use with installers, but it’s pretty simple and lets me get my feet wet. 
 
EXPORT BOOL CALLBACK EdrCenterText (HDC, PRECT, PSTR);
 
I typed this into my header file. Then took a step back to identify what these pieces parts all mean.
  • First we have the EXPORT macro that was defined earlier.
  • Next is the type of value that the function will return (BOOL).
  • The CALLBACK constant is something that I had trouble with. I did several searches in MSDN help and wasn’t finding any reference to an all caps CALLBACK. I finally got lucky and happened to have my mouse hovered over the word in the VS window. A tool tip popped up that said:

#define CALLBACK __stdcall.

Ooo! So this told me that CALLBACK is actually just a macro for the __stdcall calling convention. This convention has to do with how the function reads parameters off the stack and how it will clean up after itself. The __stdcall calling convention is… well… standard and in fact the Windows API uses it. In addition, I happen to know that we will want to use this convention when creating DLLs to be read by an installer. So.. CALLBACK is fine. 

And I have an important tip for myself -- go ahead and type the code in and use tool tips as an additional 'help' feature rather than trying to figure the code out before I type it in.  

That’s it for the header file. Moving on to the source code.  In Visual Studio I go to Source Files, Add, New Item. Then select the .cpp template and name my new file. We start off by including a few standard files, followed by our own header file.
#include <windows.h>
#include <string.h>
#include "edrlib.h" 

I’ve learned before that the <> symbols tell it to look for the header in a list of standard places, where as surrounding the filename in quotes tells it to look for it in the same directory as the .cpp file. 

Now I’ll type in the definition for the DllMain function. We don’t have any special actions needed to initialize this DLL so DllMain doesn’t do anything except just return TRUE. If I understand correctly, your DLL 'must' have a DllMain function so this should be pretty standard.

BOOL APIENTRY DllMain (HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved)
{
 return TRUE; 
}

Now I’ll enter in the definition for the actual function in this DLL. The function takes some text in a window and centers it. Again, nothying I'd use in an installer so the details of this function aren't important. I'm just copying the code out of the book so I  won't repeat it all here.

After entering in the code I go wild and try to build it. (Silly me!)  I get an error (fatal error LNK1561: entry point must be defined).

It took me a bit of research to figure out what was needed here. Keep in mind that the book I'm using is 'old school' and assumes you are using a command line compiler and editing your code in notepad. So I'm having to figure out how to use Visual Studio to achieve the same result. Eventually I ended up making a project for a non-MFC DLL and comparing the resulting project properties between it and my test project. I realize that my only problem is that my project is defined as an EXE instead of a DLL (due to the fact that I started with an Empty generic project.) So to fix the build problem I go to the project properties, General tab and set the Configuration Type to .dll.

Sweet! Now it builds without error. In the output directory I see my DLL, a .exp and a .lib. Let's just take a moment to celebrate this minimal amount of success, considering that I'm *not* a C++ programmer. Smile

Now the test to find out if the function can actually be called from an external program. Still within my original DLLtest solution, I choose to create a New Project, Other Languges, Visual C++, General, Empty Project. At the bottom of that dialog is a Solution combo box. I select Add to Solution and OK. Now I have a single solution with a project for the DLL and a project for the EXE to test it.  Skimming over the next few steps - I just copy the code from the book to create a .cpp file for an EXE that opens a window and calls the function from the DLL to show text centered in the window. Now what my book tells me to do is:

  1. Include a reference to the header file from the DLL
  2. link the OBJ file from the DLL to the EXE (using a .MAK file) 
Since I'm using Visual Studio to do this, I did a little searching around and used some knowledge that I already happened to have and figured out that I needed to do these steps in my EXE project:
  • Add the directory that contains the DLL’s .h file to the Additional Include Directories on the C/C++ General property page.
  • Add the directory that contains the DLL’s .lib file to the Additional Library Directories on the Linker General property page.
  • Add the DLL’s lib file name to the Additional Dependencies on the Linker Input property page.

Once that's done I can build my DLL, then build my EXE. Then I run the EXE and voila! I get a window with the text centered in it. 

First major accomplishment is achieved!

Posted by SusanGorman with no comments
Filed under: , ,

What the heck is a __declspec?

(Note: I’m using InstallShield 12 for my install development and Visual Studio 2005 for DLL development.)

The goal is to learn to make a simple DLL that I can use in an MSI custom action. Today I started with the basics using a classic book that still has relevance today: Programming Windows 95 by Charles Petzold.

Chapter 19, Dynamic Link Libraries.  I got some basic concepts about dynamic linking vs. static linking, what a DLL is, then read though the source code for a simple DLL with a single function.

I open up Visual Studio, start a New Project. The Project Type is Other Languages , Visual C++, Empty Project. Under Solution Explorer, Header Files, I add a new (Code) Header File (.h).

I read the first line out of the book that it wants me to write into a .h file and already I’m lost. Too many keywords that I don’t understand the meaning or use of. The first line is:

#define EXPORT extern “C” __declspec (dllexport)

Huh? So I set about doing some research.  The #define statement I can understand.  But what about EXPORT? Is that a predefined keyword or what?  For some reason I picked the “extern” as the first thing to highlight and then hit F1 in Visual Studio. Eventually I made my way to this section of the MSDN help:

In the MSDN Library, Development Tools and Languages, Visual Studio 2005, Visual Studio, Visual C++, Programming Guide, General Concepts, DLLs, Importing and Exporting, Exporting from a DLL. *whew!*

This whole section was good and explained fairly simply the basic concepts about exporting and what our keywords in this statement were really doing. After looking through this section and also looking through a few sections about using “Windows Installer DLLs” and “standard DLLs” in the InstallShield 12 User Guide (downloadable from the Documentation section off their website), I feel that I have a good basic understanding of what we are trying to do with a DLL. So here’s what I came up with…

In order to setup the DLL so that the functions are exported and made available to external applications, we need to use certain directives that tell the compiler to export them. There is 2 ways to do this – one, use a .def file, two, define export directives and use them in the function declarations. Either method is fine. In C++ programming you are more likely to see the second method, so that’s the method I’m going to use.

So we are going to define some export directives. We *could* put these directives right in the declaration for each function, but to make it simpler, we’ll setup a macro with the directives we want, then just use that (much shorter) macro in front of each function.

So in the header file start off by defining the macro with this code:

#define EXPORT extern “C” __declspec (dllexport)

This defines a macro so that where the term “EXPORT” is used in your function declarations, it will be (essentially) replaced with the whole string that follows it in the above line. So what is all the rest of the string for?

  • “extern” tells it to make the function available to external programs
  • “C” tells it that we have a C++ function that we want to be accessible to C language modules. This avoids function ‘name decoration’ that would otherwise be used when compiling for C++ language modules.
  • “__declspec (dllexport)” allows us to skip having to create and use a .def file for exporting.

By the way I've used "EXPORT" but you could use any term that's valid as a macro name. 

Ok, so far so good. I feel I’ve made some progress at learning the fundamental concepts here, so it seems like a good spot to stop.  Next time I’ll tackle actually declaring a function!

Posted by SusanGorman with 1 comment(s)
Filed under: , ,
More Posts Next page »