ISWIX, LLC View Christopher Painter's profile on LinkedIn profile for Christopher Painter at Stack Overflow, Q&A for professional and enthusiast programmers

Thursday, February 19, 2009

MSI Tip: How to Reuse a CustomAction for Deferred and Rollback

Neil Sleightholm recently asked how to tell if your in a rollback custom action using C#/DTF. The answer is simple.

Let's suppose we want to export one custom action method and make it multipurposed. In other words, if we call into while deferred execution we should do one thing but if we call into it for rollback execution we should do another.

How would we do that? The answer is in the BOOL MsiGetMode( __in MSIHANDLE hInstall, __in MSIRUNMODE iRunMode) Windows Installer function exposed by the Session.GetMode( InstallRunMode installRunMode ) DTF method.

Consider the following WiX Code:

    <Binary Id="CustomActionModeTest"
            SourceFile="PATH_TO_DLL.CA.dll "
    </Binary>
 
    <CustomAction Id="RollbackCA"
                  BinaryKey="CustomActionModeTest"
                  DllEntry="CustomAction1"
                  Execute="rollback"
                  Impersonate="yes">
    </CustomAction>
    <CustomAction Id="DeferredCA"
                  BinaryKey="CustomActionModeTest"
                  DllEntry="CustomAction1"
                  Execute="deferred"
                  Impersonate="yes">
    </CustomAction>
 
    <InstallExecuteSequence>
      <Custom Action="RollbackCA" 
              After="InstallInitialize">
      </Custom>
      <Custom Action="DeferredCA"
              After="RollbackCA">
      </Custom>
    </InstallExecuteSequence>



You'll notice we have 2 custom actions pointing to the same exported function in the binary table. Also notice that one custom action is scheduled as deferred and the other rollback. The deferred is scheduled right after the rollback. This results in Windows Installer scheduling RollbackCA after InstallInitialize and DeferredCA after RollbackCA.

Now let's look at the code inside of that Custom Action.

using System;
using System.Windows.Forms;
using Microsoft.Deployment.WindowsInstaller;
 
namespace CustomActionModeTest
{
    public class CustomActions
    {
        [CustomAction]
        public static ActionResult CustomAction1(Session session)
        {
            ActionResult result = ActionResult.Success;
 
            if (session.GetMode(InstallRunMode.Scheduled))
            {
                MessageBox.Show("I'm not in rollback, let's cause one to occur!");
                result = ActionResult.Failure;
            }
            
            if (session.GetMode(InstallRunMode.Rollback))
            {
                MessageBox.Show("We are now in a rollback." );
            }
 
            
            return result;
        }
    }
}


At runtime, RollbackCA is skipped and DeferredCA is executed. The CustomAction1 method will be executed by the DeferredCA and it will detect that it's scheduled in deferred mode and display a message saying that it's not in rollback and that it'll cause one. It does so by returning a failure to Windows Installer. Now MSI starts walking the script backwards executing any rollback CA it finds. This causes it to run RollbackCA and once again we are inside of the CustomAction1 method.

This time we will evaluate that we are in rollback, display a message stating so and quit gracefully. At this point the product is not installed.

This pattern can be extended so that CustomAction1 also handles immediate execution, implicit scheduling of the deferred custom actions and CustomActionData serialization/deserialization. But I'll leave that for another blog.

Sunday, February 15, 2009

MSI Tip: Authoring an ICE using C# / DTF

I once wrote an article for InstallShield entitled "MSI Tip: Authoring a Custom ICE using InstallShield 2008". The article can be found here. [Warning: PDF]
In the article, I demonstrated how to write a unit test that could find evil script custom actions. While there is value and humor in doing so, the real point of the article was to demonstrate how InstallShield's refactored InstallScript language could be used to write ICEs.

While InstallScript is certainly an improvement over C++ in terms of cutting down on the line noise and complexity, the difference between C++/InstallScript and C# is nothing short of amazing. As such, I've created a sample project demonstrating how to author ICE's using C#/DTF and Stefan Krueger of InstallSite.org is kind enough to host it for me here.

Consider the following snippet from an MSDN sample demonstrating how to write an ICE in C++:


#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <strsafe.h>
#include <MsiQuery.h>
 
///////////////////////////////////////////////////////////
// ICE01 - simple ICE that does not test anything
UINT __stdcall ICE01(MSIHANDLE hInstall)
{
// setup the record to describe owner and date created
PMSIHANDLE hRecCreated = ::MsiCreateRecord(1);
::MsiRecordSetString(hRecCreated, 0, TEXT("ICE01\t3\tCreated 04/29/1998 by <insert author's name here>"));
 
// post the owner message
::MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER), hRecCreated); 
// setup the record to describe the last time the ICE was modified
::MsiRecordSetString(hRecCreated, 0, TEXT("ICE01\t3\tLast modified 05/06/1998 by <insert author's name here>"));
 
