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.
问题:
回答1:
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:
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.
回答2:
This seems to be a MUCH simpler solution: Visual Studio Installer > How To Launch App at End of Installer
回答3:
OK!!! Here is the code (without the 2 auxiliary functions \'FindFileIdentifier\' & \'StringEndsWith\' in the end - use the original ones instead) which gives us the ability to change the Ys and Heights of the controls, along with adding the Checkbox Control Visibility Conditions (see the 2 comments that are marked between \'NEW - START\' to \'NEW - END\'):
// EnableLaunchApplication.js
// 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 = \"*.exe\"; // The name of the executable to launch - change * to match the file name 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();
// NEW - START
// Insert the new CheckBox control
// I changed the value for Y below from 201 to 191 in order to make the checkbox more obvious to the user\'s eye. In order to do so, and avoid the controls \'BodyText\' & \'BodyTextRemove\' in the same form to
// overlap the checkbox, I added yet 2 more sql statements that change the values of the heights for the \'BodyText\' & \'BodyTextRemove\' from 138 to 128. This way I can play around with the values without using
// the Orca msi editor.
var CheckBoxY = 191; //This was initially set to 201
var NewHeight = 128; //This was initially set to 138
sql = \"INSERT INTO `Control` (`Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help`) VALUES (\'FinishedForm\', \'CheckboxLaunch\', \'CheckBox\', \'9\', \'\" + CheckBoxY + \"\', \'343\', \'12\', \'3\', \'LAUNCHAPP\', \'{\\\\VSI_MS_Sans_Serif13.0_0_0}\" + checkboxText + \"\', \'CloseButton\', \'|\')\";
view = database.OpenView(sql);
view.Execute();
view.Close();
sql = \"Select `Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help` FROM `Control` WHERE `Dialog_` = \'FinishedForm\' AND `Control` = \'BodyText\'\";
view = database.OpenView(sql);
view.Execute();
record = view.Fetch();
record.IntegerData(7) = NewHeight;
view.Modify(msiViewModifyReplace, record);
view.Close();
sql = \"Select `Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help` FROM `Control` WHERE `Dialog_` = \'FinishedForm\' AND `Control` = \'BodyTextRemove\'\";
view = database.OpenView(sql);
view.Execute();
record = view.Fetch();
record.IntegerData(7) = NewHeight;
view.Modify(msiViewModifyReplace, record);
view.Close();
// NEW - END
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();
}
// NEW - START
WScript.Echo(\"Updating the ControlCondition table...\");
// Insert the conditions where the Launch Application Checkbox appears
sql = \"INSERT INTO `ControlCondition` (`Dialog_`, `Control_`, `Action`, `Condition`) VALUES (\'FinishedForm\', \'CheckboxLaunch\', \'Show\', \'REMOVE=\\\"\\\"\')\";
view = database.OpenView(sql);
view.Execute();
view.Close();
sql = \"INSERT INTO `ControlCondition` (`Dialog_`, `Control_`, `Action`, `Condition`) VALUES (\'FinishedForm\', \'CheckboxLaunch\', \'Hide\', \'REMOVE<>\\\"\\\"\')\";
view = database.OpenView(sql);
view.Execute();
view.Close();
//NEW - END
database.Commit();
}
catch(e)
{
WScript.StdErr.WriteLine(e);
WScript.Quit(1);
}
回答4:
Concerning the \"hidden checkbox bug\" I figured out the following which is not explained by Cheeso\'s and Muleskinner\'s answers above:
The change of the script (provided by Muleskinner) places the Y position of the checkbox to 201 (I guess top Y pixel for the control). If you change Y to, say, 151 (in order to kind of align it in the center vertically), the bug \"suddenly\" appears. The reason for that is that there is another control in the Control table of the msi, namely the \'BodyText\' (\'Dialog\' field = \'FinishedForm\') which its Y is set to 63 and its height to 138. That is 138 + 63 = 201. Therefore, if you change the Y value for the checkbox, the \'BodyText\' control overlaps the newly added control and that\'s why the user needs to put their mouse over in order to show the checkbox. If you have no \'BodyText\' or its number of characters is small enough you could change (by using Orca msi editor as I do, or by altering the script above) the Ys and Heights of these 2 controls in order to be able and accomodate a different Y position for the newly added checkbox. The same applies for the control: \'BodyTextRemove\' in which again we should alter its height value (which appears during uninstall)
Hope that this helps all the users that had the same question as I had about this \"bug\"
Nevertheless, the script does a really good job!
Another question was how to make invisible the Checkbox during unistallation procedure. Using the Orca msi editor I added the following 2 rows in the ControlCondition table of the msi:
Row 1 (When control should be shown):
(Dialog)FinishedForm (Control)CheckboxLaunch (Action)Show (Condition)REMOVE=\"\"
Row 2 (When control should be invisible):
(Dialog)FinishedForm (Control)CheckboxLaunch (Action)Hide (Condition)REMOVE<>\"\"
P.S. I am using VS 2010, on windows 7 (x64), but I believe these should work with previous versions too.
回答5:
Regarding the \'PostBuildEvent\' failed with error code \'1\' \'Unspecified error\' error, change the PostBuildEvent from
cscript.exe \\\"$(ProjectDir)ModifyMsiToEnableLaunchApplication.js\\\" \\\"$(BuiltOuputPath)\\\"
to
cscript.exe \"$(ProjectDir)ModifyMsiToEnableLaunchApplication.js\" \"$(BuiltOuputPath)\"
Regarding the hidden checkbox bug you can edit line 54 of the script to become:
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\', \'|\')\";
回答6:
Yes.. I would write a custom action, and stick it at the end of the InstallExecutionSequence table