Now that I know Rob still reads my mostly stagnant blog, I guess it is the appropriate time to write a long-overdue post.
When I started working with Windows Installer technology, back when it was first introduced with Office 2000, I played around with customizing the package for the IT department to push out a customized version. The tools were quite primitive, the technology was new and largely unknown, and the concept of having blogs, yet alone Microsoft folks blogging, seemed completely foreign. Support was pretty much nonexistent, and much of the documentation was unintelligible. Fast-forward 10 years, and what a difference that makes! Today there are several free and low-cost repackaging tools for transitioning non-Windows Installer based setups to the MSI format, authoring tools, and lots of community support.
Most setup authoring tools have significant issues. Non-MSI or script based installations have issues because they encourage hacks - I can't tell you how many installations I encountered that install services by writing keys to the CurrentControlSet hive and forcing you to reboot merely so the Service Control Manager can pick up that addition. Furthermore, if you are targeting any sort of enterprise where more than one of your setups will be installed IT departments want MSI deployments for very good reasons. GUI based Windows Installer tools fail to do a good job of grouping related things into the same component, and dynamically adding a directory of files at build time breaks patching semantics horribly. Another big disadvantage to these tools lie in the setup author because he or she does not need to understand the underlying technology and can get away with "programming by coincidence" (as described in The Pragmatic Programmer).
I remember several paradigm shifts throughout my experiences with setup technology - nested MSIs, merge module distribution, and chaining installations. During this time the stock price of Rolaids likely skyrocketed. The biggest challenge was attempting to get developers to take a more proactive approach to deployment considerations as they were writing their code. One approach that I took was the use of merge modules - developers of feature-units would package their build output in an MSM that was consumed when building the final product. Using Visual Studio 2005+ with their deployment projects was not only difficult, but downright impossible because of how limited, shortsighted, and buggy deployment projects are. Adding custom actions to these modules involved a complex and convoluted post-build scripting process that nobody understood, but it DID move teams towards the direction of thinking of deployment while coding.
These days, the tag-team of MSBuild plus Wix 3.0 is THE enabler to accomplishing those goals and largely eliminating the disadvantages of the GUI-based tools. Since there is close to a one-to-one correlation of XML elements to the Windows Installer tables, it is quite simple to follow if you understand the underlying Windows Installer engine. To use WiX to author a complete installation, you MUST have an understanding of the Windows Installer engine. To make a few tweaks or additions once the basic skeleton of the installer is laid out, just about any developer can do it provided access to the WiX documentation. I have team members that are NOT setup developers add services, event log sources, and more with no official training.
Some of the more compelling points in favor of WiX is how you can use it to easily and properly make multiple product editions which share components, separate units of related components into their own WXS file(s) for easier understanding and maintenance, and integrate it easily as a first-class citizen into an MSBuild project. No other product is available to my knowledge that accomplishes those goals. Best of all - WiX is free, fast, and easily installable onto any developer machine.
If you are looking to switch authoring tools, take WiX for a test run by using the dark.exe decompiler to convert your existing MSIs and play around with it a bit. Subscribe to the WiX mailing list and ask a few questions. You just might like it.
Congratulations to Rob and the entire team and individuals who have contributed to it, as well as the community of developers who support it via the mailing list on a daily basis. If you are ever in Pittsburgh, let me know. I'll buy you a beer.
Thursday, August 27, 2009
Wix way should you go?
Monday, June 27, 2005
All About Properties
Late last week an anonymous poster commented it has been a while since my last blog post. I'm going to attempt to make up for it with this rather long post as the first in a short series on MSI Properties. Newbies to MSI technology have a difficult time understanding them, and even experienced setup developers get burned by forgetting something. Let's start by looking at some of the basic properties of, well, Properties.
Properties are sort of like variables. Properties cannot have spaces in their name and must begin with a letter or underscore. All properties can be defined in the Property table or set by other mechanisms – via AppSearch, Custom Actions, dialog control elements, etc. They have scope: Public, Private, and Restricted. Cosmetically, Private properties have lower case letters, Public properties are all uppercase. Under the hood, there are plenty of differences. I am going to ignore the Win9x and Terminal Server aware differences and concentrate on the NT based MSI engine.
We first need to cover something I should have covered in an earlier blog post – the MSI engine's UI phase and execution phase. The UI phase (sometimes also referred to as the 'acquisition phase') is carried out in the current logged on user's context. The execution phase (sometimes also referred to as the 'server process' or 'server side', or simply 'service' – lets hear it for MSDN documentation consistency!) is carried out in a completely different process (or processes). This is pretty easy to see for yourself – write a custom action that pops up a message box with the current process ID and run it in three different contexts – UI (Immediate), Execute (Immediate), and Execute (msidbCustomActionTypeNoImpersonate + msidbCustomActionTypeInScript). If you are an administrator, you will notice that the UI action runs in one process and the execute actions run in a different process – all owned by the interactive user. If you are a non-admin user, you will note that each of the three actions run in their own separate process, with the difference being the "NoImpersonate" action, which runs in the SYSTEM context.
Back to the property discussion – Private properties have lower case letters as stated earlier. They cannot be passed into an installation via the command line method or modified by transforms. Predefined Private properties may be set by the MSI engine – see "Property Reference" in the MSI docs for a list of properties set by the MSI engine automatically. Private properties can be changed by Custom Actions, although you likely do not want to change one of the predefined private properties. One last important distinction is that changes to private properties are NEVER passed from the UI phase to the Execution phase. Although there may be a reason to use a Private Property for something, I have never created a private property for use in an installation.
Public Properties are a bit more useful. There is a slight difference between Public and Restricted which I will cover next. For the purposes of this paragraph, treat them as synonymous. Public Properties must contain all uppercase letters. They can be set via the command line and modified by transforms. Public properties CAN be passed into the execution phase, and the key word here is CAN (more on this later). Also, values of properties are NEVER passed from the execution phase back to the UI phase when the execution phase is done processing – so if the value of a property is changed in the execution phase, it is not reflected in the UI phase afterwards.
Restricted, or Restricted Public Properties, are Public properties with the distinction being purely based on system or user state. Looking at an out-of-the-box Windows installation and a generic template MSI installation the following statements apply: If you are an administrator running an installation, properties WILL be passed to the execution phase (Restricted public property behavior). If you are not an admin user, most public properties WILL NOT be passed to the execution phase (normal public property behavior). If you want ALL properties set in the UI phase to pass to the execute phase (i.e. you want all public properties to be restricted), enter a property into the property table "EnableUserControl" and set it to "1". This setting can also be set as a system group policy by a network administrator. Alternatively, each property that you want to allow to be passed into the execution phase (made Restricted) can be specified in another entry in the property table called "SecureCustomProperties" which is a semicolon delimited list of public properties. By default, some properties are set to be restricted, see the "Restricted Public Properties" list in MSDN. This concept is a bit confusing, as the "Restricted" properties are the ones allowed to transition from UI to execute phases, even though you may think the opposite when first hearing the term.
The concept of "Restricted Public Properties" is rather important for your installation to work if installed by a non-admin user via group policy style deployments. It is highly advisable to test your installation for this deployment type by creating a local admin user and setting both HKLM and HKCU Software\Policies\Microsoft\Windows\Installer "AlwaysInstallElevated" (DWORD) to 1. Then take the user out of the admin group. This will have the effect described above. Test your installation in this configuration to make sure it functions correctly.
There are more fun tidbits about properties…
First, all entries in the directory table are accessible as properties. Therefore, be sure not to run into naming clashes when determining your property names. Be sure not to rely on the directory properties until after CostFinalize has been run.
Some properties, if authored into the Property table, result in an ICE validation warning. The documentation for ICE 87 explains some of them. Other properties in this table are validated with ICE 05, ICE 16, ICE 24, ICE 74, ICE 80, ICE 86, ICE 90, and ICE 99. I found it useful to review all the ICE's when learning MSI, since it locks in the "don't do this" mentality before you try something and it later fails...
You can set the "MsiHiddenProperties" Property to a list of semicolon delimited properties that should not be placed into the install log. This only works on MSI 2.0 or greater schemas, and is really not a good means to secure vital information, such as passwords or product codes.
Many Properties (and several Windows Installer API's) are not available to a custom action running deferred. To pass a property to a deferred custom action, use a type 51 custom action to set a property with the name of the deferred custom action you want to pass data into, sequenced prior to the deferred action in the execute sequence. In the deferred custom action, you can access this property by getting a property called "CustomActionData". Since you typically need to pass more than one property to a custom action, the technique of stuffing a bunch of properties together and later separating them is commonly used.
Any property value that is changed during the installation process is not preserved or persisted – thus, properties that were set during installation are not present when uninstallation occurs. IMHO, not providing a built-in persistence mechanism is a serious oversight made by the MSI team.
Speaking of persistence, the "AdminProperties" property is one way to persist a property – albeit only during an administrative installation. To allow this behavior, add the property you wish to set into a property called "AdminProperties" in the usual semicolon delimited list manner. When a user runs an installation after it was deployed to an administrative installation point, these properties are loaded with their saved values – a good easy way to provide default values specific to an enterprise without developing a transform.
The last important thing to realize about properties is their "Order of Precedence". Essentially, this describes when a property is set. I'm going to list these in the reverse order as MSDN does, as it makes more sense to do so. A property can potentially start its life as an entry in the Property table. If a transform was applied to the MSI, this will change what was described in the Property table. Next, the saved properties from an administrative installation that were listed in the "AdminProperties" property are restored. If anything was specified on the command line, those properties are used. Finally, operating system properties are set. To illustrate by example, if you declare property FOO to be "prop table" in the property table, then run the installation from the command line by "msiexec /I test.msi FOO=cmdline" FOO will be set to "cmdline" since the property is set by the command line AFTER it was set by reading the property table.
My next blog entry will discuss some techniques to persist properties and their data across installation sessions.
Monday, April 25, 2005
Installing an Internet Explorer ActiveX Control Part II
In part I of this article, I expressed concern over the various methods of installing an ActiveX control normally distributed and/or updated in a CAB file via Internet Explorer. This follow-up post addresses some concerns of a commenter to that last post, and describes what my solution was.
The comment made by Troy was from the perspective of a repackaging solution. Troy is quite correct - using pre and post installation snapshots will grab the changes to the ModuleUsage subkey. However, this solution fails to achieve the other goals for this particular installation. We still have the issue of a later update coupled with the MSI repair (or self-repair) operation. A point I failed to mention but Troy unintentionally reminded me of is compatibility across versions of IE. With the type of software this is, it may have a shelf life that would include IE7. Are the changes made to the system under IE6 applicable to all versions of IE under all OS's? I don't know. Using the API to install the control alleviates most (but not all) of the potential future incompatibility concerns.
I especially like how he disliked option #3 - changing the IE security settings to assure a successful installation, calling the API, and resetting them. I laughed as I wrote this option, as I could not imagine anyone doing this. In the middle of last week, I came across the "Get In the Zone" section of an issue of "Web Team Talking." They actually suggest and provide code for doing just that - although in a different context for a slightly different reason. I happen to agree with the KISS directive - "Keep it Simple, Stupid" - but sometimes to assure a successful operation you need to handle the corner cases.
The last part of his comment - "[this is] why many enterprise customers choose to repackage installations from ISVs" is a completely different blog post. I cannot tell you how true this statement is - and it is usually caused by setup developers, either intentionally or unintentionally. I'll add this one to the list of future topics for sure.
For the sake of discussion we were using the Crystal Reports Web Viewer which can generically be replaced with any properly signed cab file that works in the normal deployment mechanism of IE using the OBJECT tag. I have not tried this with unsigned controls, but I tested the approach using Macromedia's Flash player. You can play along by visiting the Macromedia's Flash download page and getting the CLSID from the OBJECT tag in the page's source. I then downloaded the swflash.cab file from the codebase attribute of the object tag and stored it on my local disk. The example below is going to use this flash cab file and the flash Class ID. Additionally, I am going to assume the reader is familiar with COM, C++, and MFC. The code is a bit lengthy to post here, but using the steps below a reasonable Windows developer should be able to complete this exercise in an hour or two.
I chose MFC since we need to implement IBindStatusCallback. This is easily accomplished by deriving a class from CCmdTarget. Make sure the implementations of QueryInterface(), AddRef(), and Release() call the External...() base class implementations. GetPriority() and OnLowResource() can simply return E_NOTIMPL. All the other methods can return S_OK. Since this is an async callback we need to have an event handle member variable, initialized in the Constructor, and have the event set in the OnObjectAvailable() method. That's the essential plumbing for the callback class. The callbacks for download progress can be implemented for the progress bars in the MSI.
Before we continue, let's make sure we have all the data we need to install the control.
- The class id of the control is {D27CDB6E-AE6D-11cf-96B8-444553540000}. It needs to be in this format as a string (with the braces) so we can call CLSIDFromString to convert it to a CLSID type.
- We are going to use major and minor versions of 0xFFFFFFFF to assure it is always installed.
- Finally, we need a codebase - or place to "download" the cab file from. Since we are installing the cab file locally, the CA needs to be appropriately place in the sequence and we need to get the local path to the file. The path needs to be in appropriate format - for a local disk where the file is at C:\swflash.cab, we need to make it file://c:\\swflash.cab
The entry point code needs to hook up the plumbing - instantiate the IBindStatusCallback implementation class, call ExternalQueryInterface() on it to get the interface pointer. Then we need to create and register the binding context by calling CreateBindCtx() and RegisterBindStatusCallback().
The meat of what we are doing is a call to CoGetClassObjectFromURL() with the parameters described above. The two common returns from this call will be
MK_S_ASYNCHRONOUS, S_OK, or an error. If S_OK is returned, the object is already installed. If the async value is returned, we need to loop until the event in the callback class is set or some predefined timeout elapses. After the event is set, check to see that the object was created based on what was set in the OnObjectAvailable() method.
If you wish to have the installation always grab the "latest" version of the code from a URL, you will need to implement the code to change and restore the IE security settings from the article linked to above and also implement the download callbacks to move the MSI progress bar. Be sure to alert the user that you will be temporarily altering the IE security settings, and make sure when handling a cancel button press from the MSI that you restore the settings. This particular corollary to the problem can be useful if you want to refresh the clients using a simple repair/maintainence/reinstall option after the server-side control was updated without having Bob the network admin do much extra work (or having to release client side patches).
Sunday, April 17, 2005
Installing an Internet Explorer ActiveX Control
This post is different from my usual posts, mainly because I have no idea what I am talking about! Well, at least I am admitting it this time!
I am tasked with creating an installation for an intranet type application. I need to create a shortcut to a URL and install a client side ActiveX control. For the sake of argument lets make it a fairly common one, such as the Crystal ActiveX viewer control. Sounds pretty simple, eh?
Let's go over a typical usage scenario. Hopefully, when designing applications and their deployment packaging you have some made-up profiles of typical users. Lets make up a few for our purposes. "Bob the system admin," "Mary the power user," and "Joe the restricted user" should be good enough. We can be a bit more specific and cover XP SP2 users, users with severely restricted ActiveX Browser settings, and non-IE browser users as well, but lets ignore these for now.
Essentially, Joe has a problem. We can give him the URL to the web application or even create a shortcut and/or favorite for him. The problem is, the first time he tries to run a Crystal Report, he gets a security warning, and the Crystal ActiveX control does not install. Of course, it is because he has no permissions to install software, even if everything else we ignored above is in good shape. Incidentally, if we can't figure out that Joe's problem is that he is a limited user, you can try the CDLLogViewer application to find out why.
Bob and Mary never have issues with the system. In fact, if Bob and/or Mary perform the same steps as Joe everything works, and Joe logs in later on, even Joe can view and print reports. The problem is that Bob and Mary would never run reports on the system. Heck, Bob doesn't even launch the application. Bob may even deploy the install package using SMS or Group Policy.
In the corporate network world, pretty much everyone is a Joe. There are a few Privileged Mary's, and even less Bob's. To make all this work, Bob needs to install applications for the likes of Joe. This behavior is not only acceptable, but practically expected.
So how would Steve, our installation guru, make all this work? He would not only need it to work, but be practically bulletproof.
Option 1: The easiest approach. 99% of the time, this is synonymous with the wrong approach. We can extract the Crystal Reports ActiveX viewer cab file, read the .inf file to figure out what it is doing, add the files to a MSI project selecting the proper registration options, create the shortcut file, and be done with the project in under 20 minutes.
Why it doesn't work: Since Joe is not the only user of the machine (remember Mary and Bob?), any updates to the control will cause the MSI-based installation to screw up at some point. If the control is updated by Mary, and Bob uninstalls the application, the component/reference count then causes havoc with Mary by possibly removing the file on uninstall. Effectively, we have precluded the normal installation/updating/removal of the ActiveX Viewer by repackaging it in an MSI. More information on the hazards of this approach can be found on MSDN in the "Registry Details" page. Bad stuff.
Option 2: Install the shortcuts as in Option 1. Call the appropriate API's to install the CAB file from the web server. In this case, (after a bit of digging through MSDN I see this process is called "Internet Component Download" or ICD for short) we need to call CoGetClassObjectFromURL.
Why it doesn't work: Referring to the ICD link above, we assume the web server is up and running at installation time - #1 in the "Download and install process". We also assume that WinVerifyTrust returns a value that is compatible with the setting for ActiveX controls for the Zone in which the server we are pulling the cab file from resides - #2 in the "Download and install process". Last, but not least, we are assuming that good old Bob didn't muck with the CODEBASE setting of the Internet Search Path.
Option 3: Completely decimate the user's machine by querying the IE Security Settings, saving them, adding the server to a trusted zone, modifying the zone parameters to set the ActiveX settings appropriately, call CoGetClassObjectFromURL, after it succeeds, set everything back to the way it was.
This seems like a heck of a lot of work, but it appears to be the only way to bulletproof an installation of an ActiveX component. Plus, we ignored several issues. If the default browser was (or will be) Firefox or even Lynx, a shortcut to this URL will always fail to display the reports. To solve this problem, an EXE wrapper would need to be written to host the IE control to assure that IE is actually the browser engine used to access the page. Joel, the API war is far from over - it just made things more complicated!
Of course, I'll get to play around with these API's and see if installing the cab file from a file:// UNC path changes anything. I'll report back what I find. If anyone else has already "solved" this problem using an approach I haven't considered, please let me know. In all likelihood, I'll end up calling CoGetClassObjectFromURL, and if it fails, try to provide some meaningful feedback to the user as to why the installation can not succeed and let Bob worry about the corner cases. After all, he ultimately created them.
UPDATE: Please see Part II of this post!
Monday, November 15, 2004
Insane security
Raymond Chen posted about some questions better left unanswered because the answer is unreliable. This quote cracked me up:
"You cannot reliably reason about the security of a system from within the system itself. It's like trying to prove to yourself that you aren't insane."
Although the quote is funny, he makes several points about security that developers should heed. Commenters have taken the post a bit out of context - but the side point he makes is valid. Since most of my audience is setup/installation developers, I'll direct my comments towards that community.
The next time you are given a design document for installing a service, please make sure to ask about the security level required for the service. Make sure to raise objections to installing a service with access to the desktop without good reason (read comments to this post for more information related to the shatter attack) . Ask if LocalSystem access for the service can be reduced to a more restrictive account (HINT: There is a sample for creating user accounts within an MSI in the Platform SDK). Most of all, insist that scoping of installation/deployment design is best when it is done with the scoping of the feature or application - not after. This way, the application developer can develop in an environment closest to that which will ultimately be deployed to customers in the installation with no surprises looming at the end of a development cycle.
In case you haven't noticed, Windows XP SP2 and Windows Server 2003 have tightened up security in these areas. Instead of blindly loosening up the default restrictions this brings, take the application to the next level by understanding why the restriction is now the default in the first place, and learn how to program taking into account this more secure environment. As a setup developer, your customer is the end user. Make sure you are on the side of the customer when it comes to security.
Friday, October 22, 2004
Raymond is on a roll... (shared files, versioning and setup)
Fellow blogger Raymond Chen has posted yet another blog of note to setup developers. I'm going to use a different example than he did, plus use it as a springboard for yet another rant. If you remember back to Windows 95, especially when configuring network settings, it would prompt for the CD (bad) and then prompt to if you wanted to overwrite a newer version of a file with an older version (even worse). As an added bonus, it asked this question more than once (the absolute worst).
Why did it do this? Read Raymond's answer. For this specific example, most people installed updates to the original TCP/IP stack. Windows 95 wanted to restore the initial version of all files in all cases. This could leave the user hosed if they didn't answer the questions consistently. Microsoft learned from this example. We should, too.
Lets look at how the MSI engine handles patching. OK, maybe that would be too long of a rant:) The essential thing to take away is that a MSI patch (msp file) will not overwrite a newer version of a file with an older version. In the usual case, this is not a problem, since all identically named DLL's are always backwards compatible (in Utopian software development), right? This is a decision the Windows Installer engine designers had to make up-front. And as long as we are in Utopia, it works.
If you recall, many older InstallShield installations also prompted the user on uninstall if they wanted to remove a DLL that is marked as shared, since no other programs claim they are using it. The more modern versions of InstallShield don't. Why? They expect us (setup developers) to get it right. Make sure you are using the appropriate method of installing files that may be shared by other installations. In the MSI world, Merge Modules accomplish this. But especially for non-MSI installations, make sure you are setting the "shared" flag, so the uninstallation of you application does not impact other installed applications by removing files critical to their operation.
As a side note, Raymond links to an older article related to asking useless questions that is quite humorous, yet further proves this important point: If you ask the user a technical question, odds are they they're just going to stare at it blankly for a while, then try to cancel out of it.
Don't make these mistakes.
Friday, September 24, 2004
Wix video presentation
Rob Mensching published a video of a presentation he gave back in June regarding the Open Source Wix MSI toolset.
The video is a 64 MB download. My recommendation - do a right-click and SaveAs on the link.
Tuesday, September 21, 2004
GDI Plus Vunerability Information for Setup Developers
Stefan Krueger from InstallSite.org has posted a very well-written article about the GDI+ vunerability and how setup developers should respond to it. Please read it ASAP, it is a great consolidation of all the available information, and should be acted upon quickly.
If you haven't subscribed to Stefan's RSS feed, here is the link. If you don't know what RSS is, try this link.
Monday, September 20, 2004
Making Progress Bars behave nicely
After my previous blog entry about installation pet-peeves, including how writing progress bar code is hard, I figured I would try to help a bit by providing a simple sample for MSI authors. At the same time, a question related to progress bars appears on the newsgroups. Coincidence?
If any of you have some stylesheet stuff I can add to make posting/reading code snippets on a blog less painful for both you and me, please send it in.
The usage in a Custom Action is pretty simple: you parse CustomActionData if you are running deferred and determine how many things you have to do. Then call setupProgress("MyCA","Transmorgifying files",10); if there are 10 things to do. When performing each of the 10 things, shoot off a call to tickProgress("this filename"); and the bar should behave nicely. It's pretty easy to convert to the Custom Action language of your choice. Most of the information needed to create this sample is in the Session.Message() documentation.
var msiMessageTypeProgress = 0x0A000000;
var msiMessageTypeActionStart = 0x08000000;
var msiMessageTypeActionData = 0x09000000;
function setupProgress(ActionName, ActionDescription, numTicksExpected)
{
//set the UI Status ACTIONSTART
var statusRecordObj = Installer.CreateRecord(3);
statusRecordObj.StringData(1) = ActionName;
statusRecordObj.StringData(2) = ActionDescription;
//below line defines a template for display on each message tick
statusRecordObj.StringData(3) = "[1]";
Session.Message(msiMessageTypeActionStart, statusRecordObj);
//resets the progress bar.
var progressRecordObj = Installer.CreateRecord(4);
progressRecordObj.IntegerData(1) = 0; //0 = reset bar
progressRecordObj.IntegerData(2) = numTicksExpected+1;
progressRecordObj.IntegerData(3) = 0; //0 = left-to-right, 1= R-to-L
progressRecordObj.IntegerData(4) = 0; //0 = calc time remaining
Session.Message(msiMessageTypeProgress, progressRecordObj);
//tell the progress bar to increment for each ActionData message
progressRecordObj.IntegerData(1) = 1; //Action Info
progressRecordObj.IntegerData(2) = 1; //numTicks each ActionData
progressRecordObj.IntegerData(3) = 1; //increment bar for each ActionData
Session.Message(msiMessageTypeProgress, progressRecordObj);
}
function tickProgress(message)
{
//The ActionData record follows the template defined in the msiMessageTypeActionStart
//item #3. Refer to the MSI documentation related to templates. If you add more fields
//to the template, make sure to add the additional fields here as well.
var progressObject = Installer.CreateRecord(1);
progressObject.StringData(1) = message;
Session.Message(msiMessageTypeActionData, progressObject);
}
Saturday, September 11, 2004
Wrestling with the MSI setup editor in Visual Studio
Tim Anderson blogs about editing an MSI after it is built by Visual Studio .NET. There are various tips hidden in here about how to cope with duplicate assemblies being added to the MSI, marking a file so upgrades don't overwrite it, invoking the Custom Action editor, and MSI SQL.
For more detailed information related to editing an MSI after it is built, check my previous blog entry "MSI - Modifying the Installer Database at Runtime" and scroll down to Solution 1.
For more information as to how to launch a program after installation, see How do I use a custom action to launch an installed file at the end of the installation? from MSDN.
Tim also mentions as to why he stuck with using the Visual Studio MSI packager instead of a commercial editor. He gives 4 reasons, and I would add purity as a fifth. Regardless of the authoring tool he uses, his code will always work. Granted, pretty much any tool he uses will eliminate the need for his particular edits to the MSI, but the concept is still valid.
Monday, September 06, 2004
MSI - Modifying the Installer Database at Runtime
A recent question posted to the Microsoft.public.platformsdk.msi newsgroup:
I'm trying to prevent some NT services installation with a VBScript custom action (type 38) that removes the corresponding rows from the ServiceInstall table.The poster shows some example code that does not seem to work. Let's look at some approaches to try to solve this problem.
Solution 1
Here's some theoretical JScript to run in a Custom Action that modifies the installer Database:
oDatabase = Session.Database;
oView = oDatabase.OpenView("select * from ServiceInstall");
oView.Execute();
//now our view object has records we can "fetch"
oRecord = oView.Fetch(); //get the first record from the query
while (oRecord != null) //make sure we are not at the end.
{
//TODO: Either handle the logic on what to remove based
// on the oRecord here or in the query. Assumes all
// records matching the query are removed.
oView.Modify(6,oRecord); //6 = msiViewModifyDelete
//get the next record from the query
oRecord = oView.Fetch();
}
oView.Close(); //close the view
oDatabase.Commit(); //commit our changes
Looks pretty good, right? We are opening the database, fetching records, modifying the records, and committing the changes. Sad to say even though the API logic is correct, this won't solve the problem at hand! Why?
Custom actions can only add, modify, or remove temporary rows, columns, or tables from a database. Custom actions cannot modify persistent data in a database, such as data that is a part of the database stored on disk. (View.Modify documentation excerpt)As an aside, you could substitute the following 2 lines for the first one and modify the msi (permanently) outside a CA:
oInstaller = new ActiveXObject("WindowsInstaller.Installer");
oDatabase = oInstaller.OpenDatabase("myInstall,msi",2);
Solution 2
So, with this restriction, how do we solve your problem? The answer lies (buried) in the View.Modify documentation. The action type msiViewModifyInsertTemporary is our friend. Instead of solving the problem by deleting the services you don't want to install, we can add the services you DO want to install at runtime! Remember to delete the lines from the ServiceInstall table if you use this method
Lets look at an example of this:
oDatabase = Session.Database;
oView = oDatabase.OpenView("select * from ServiceInstall");
oView.Execute();
oRecord = Installer.CreateRecord(13);
oRecord.StringData(1)="MyService";
oRecord.StringData(2)="MyServiceName";
oRecord.StringData(3)="MyServiceDisplayName";
//SERVICE_WIN32_OWN_PROCESS
oRecord.IntegerData(4)=16;
//TODO: continue for the rest of the columns in this table. See
// the ServiceInstall table docs for more information
//msiViewModifyInsertTemporary the record.
oView.Modify(7,oRecord);
oView.Close(); //NOTE: no need to call commit..
You do need to deal with the uninstall issue using another CA. That is not covered here, but the concept is similar.
As an aside, I use the above technique to adjust the ReserveCost table when I need to create a large database. If the DB exists, I do nothing, otherwise, I add the size of the database to the table for proper costing.
Solution 3
Although Solution 2 will solve this problem, why would you want to go through that complexity when there is a much better (and simpler) method built into the MSI engine? In the ServiceInstall table, there is a Foreign Key into the Component table. If the component is to be installed, so is the service.
Simply create a hidden feature, assign the components necessary for the service to that feature, and conditionally install (or not install) that feature.
Monday, August 23, 2004
COM+ in an MSI - Of Trials and Tribulations
I recently answered a question on installing COM+ applications in an MSI on one of the newsgroups. I promised that I would go into a bit more depth on the subject here, specifically on the different methods of installation I have tried and why they didn't work (and of course, what did work).
I had an application that consisted of about 200 COM+ components. These components were to be installed into three different directories, essentially separated by their tier (DB, Business, Common). The naming convention was related to the purpose of the DLL. As an example the Business component was "CurrentCustomerListBus.dll" and the DB component was "CurrentCustomerListDB.dll." The build output mimicked the actual desired installation directory structure. Several of these DLL's required configuration by means of adjusting the activation string and timeouts. These settings were different for each installation.
The first approach tried is the Microsoft suggested approach of exporting the COM+ application as an MSI. This pretty much populates an MSI with the files and TypeLibraries, plus adds a blob of undocumented stuff to the ExpType column of the Complus table of the MSI. When this is done, the output was less than desirable:
- MSI Validation fails due to short file name (SFN) clashes. Not really a big deal, but worth mentioning. This is due to point 2 below.
- The destination directory is a single directory. When exporting the application, there were no SFN clashes since they were in separate directories. Note the similarity of the first several filenames above. When the package is deployed they are going to the same directory thanks to the export process, so these clashes now exist. Although the single directory was not according to specs, it was a livable limitation.
- The installation specific activation strings could not be configured in the MSI. A Custom Action would still need to be written to programmatically change them after installation in the host MSI. Again, this is a problem we could live with.
- Deployment via a nested installation, meaning this exported COM+ MSI would need to be installed as part of another MSI. Nested applications are extremely difficult (if not impossible) to patch (exact words from the above link are "Patching and upgrading may not work with nested installations"). Additionally, to properly assure the application (which in my case was a Web application) would not be accessed until the application was fully installed, the nested approach would not be ideal. This one was a deal-breaker, but as it turned out, not the only one.
- Type Library clashes. Although this is not a problem with the export, it does indicate a problem with the construction (programming) of the COM+ application. While the application still functioned OK with these issues, redeployment through the exported MSI was just not possible. Plus, this was not discovered until much later - so I did not know the exported MSI really didn't work.
Approach #2 built upon the first approach. We were using Wise for Windows Installer 3.x which did not have COM+ deployment, so this limited other options. I took the exported MSI and turned it into a merge module using the Windows Installer API's and some tricky scripting. This approach solved problems 1 and 4 above. That is when we found out about problem #5 above, which completely killed this option.
Approach #3 was upgrading to Wise for Windows Installer 4.x Pro (Pro edition allowed for COM+ deployment plus build scripting). This approach failed due to all but reason #4 above, which was the only thing the Wise method solved. To blatantly rip-off Wise's new saying that they stole from my former coworkers (yes, you know who you are) "...but it worked on my machine." Yeah, right!
Approach #4 was the final, shipping method that I found to be 100% reliable, extensible, and portable. We started using Wise and moved to InstallShield with very little work (and none related to COM+) because of this method. For the tool migration benefits alone, this is why I prefer not to use vendor or tool-specific MSI extensions (InstallScript especially). But that is a subject for another future blog entry...
One last bit about approach #4 before I give away my secrets. I implemented it not exactly in the manner I am suggesting here. The main reason I didn't is to hit the testing deadlines and be sure what I released would work in the field. My approach is not perfect, and is difficult to move to another install. In other words, it is not a well-written series of CustomActions and tables that are portable enough to move to another installation without some modification. After this release, time became an issue so I couldn't reimplement it in a more extensible and portable manner. Please don't make the same mistake with your implementation.
To use approach #4, you should be familiar with interacting with Windows Installer via custom actions (VB Script if you want to use the many examples floating around - I prefer JScript). Secondly, you should not be afraid of programmatic access to COM+.
- Create a feature and mark as hidden from the UI. Assure that this feature is always installed via using a Level or a custom action that sets it to be installed based on whatever conditions you require. Note that this feature can not be advertised.
- Add your COM+ component dlls, and only your COM+ component dlls to this feature. Assure that you do not have them self-register or extract the COM information at install build time.
- Use the COM+ APIs to construct some CustomAction(s):
- At uninstall, call ShutdownApplication() on the COM+ Application and remove the COM+ application before the files are being removed. If your COM+ components are used by a service (homebrew, IIS, etc.), make sure you stop the means of access to the COM+ application. I generally place this action after the ServiceControl action in the standard sequence. This will prevent in-use problems at uninstall time.
- During repair/reinstall/maintenance, you want to restrict access to creating the COM+ application and call ShutdownApplication() on it prior to any file actions.
- During installation (and I do it as part of repair), delete and recreate the COM+ application and install each component. Essentially, you need to create a COMAdminCatalog, and on it call GetCollection("Applications"). You are returned a COMAdminCatalogCollection object consisting of Applications. Populate it by calling Populate() on it. Iterate the collection, remove/edit/create as necessary a new COMAdminCatalogObject to represent your COM+ application. Set the Name, Description, and ID values appropriately (refer to this) and then call SaveChanges() on the collection. This gets you the configured COM+ Application. To add components to it, call InstallComponent() for each COM+ dll. I make sure to create progress bar information as well so it does not appear to make the installation hang. I would use the information in the FeatureComponents, File, and Component tables to create a query that will ultimately allow you to get the path to each COM+ dll included in the hidden feature to build this list, then pass it as CustomActionData to the deferred action that actually does the work described above. Most of the COM+ stuff can be seen in action at the link I gave earlier. The MSI interaction stuff I expect you to should already know or be able to figure out from the inline links above - if you have specific questions, just ask!
- Optional - configure component properties as required, such as the activation string, timeouts, etc. on a per component basis. I chose to hard-code this instead of creating a new table in the MSI to store these customizations, but in the future I would use a custom MSI table based approach.
That's all there was to it. It actually took less time to complete approach #4 than approach #2. I am not looking at the source code of my implementation to be sure I'm not missing anything, but this should be complete enough for you to use as a starting point at the very least.
Thursday, August 19, 2004
Why can't I use WScript in my MSI Custom action?
This is a fairly frequent question, and one that deserves a decent answer. VBScript and JScript custom actions run inside of the Windows Installer engine - in technical terms, the MSI engine is the scripting host for all scripts running as a custom action.
The scripting host program can make available objects to the scripts if it so desires. I've written applications that host the scripting engine, so this is familiar to me. In the case of MSI custom actions, the Session object is an example of this. When you run a script from the command line or by double-clicking it, it is hosted by the "Windows Scripting Host" (WSH), and it kindly supplies the WScript object. The main reason these objects are provided is so the script can interact with the host environment. In the case of WSH, you usually need to interact with the user since it is being run from Explorer or the console, hence the "Echo" method. In the MSI world, you are encouraged to interact with the MSI engine and let the MSI engine interact with the user based on the appropriate UILevel settings - this is all handled for you if you use the Session.Message API's to interact with the user or to write log files.
To summarize, if you have a script that contains WScript.something, it will not work in an MSI, as the WScript object does not exist and you cannot create it.
An unrelated issue (but people think it is related...) is the "WScript.Shell" object. You can create the WScript.Shell object (in JScript foo = new ActiveXObject("WScript.Shell")) and use it. In this case, the full name of the object you are creating is merely WScript.Shell by coincidence, and you are not calling a method on the WScript object, as you are in a line similar to WScript.Echo("foo"). An interesting related aside here is an article detailing the differences between CreateObject and WScript.CreateObject.
Danger, Will Robinson! Some antivirus programs and other script blockers will either silently block or loudly proclaim attempts to instantiate the WScript.Shell and/or the Scripting.FileSystemObject. These are commonly used by malware since they can access the registry and filesystem. Write a C++ dll custom action if you need to interact with the system in this way.
A fairly common question posted to the Windows Installer boards is "How do I sleep?" or otherwise delay the script. While I don't have an answer you want to hear, there is an excellent article about the subject here. BTW, the answer you don't want to hear is to write a DLL custom action that merely sleeps, add it to the CustomAction table, and in your CA script call Session.DoAction()
Sunday, June 06, 2004
Custom actions that should be standard actions
On the MSI front, Aaron Stebner from Microsoft posted a list of Common custom actions that should be standard actions. It is a fairly good list. Please check the link and add to the list!
Aaron is quite correct that writing a solid (read best practices compliant) custom action is difficult. Number 3 on his list is "Create user accounts." If you need to do this, make sure to check the Platform SDK, as well written sample code for this action is included.
I'm going to head over and add my two cents to the list...
Thursday, June 03, 2004
VBScript (and Jscript) MSI Custom Actions (don't have to) suck
One of the greatest things about blogging and its popularity (especially at Microsoft) is that you can get solid information and advice straight from the horse's mouth. In the Windows Installer world, the sole blogger at Microsoft (that I am aware of) is Rob Mensching. A recent post by him entitled VBScript (and Jscript) MSI Custom Actions suck was one that struck some discord with me.
Script custom actions do not have to "suck," contrary to what Rob points out. I prefer to use the KISS concept. There are several concepts that ANY developer, REGARDLESS of using script or C++ Custom Actions must understand.
- Debugability - If this is a real word, it would mean the ability of an individual to debug the Custom Action. In C++, use of DebugBreak() after attaching the debugger to the process is a simple way. In script, I have not found any way of attaching a debugger to a Custom Action (if you know of a way, that would be a great feedback point). In either case, it is difficult, if not impossible, to debug a Custom Action gone bad in the field. Turning on logging, with judicious use of Session.Message is one tactic regardless of Custom Action language. As an aside, I don't understand why the MSI does not allow the author to specify some minimum level of logging.
- Understand your Lowest Common Denominator (LCD) - If you are attempting to instantiate an ADO object on an otherwise vanilla NT4 machine, you deserve what you get - a failure. If the application you are trying to install requires something, make sure it is already installed via a LaunchCondition, or that it is going to be installed. If you depend on it in a Custom Action, make sure the install action takes place BEFORE you attempt to use it. In the C++ world, a missing dynamic link library such as MFC could get you into trouble...
- Use the right language for the job - Lets say you ignore Rob's advice and make a script custom action to use ADO. Some ADO objects take variables by reference. VBScript supports this, JScript does not. This is no different from the debate on which language you used to code your application. You can code yourself into a corner using the "easier" languages, but on the other hand C++ can sometimes be overkill.
- Other Considerations - With the plethora of third party software and networking techniques in use today, a developer must be fluent in new technologies. It is uncommon for a system to have no virus/worm protection. Products such as Symantec's Norton Antivirus have script blocking technologies to block against certain objects being instantiated in script that are typically used in worms and viruses. Instantiating the FileSystemObject will likely fail on systems with this type of protection. If your script is dependent on actions or objects outside the Session or Installer object, I would suggest using only C++ CA's to ensure the installation does not suffer in the future. Additionally, some firewalls prevent applications from accessing the internet (even XP SP2). If your setup uses or relies on these capabilities, be aware of the target audience and its possible setup.
Rob's assertion that a robust script Custom Action cannot be written is indefensible. I can (and surely have) written unrobust C++ Custom Actions. The beauty of software development is you can be as robust as you want to be, no matter the language. You, as the Custom Action author, are responsible for providing a meaningful error messages that can direct the user to the appropriate action to get the installation to work correctly.
To play Devil's Advocate, one point against script custom actions he didn't make include obscurity. Since MSI is easily decompileable, and the scripts can easily be viewed, edited, and replaced, it is likely there are some things you would like your competition to devote more time to reverse-engineering.
InstallShield provides its own scripting engine that can be used in Custom Actions. Although there is additional debugability and no antivirus blocking concerns, be aware that the size of the MSI increases (it has to install the scripting engine), and you are locked into InstallShield until the action is rewritten. I was able to quickly migrate almost everything from Wise for Windows Installer to InstallShield because I stuck to the MSI standards and ignored Wise's custom functionality. Better yet, I can move back if Wise leaps in front of InstallShield.