// post the last modification message
::MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER), hRecCreated);
 
// setup the record to describe what the ICE evaluates
::MsiRecordSetString(hRecCreated, 0, TEXT("ICE01\t3\tSimple ICE illustrating the ICE concept"));
 
// post the description of evaluation message
::MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER), hRecCreated);
// time value to be sent on
TCHAR szValue[200];
DWORD cchValue = sizeof(szValue)/sizeof(TCHAR);
 
// try to get the time of this call
if (MsiGetProperty(hInstall, TEXT("Time"), szValue, &cchValue) != ERROR_SUCCESS)
StringCchCopy(szValue,  sizeof("(none)")/sizeof(TCHAR)+1, TEXT("none"));// no time available
 
// setup the record to be sent as a message
PMSIHANDLE hRecTime = ::MsiCreateRecord(2);
::MsiRecordSetString(hRecTime, 0, TEXT("ICE01\t3\tCalled at [1]."));
::MsiRecordSetString(hRecTime, 1, szValue);
 
// send the time
::MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER), hRecTime);
 
return ERROR_SUCCESS; // allows other ICEs will continue
}

With DTF and a little OOP ( Object Oriented Programming ) this can be reduced to the following snippet in C# / DTF:

using DE.TestFramework;
using Microsoft.Deployment.WindowsInstaller;
 
namespace DE.Tests
{
    partial class ICE01 : TestBase
    {
        [ICETest]
        public void TestExample()
        {
            Publish( ICELevel.Information, "Created 04/29/1998 by <insert author's name here>");
            Publish( ICELevel.Information, "Last modified 05/06/1998 by <insert author's name here>");
            Publish( ICELevel.Information, "Simple ICE illustrating the ICE concept");
            Publish( ICELevel.Information, "Called at " + Session["Time"] );
        }
    }
}
 
That may not seem like a big deal but consider the flexibity a few overloads can provide:

public void Publish(ICELevel iceLevel, string Description)
public void Publish(ICELevel iceLevel, string Description, string HelpLocation )
public void Publish(ICELevel iceLevel, string Description, string HelpLocation, string Table, string Column, string PrimaryKey)
public void Publish(ICELevel iceLevel, string Description, string HelpLocation, string Table, string Column, string[] PrimaryKeys)

Of course the real power comes from DTF's awesome interop classes. Going back to example the of detecting script custom actions in InstallScript, here is what it would look like in C# / DTF:

using DE.TestFramework;
using Microsoft.Deployment.WindowsInstaller;
using System;
 
namespace DE.Tests
{
    partial class ICE_DE_10
    {
        [ICETest]
        public void TestForScriptCustomActionsAreEvil()
        {
            Publish(ICELevel.Information, "Searching for Evil Script Custom Actions...");
 
            using (View view = Session.Database.OpenView("SELECT `CustomAction`.`Action`, `CustomAction`.`Type` FROM `CustomAction`"))
            {
                view.Execute();
                foreach (var record in view)
                {
                    string customActionName = record.GetString("Action");
                    int type = record.GetInteger("Type") & 0x00000007;
                    CustomActionTypes customActionType = (CustomActionTypes)type;
 
 
                    if (customActionType.Equals(CustomActionTypes.VBScript) || customActionType.Equals(CustomActionTypes.JScript))
                    {
                        Publish(ICELevel.Error, 
                            "Found Evil " + customActionType.ToString() + " Custom Action " + customActionName,
                            "http://blogs.msdn.com/robmen/archive/2004/05/20/136530.aspx",
                            "CustomAction",
                            "Action",
                            customActionName ); 
                    }
                    else
                    {
                        Publish(ICELevel.Information, "Found Nice Custom Action: " + customActionName + " Type: " + customActionType.ToString());
                    }
                }
            }
        }
    }
}

Thursday, January 22, 2009

IS 2009 Bug - Patch Optimization & Custom CAB Names Mutually Exclusive

I realize this is a very unique scenario, but I thought I would post it in case you find yourself in this scenario. The bug only applies if you use InstallShield 2009 to build a ‘Basic MSI’ installation and you use ALL of the build options specified below:
  • You specify custom CAB names (by editing the ISFeatureCabName column of the custom InstallShield Feature table).
  • You specify the “One .cab per Feature” option on the Custom Compression Settings dialog of the Release Wizard.
  • You sync file IDs with a previous build using the “Previous package” option on the Advanced Settings dialog of the Release Wizard.

If you do build under the scenario described above, then you should be aware that the options appear to be mutually exclusive and the custom CAB names you specified will NOT be generated correctly by InstallShield at build time.

