16

I have some commands in the [Run] section of my Inno Setup script. Right now, if any of them returns a failure code (non-zero return value), the setup continues without any warning to the user. The desired behavior is to have the setup fail and roll back.

How do I enable this? I couldn't find any flag for the Run entry that would force this behavior. Am I missing something?

Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
dso
  • 9,463
  • 10
  • 53
  • 59
  • Similar question: http://stackoverflow.com/questions/582452/msi-return-codes-in-inno-setup – mghie Jul 15 '09 at 20:26
  • See also [Using Process Exit code to show error message for a specific File in \[Run\]](http://stackoverflow.com/q/9621099/850848) and [Inno Setup: How to Abort/Terminate Setup During Install?](http://stackoverflow.com/q/6345920/850848) – Martin Prikryl May 11 '17 at 13:11

4 Answers4

12

As far as I'm concerned, you have to use [Code] section for that, run the files with Exec function, check ResultCode upon return and run your uninstall script.

Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
GSerg
  • 76,472
  • 17
  • 159
  • 346
7

I did it this way:

  1. Write error message (either abort confirmation message or just notification message) to temporary file {tmp}\install.error using Inno Setup's BeforeInstall parameter with SaveStringToUTF8File procedure. You can use Inno Setup's constants, such as {cm:YourCustomMessage}.

  2. Use Windows command shell cmd.exe /s /c to run desired program. Also use conditional execution of del command with && - https://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/ntcmds_shelloverview.mspx. So error message file would be deleted if command succeed (exit code 0). Please be aware of special quotes handling in cmd.exe /s /c. Use code below as example.

  3. Check existence of error message file {tmp}\install.error using Inno Setup's AfterInstall parameter with either ConfirmInstallAbortOnError or NotifyInstallAbortOnError procedures depending on error severity. They will abort install with proper notification or confirmation (and optional presenting of log file) and perform rollback using Exec(ExpandConstant('{uninstallexe}'), ...

  4. ShouldAbortInstallation global variable is used to keep status. Inno Setup's ShouldSkipPage(PageID: Integer) function is used to hide final page. All commands in [Run] section should use Check parameter with CheckInstallationIsNotAborted function. It will prevent their execution after failure at some point.

See example below. Hope this helps.

[CustomMessages]
InstallAbortOnErrorConfirmationMessage=An error has occurred during setup.%nAbort installation?
InstallAbortOnErrorNotificationMessage=An error has occurred during setup.%nInstallation will be aborted.
RunProgram1ErrorMsg=Post installation phase 1 failed. Should abort install?
RunProgram2ErrorMsg=Post installation phase 2 failed. Installation will be aborted. Please, contact tech support.
RunProgram1StatusMsg=Post installation phase 1 is in progress
RunProgram2StatusMsg=Post installation phase 2 is in progress

[Run]
; Write error text to file. Delete file on succeed. Abort installation if file exists after command execution.
Filename: "cmd.exe"; Parameters: "/s /c "" ""{app}\program1.exe"" /param1 /param2:""val2"" && del /F /Q ""{tmp}\install.error"" """; \
  WorkingDir:"{app}"; Flags: runhidden; \
  BeforeInstall: SaveStringToUTF8File('{tmp}\install.error', '{cm:RunProgram1ErrorMsg}', False); \
  AfterInstall: ConfirmInstallAbortOnError('{tmp}\install.error', '{app}\logs\setup.log'); \
  StatusMsg: "{cm:RunProgram1StatusMsg}"; \
  Check: CheckInstallationIsNotAborted;
Filename: "cmd.exe"; Parameters: "/s /c "" ""{app}\program2.exe"" && del /F /Q ""{tmp}\install.error"" """; \
  WorkingDir:"{app}"; Flags: runhidden; \
  BeforeInstall: SaveStringToUTF8File('{tmp}\install.error', '{cm:RunProgram2ErrorMsg}', False); \
  AfterInstall: NotifyInstallAbortOnError('{tmp}\install.error', '{app}\logs\setup.log'); \
  StatusMsg: "{cm:RunProgram2StatusMsg}"; \
  Check: CheckInstallationIsNotAborted;
[Code]
var
  ShouldAbortInstallation: Boolean;

procedure SaveStringToUTF8File(const FileName, Content: String; const Append: Boolean);
var
  Text: array [1..1] of String;
begin
  Text[1] := Content;
  SaveStringsToUTF8File(ExpandConstant(FileName), Text, Append);
end;

function LoadAndConcatStringsFromFile(const FileName: String): String;
var
  Strings: TArrayOfString;
  i: Integer;
begin
  LoadStringsFromFile(FileName, Strings);
  Result := '';
  if High(Strings) >= Low(Strings) then
    Result := Strings[Low(Strings)];
  for i := Low(Strings) + 1 to High(Strings) do
    if Length(Strings[i]) > 0 then
      Result := Result + #13#10 + Strings[i];
end;

procedure ConfirmInstallAbortOnError(ErrorMessageFile, LogFileToShow: String);
var
  ErrorCode: Integer;
  ErrorMessage: String;
begin
  ErrorMessageFile := ExpandConstant(ErrorMessageFile);
  LogFileToShow := ExpandConstant(LogFileToShow);

  Log('ConfirmInstallAbortOnError is examining file: ' + ErrorMessageFile);
  if FileExists(ErrorMessageFile) then
  begin
    Log('ConfirmInstallAbortOnError: error file exists');

    { Show log file to the user }
    if Length(LogFileToShow) > 0 then
      ShellExec('', LogFileToShow, '', '', SW_SHOW, ewNoWait, ErrorCode);

    ErrorMessage := LoadAndConcatStringsFromFile(ErrorMessageFile);
    if Length(ErrorMessage) = 0 then
      ErrorMessage := '{cm:InstallAbortOnErrorConfirmationMessage}';
    if MsgBox(ExpandConstant(ErrorMessage), mbConfirmation, MB_YESNO) = IDYES then
    begin
      Log('ConfirmInstallAbortOnError: should abort');
      ShouldAbortInstallation := True;
      WizardForm.Hide;
      MainForm.Hide;
      Exec(ExpandConstant('{uninstallexe}'), '/SILENT', '', SW_HIDE,
           ewWaitUntilTerminated, ErrorCode);
      MainForm.Close;
    end;
  end;
  Log('ConfirmInstallAbortOnError finish');
end;

procedure NotifyInstallAbortOnError(ErrorMessageFile, LogFileToShow: String);
var
  ErrorCode: Integer;
  ErrorMessage: String;
begin
  ErrorMessageFile := ExpandConstant(ErrorMessageFile);
  LogFileToShow := ExpandConstant(LogFileToShow);

  Log('NotifyInstallAbortOnError is examining file: ' + ErrorMessageFile);
  if FileExists(ErrorMessageFile) then
  begin
    Log('NotifyInstallAbortOnError: error file exists');

    { Show log file to the user }
    if Length(LogFileToShow) > 0 then
      ShellExec('', LogFileToShow, '', '', SW_SHOW, ewNoWait, ErrorCode);

    ErrorMessage := LoadAndConcatStringsFromFile(ErrorMessageFile);
    if Length(ErrorMessage) = 0 then
      ErrorMessage := '{cm:InstallAbortOnErrorNotificationMessage}';

    MsgBox(ExpandConstant(ErrorMessage), mbError, MB_OK);
    Log('NotifyInstallAbortOnError: should abort');
    ShouldAbortInstallation := True;
    WizardForm.Hide;
    MainForm.Hide;
    Exec(ExpandConstant('{uninstallexe}'), '/SILENT', '', SW_HIDE,
         ewWaitUntilTerminated, ErrorCode);
    MainForm.Close;
  end;
  Log('NotifyInstallAbortOnError finish');
end;

function ShouldSkipPage(PageID: Integer): Boolean;
begin
  Result := ShouldAbortInstallation;
end;

function CheckInstallationIsNotAborted(): Boolean;
begin
  Result := not ShouldAbortInstallation;
end;
Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
Ivan Samygin
  • 4,210
  • 1
  • 20
  • 33
2

The [Run] section happens after installation is complete, so there's no rollback possible at that point, because it's already finalized.

However, what you can do is use AfterInstall in the [Files] section, after your .exe or whatever is required to execute your method. This runs before finalizing the installation, so canceling at this point does a rollback that removes all files.

If you combine that with the "CancelWithoutPrompt" from this answer you can do a rollback when running in interactive mode. Unfortunately, there doesn't seem to be a rollback for silent mode.

[Files]
Source: src\myapp.exe; DestDir: "{app}"; AfterInstall: RunMyAppCheck

[Code]
var CancelWithoutPrompt: boolean;

function InitializeSetup(): Boolean;
begin
  CancelWithoutPrompt := false;
  result := true;
end;

procedure CancelButtonClick(CurPageID: Integer; var Cancel, Confirm: Boolean);
begin
  if CancelWithoutPrompt then
    Confirm := false; { hide confirmation prompt }
end;

procedure RunMyAppCheck();
var
  resultCode: Integer;
begin
  Exec(ExpandConstant('{app}\myapp.exe'), '--verify --example-other-params',
    '', SW_HIDE, ewWaitUntilTerminated, resultCode);
  
  if resultCode <> 0 then
  begin
    SuppressibleMsgBox(
      'MyApp failed, exit code ' + IntToStr(resultCode) + '. ' + 
        'Aborting installation.',
      mbCriticalError, MB_OK, 0);
  
    CancelWithoutPrompt := true;
    WizardForm.Close;
  end;
end;
Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
gregmac
  • 24,276
  • 10
  • 87
  • 118
0

You can use the AfterInstall flag in the Run section to trigger the execution of your program and catch the result code.

See my answer here.

Then according to the result code you can cancel the installation.

Community
  • 1
  • 1
Jason
  • 920
  • 8
  • 19
  • *Then according to the result code you can cancel the installation.* How ? – TLama Jul 15 '15 at 10:04
  • One comment mentioned this answer: http://stackoverflow.com/a/12849863/672870 It is not a roll back though. – Jason Jul 15 '15 at 11:13
  • So you cannot cancel the installation (with rollback), can you ? – TLama Jul 15 '15 at 11:16
  • 2
    Correct, there is no rollback mechanism at this stage of the installation. The work around is to trigger the uninstaller binary after warning the user the script failed. It is still better that the default behaviour of failing silently and pretend everything went fine. – Jason Jul 15 '15 at 11:22