visual-studiovisual-studio-2008windows-installersetup-projectcustom-action

Run exe after msi installation?


Using Visual Studio 2008 to create an msi to deploy my program with a setup project. I need to know how to make the msi run the exe it just installed. A custom action? If so please explain where/how. Thanks.


Solution

  • This is a common question. I don't do it with just a custom action. The only way I know, is to modify the .msi after it has been generated. I run a Javascript script as a post-build event to do exactly that. It inserts a new dialog in the installer wizard, with a checkbox that says "Launch Application Foo?". And then there is a custom action to run the app, if the checkbox is checked.

    It appears as the last screen in the install Wizard sequence. Looks like this:

    alt text


    This is the script I use to modify the MSI:

    // EnableLaunchApplication.js <msi-file>
    // Performs a post-build fixup of an msi to launch a specific file when the install has completed
    
    // Configurable values
    var checkboxChecked = true;                     // Is the checkbox on the finished dialog checked by default?
    var checkboxText = "Launch [ProductName]";      // Text for the checkbox on the finished dialog
    var filename = "WindowsApplication1.exe";       // The name of the executable to launch - change this to match the file you want to launch at the end of your setup
    
    // Constant values from Windows Installer
    var msiOpenDatabaseModeTransact = 1;
    
    var msiViewModifyInsert         = 1;
    var msiViewModifyUpdate         = 2;
    var msiViewModifyAssign         = 3;
    var msiViewModifyReplace        = 4;
    var msiViewModifyDelete         = 6;
    
    if (WScript.Arguments.Length != 1)
    {
            WScript.StdErr.WriteLine(WScript.ScriptName + " file");
            WScript.Quit(1);
    }
    
    var filespec = WScript.Arguments(0);
    var installer = WScript.CreateObject("WindowsInstaller.Installer");
    var database = installer.OpenDatabase(filespec, msiOpenDatabaseModeTransact);
    
    var sql;
    var view;
    var record;
    
    try
    {
            var fileId = FindFileIdentifier(database, filename);
            if (!fileId)
                    throw "Unable to find '" + filename + "' in File table";
    
            WScript.Echo("Updating the Control table...");
            // Modify the Control_Next of BannerBmp control to point to the new CheckBox
            sql = "SELECT `Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help` FROM `Control` WHERE `Dialog_`='FinishedForm' AND `Control`='BannerBmp'";
            view = database.OpenView(sql);
            view.Execute();
            record = view.Fetch();
            record.StringData(11) = "CheckboxLaunch";
            view.Modify(msiViewModifyReplace, record);
            view.Close();
    
            // Insert the new CheckBox control
            sql = "INSERT INTO `Control` (`Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help`) VALUES ('FinishedForm', 'CheckboxLaunch', 'CheckBox', '9', '201', '343', '12', '3', 'LAUNCHAPP', '{\\VSI_MS_Sans_Serif13.0_0_0}" + checkboxText + "', 'CloseButton', '|')";
            view = database.OpenView(sql);
            view.Execute();
            view.Close();
    
            WScript.Echo("Updating the ControlEvent table...");
            // Modify the Order of the EndDialog event of the FinishedForm to 1
            sql = "SELECT `Dialog_`, `Control_`, `Event`, `Argument`, `Condition`, `Ordering` FROM `ControlEvent` WHERE `Dialog_`='FinishedForm' AND `Event`='EndDialog'";
            view = database.OpenView(sql);
            view.Execute();
            record = view.Fetch();
            record.IntegerData(6) = 1;
            view.Modify(msiViewModifyReplace, record);
            view.Close();
    
            // Insert the Event to launch the application
            sql = "INSERT INTO `ControlEvent` (`Dialog_`, `Control_`, `Event`, `Argument`, `Condition`, `Ordering`) VALUES ('FinishedForm', 'CloseButton', 'DoAction', 'VSDCA_Launch', 'LAUNCHAPP=1', '0')";
            view = database.OpenView(sql);
            view.Execute();
            view.Close();
    
            WScript.Echo("Updating the CustomAction table...");
            // Insert the custom action to launch the application when finished
            sql = "INSERT INTO `CustomAction` (`Action`, `Type`, `Source`, `Target`) VALUES ('VSDCA_Launch', '210', '" + fileId + "', '')";
            view = database.OpenView(sql);
            view.Execute();
            view.Close();
    
            if (checkboxChecked)
            {
                    WScript.Echo("Updating the Property table...");
                    // Set the default value of the CheckBox
                    sql = "INSERT INTO `Property` (`Property`, `Value`) VALUES ('LAUNCHAPP', '1')";
                    view = database.OpenView(sql);
                    view.Execute();
                    view.Close();
            }
    
            database.Commit();
    }
    catch(e)
    {
            WScript.StdErr.WriteLine(e);
            WScript.Quit(1);
    }
    
    function FindFileIdentifier(database, fileName)
    {
            // First, try to find the exact file name
            var sql = "SELECT `File` FROM `File` WHERE `FileName`='" + fileName + "'";
            var view = database.OpenView(sql);
            view.Execute();
            var record = view.Fetch();
            if (record)
            {
                    var value = record.StringData(1);
                    view.Close();
                    return value;
            }
            view.Close();
    
            // The file may be in SFN|LFN format.  Look for a filename in this case next
            sql = "SELECT `File`, `FileName` FROM `File`";
            view = database.OpenView(sql);
            view.Execute();
            record = view.Fetch();
            while (record)
            {
                    if (StringEndsWith(record.StringData(2), "|" + fileName))
                    {
                            var value = record.StringData(1);
                            view.Close();
                            return value;
                    }
    
                    record = view.Fetch();
            }
            view.Close();
    }
    
    function StringEndsWith(str, value)
    {
            if (str.length < value.length)
                    return false;
    
            return (str.indexOf(value, str.length - value.length) != -1);
    }
    

    I originally got this from Aaron Stebner's blog, and then modified it.

    Save that Javascript file to the project directory (same dir as contains .vdproj), name it ModifyMsiToEnableLaunchApplication.js . For each unique setup project, you need to modify that script and put the proper exe name into it. And then, you need to set the post-build event in the Setup project to be this:

    cscript.exe "$(ProjectDir)ModifyMsiToEnableLaunchApplication.js" "$(BuiltOuputPath)"
    

    Be sure to type the name of the macro $(BuiltOuputPath) correctly. The word Ouput is misspelled by Microsoft, and Built is not spelled Build !

    That oughtta do it.

    See also: this modification which does not include the "run Foo.exe" checkbox on UNINSTALL.