Below is a description of the problem I reported to Acresso support yesterday (incident #SIOA-000140045):


I have a serious problem with the IsCmdBld.exe in InstallShield 2009 Premier that I need a fix for ASAP.

We are in the process of upgrading our build environment to use IS 2009 to build our primary application installation and are seeing different behavior during the build between the IsSABld.exe we used with previous versions of IS (both IS 12 Premier and IS 2008 Premier) and the behavior seen using IsCmdBld.exe in IS 2009 Premier.

Our primary application installation is a very complex and large MSI installation. Since we must control the names of the CAB files we generate at build time, we use the InstallShield Direct Editor to modify the Feature table in the InstallShield source project and specify a different CAB name for each defined feature (using the ISFeatureCabName field). Using the Release Wizard, we also set the Release Configuration to 'Custom' and select the "One .cab per Feature" option ("Custom Compression Settings" dialog of the Release Wizard). In addition, we specify the fully qualified path and name of a previous MSI package build (by setting the "Previous package" option on the "Advanced Settings" dialog of the Release Wizard. The only other configuration information that may be relevant to this issue is the fact that we have multiple merge modules in the project and each defined feature has one or more merge modules associated with it (no merge module is associated with more than one feature).

When we build the installation (with NO CHANGES other than upgrading the project to IS 2009 schema format) using the IsCmdBld.exe command line utility from IS 2009 the CAB files are not generated correctly. In other words, the files associated with feature A are not being placed into the CAB file specified in the ISFeatureCabName for feature A. However, if I remove the option to point the build to a previous package and then build the project then the CAB files are generated correctly (the files associated with feature A are being correctly placed into the CAB file specified in the ISFeatureCabName for feature A.

In other words, the options to specify the CAB file name for each feature (ISFeatureCabName column of the Feature table) and the Release Wizard option for Patch optimization ("Previous package" option) ARE MUTUALLY EXCLUSIVE IN IS 2009.

I have confirmed this behavior by building under the different scenarios described below:

  • Build A: IS 12 project file, one .cab per feature enabled, custom CAB names specified in Feature table, previous package path specified, and IsSABld.exe used to build the project.
  • Build B: IS 12 project file, one .cab per feature enabled, custom CAB names specified in Feature table, previous package path NOT specified, and IsSABld.exe used to build the project.
  • Build C: IS 2008 project file, one .cab per feature enabled, custom CAB names specified in Feature table, previous package path specified, and IsSABld.exe used to build the project.
  • Build D: IS 2008 project file, one .cab per feature enabled, custom CAB names specified in Feature table, previous package path NOT specified, and IsSABld.exe used to build the project.
  • Build E: IS 2009 project file, one .cab per feature enabled, custom CAB names specified in Feature table, previous package path specified, and IsCmdBld.exe used to build the project.
  • Build F: IS 2009 project file, one .cab per feature enabled, custom CAB names specified in Feature table, previous package path NOT specified, and IsCmdBld.exe used to build the project.

In all scenarios specified above EXCEPT "Build E", the CAB files are correctly generated during the build. In other words, in each of the scenarios EXCEPT "Build E", the files associated with feature A are correctly placed into the CAB file whose name is specified in the ISFeatureCabName column of the Feature table.It is absolutely critical that this be fixed or we cannot use IS 2009 without completely redesigning our MSI installation packaging.

Friday, January 16, 2009

Please, Just One More Miracle

I know it's been awhile since I posted an update. I've been incredibly swamped by life and my new work. I sit here this morning reading about United 1549 which went down in the Hudson river. Miraculously, no one perished.

I mention this because I'm praying for one more miracle. Cheryl had her quarterly CT scan on Monday and in a couple of hours we'll visit her oncologist to see if she remains no evidence of disease.

[Update]

The CT Scan looks perfect! Thank You God!!

Monday, December 29, 2008

Windows Installer 10 Year Anniversary

I'm sorry that I haven't posted in awhile. Frankly I'm starting to wonder if there is actually anything new to ever talk about in the MSI world. After all, it's an aging platform SDK that has changed very little over the last 10 years. ( For example, here's a thread between Stefan Krueger (InstallSite had long been around) and Heath Stewart in what seems to be his pre-Microsoft days.)

(Interesting aside: In the above thread, Heath says he's had "Great Success" with InstallShield. Later in September 2000 you'll find threads where's promoting Wise and bashing InstallShield saying that it "sucks". Hmmm... anyways, I remember trying the InstallShield for Windows Installer back in the summer of 1999 and thinking both ISWI and MSI in general sucked. I would continue to use InstallShield 5 for the next 4 years! )

Back to my original thought: Some day soon will mark the official 10 year anniversary of Windows Installer. The problem is, I'm not sure what date exactly. I suppose it'll be Jan 27, 2009 since Wikipedia says this is the ship date of Office 2000. I was searching the usenet archives and I came across the very first thread to ever refer to windows installer on this date 10 years ago. I think you'll find it a fitting:


From: "Tom Savage"
Subject: Office 2000 install error 1601!
Date: 1998/12/30
Message-ID: #1/1
X-Deja-AN: 427080025
X-MimeOLE: Produced By Microsoft MimeOLE V4.72.3110.3
Newsgroups: microsoft.public.msdn.general

Can't install the msdn version of Office 2000 on two test machines. Install
updated the Windows installer and asked me to reboot. Now it stops almost
immediately with an "install error 1601".
Could it be related to my previous installation of Frontpage 2000 B1
(removed it before the current install, but uninstall mentioned a registry
write error)? I removed all Frontpage references I could find from the
registry, and it didn't help.
Would really appreciate any help you could give me on this one. I had grown
very comfortable with Frontpage2000 despite its quirks.
thanks,
ts

From: "Steven Toney"
Subject: Re: Office 2000 install error 1601!
Date: 1998/12/30
Message-ID: <#soTrZEN#GA.255@uppssnewspub04.moswest.msn.net>#1/1
X-Deja-AN: 427260501
References:
X-Priority: 3
X-MimeOLE: Produced By Microsoft MimeOLE V5.00.0810.800
X-MSMail-Priority: Normal
Newsgroups: microsoft.public.msdn.general

I think the new windows installer sucks!!

much to busy ,, difficult to tell plainly what is being installed and what
is not

install programs should be simple with clear choices and feedback on what
will be installed or removed.

this new program is none of these

steve

From: "Tom Savage"
Subject: Re: Office 2000 install error 1601!
Date: 1998/12/31
Message-ID: #1/1
X-Deja-AN: 427449318
References:
X-Priority: 3
X-MimeOLE: Produced By Microsoft MimeOLE V5.00.0810.800
X-MSMail-Priority: Normal
Reply-To: "Tom Savage"
Newsgroups: microsoft.public.msdn.general

_Lucky for me_ I found that c:\winnt\system32\msi.dll had not
self-registered during my repeated installations and uninstallations. I
registered it and the installation worked fine.
Well... almost fine. Now I have to get the Office2000 server extensions SQL
Server database to work right. "Version incompatibilities prevent
communication with the database". I'm using a hopelessly out-of-date and
incompatible SQL Server (v6.5 SP3)

Saturday, December 13, 2008

The Day The Earth Stood Still Comments

I took yesterday off from work to hang out with my wife. After sending our kids off to school, dropping by the doctors office for some blood work and eating breakfast at Kerbey Lane Cafe, we decided that we could squeeze a movie in before having to pick Emma up from school.

We decided to see The Day The Earth Stood still since it was starting at 11am and the previews looked good. Unfortunately I hadn't picked up on how disappointing the movie was until we were sitting in the theater.

I have several issues with the politics of the movie, and I found the following comment on the Internet that says it better then I can:

Spoilers… (Or “Savers” if you don’t want to waste $10)

The aliens arrive in the usual idiotic fashion that makes the Conquistadors arrival in the new world look like a well planned diplomatic mission. The response by the Americans is predictable and right out of the Hollywood cliche’ handbook of STOOPID Americanism. The military’s lack of common sense is matched only by the oblivious idiocy of the enlightened Klatu who only realizes that killing 6 billion people is a bad idea when he sees a child crying about his dead daddy. Than the whole movie wraps up with an unexplained sudden ending as Klatu turns off all the lights and leaves. So now what? Are we left in the dark ages forever? Is the blackout a temporary measure to teach us a lesson or can we look forward to a Mad Max style sequel where humanity splits up into tribes and starts killing each other while they all starve to death in famine in a new enlightened more green style of misery?

Avoid this turd. Word of mouth should kill it off in a matter of weeks. But on the bright side the Wolverine preview looked pretty cool.

-Michael

On the bright side, I actually had decent service at Kerbey Lane Cafe for a change. :-) Usually I have such a horrible experience. I really don't understand what Austinites see in that restaurant.

Tuesday, November 18, 2008

Back with old friends

From time to time my stomach tells me that it's time for a change. This recently happened again ( darn build/install guys always keep getting hit by a bus... ) and on November 17Th, 2008 I left MultiMedia Games and returned to Overwatch Textron Systems, Tactical Operations to be with old and new friends.

So after having both the TFS/BuildForge role and the InstallShield/MSI role for the last couple years, I've put my build engineering hat back in the closet and settled in to focus on the installation side of the house.

Our group is growing like crazy! So, if you live in the Austin, TX area and you are an upstanding and unquestionably loyal US Citizen, then please look us up. We are hiring all the time.