installationprogress-barinno-setupipc

Make Inno Setup Installer report its installation progress status to master installer


I currently have two Inno Setup installers working around. I need one of them to report its status as a sub installer to another installer even it running with VERYSILENT command.

I need this to display a progress bar in my main installer according to sub installer's installation progress because I don't want any infinite (marquee) progress bars for this.

I also read about IPC Mechanism in Delphi. How can I add this communication abilities like pumps to Inno Setup source code? Any tips for starting?

Thanks in advance.


Solution

  • I do not think you need to code fancy IPC stuff for this. Just exchange the information via a temporary file.

    Child installer code:

    [Code]
    
    function SetTimer(
      Wnd: LongWord; IDEvent, Elapse: LongWord; TimerFunc: LongWord): LongWord;
      external 'SetTimer@user32.dll stdcall';
    
    var
      ProgressFileName: string;
      PrevProgress: Integer;
    
    procedure ReportProgressProc(
      H: LongWord; Msg: LongWord; Event: LongWord; Time: LongWord);
    var
      Progress: Integer;
    begin
      try
        Progress :=
          (WizardForm.ProgressGauge.Position * 100) div WizardForm.ProgressGauge.Max;
        if PrevProgress <> Progress then
        begin
          if not SaveStringToFile(ProgressFileName, IntToStr(Progress), False) then
          begin
            Log(Format('Failed to save progress %d', [Progress]));
          end
            else
          begin
            Log(Format('Saved progress %d', [Progress]));
            PrevProgress := Progress;
          end;
        end;
      except
        Log('Exception saving progress');
      end;
    end;  
    
    procedure InitializeWizard();
    begin
      // When run with /progress=<path> switch, will report progress to that file
      ProgressFileName := ExpandConstant('{param:progress}');
      if ProgressFileName <> '' then
      begin
        Log(Format('Will write progress to: %s', [ProgressFileName]));
        PrevProgress := -1;
        SetTimer(0, 0, 250, CreateCallback(@ReportProgressProc));
      end;
    end;
    

    Master installer code:

    #define ChildInstaller "mysetup.exe"
    
    [Files]
    Source: {#ChildInstaller}; Flags: dontcopy
    
    [Code]
    
    function SetTimer(
      Wnd: LongWord; IDEvent, Elapse: LongWord; TimerFunc: LongWord): LongWord;
      external 'SetTimer@user32.dll stdcall';
    function KillTimer(hWnd: LongWord; uIDEvent: LongWord): BOOL;
      external 'KillTimer@user32.dll stdcall';
    
    var
      ProgressPage: TOutputProgressWizardPage;
      ProgressFileName: string;
    
    procedure UpdateProgressProc(
      H: LongWord; Msg: LongWord; Event: LongWord; Time: LongWord);
    var
      S: AnsiString;
      Progress: Integer;
    begin
      try
        if not LoadStringFromFile(ProgressFileName, S) then
        begin
          Log(Format('Failed to read progress from file %s', [ProgressFileName]));
        end
          else
        begin
          Progress := StrToIntDef(S, -1);
          if (Progress < 0) or (Progress > 100) then
          begin
            Log(Format('Read invalid progress %s', [S]));
          end
            else
          begin
            Log(Format('Read progress %d', [Progress]));
            ProgressPage.SetProgress(Progress, 100);
          end;
        end;
      except
        Log('Exception updating progress');
      end;
    end;
    
    procedure InstallChild;
    var
      ChildInstallerPath: string;
      ChildInstallerParams: string;
      Timer: LongWord;
      InstallError: string;
      ResultCode: Integer;
    begin
      ExtractTemporaryFile('{#ChildInstaller}');
      
      ProgressPage := CreateOutputProgressPage('Running child installer', '');
      ProgressPage.SetProgress(0, 100);
      ProgressPage.Show;
      try
        Timer := SetTimer(0, 0, 250, CreateCallback(@UpdateProgressProc));
    
        ChildInstallerPath := ExpandConstant('{tmp}\{#ChildInstaller}');
        ProgressFileName := ExpandConstant('{tmp}\progress.txt');
        Log(Format('Expecting progress in %s', [ProgressFileName]));
        ChildInstallerParams :=
          Format('/verysilent /progress="%s"', [ProgressFileName]);
        if not Exec(ChildInstallerPath, ChildInstallerParams, '', SW_SHOW,
                    ewWaitUntilTerminated, ResultCode) then
        begin
          InstallError := 'Cannot start child installer';
        end
          else
        if ResultCode <> 0 then
        begin
          InstallError :=
            Format('Child installer failed with code %d', [ResultCode]);
        end;
      finally
        // Clean up
        KillTimer(0, Timer);
        ProgressPage.Hide;
        DeleteFile(ProgressFileName);
      end;
    
      if InstallError <> '' then
      begin 
        // RaiseException does not work properly,
        // while TOutputProgressWizardPage is shown
        RaiseException(InstallError);
      end;
    end;
    

    You can use the InstallChild like below, or on any other place of your installer process:

    function NextButtonClick(CurPageID: Integer): Boolean;
    begin
      Result := True;
    
      if CurPageID = wpReady then
      begin
        try
          InstallChild;
        except
          MsgBox(GetExceptionMessage, mbError, MB_OK);
          Result := False;
        end;
      end;
    end;
    

    Another good solution would be to use the PrepareToInstall event function. For an example see my answer to Inno Setup torrent download implementation.


    For CreateCallback function, you need Inno Setup 6. If you are stuck with Inno Setup 5, you can use WrapCallback function from InnoTools InnoCallback library.


    It might be better to use the TFileStream instead of the LoadStringFromFile and the SaveStringToFile. The TFileStream supports read sharing. With the LoadStringFromFile and the SaveStringToFile, the progress reporting may occasionally temporarily fail, if both sides happen to try to read and write at the same time.

    See Inno Setup LoadStringFromFile fails when file is open in another process.


    This shows how the child and master installer progresses are linked (if the child installer is not running with the /verysilent switch, but with the /silent only):

    Linked progress


    If you want to display the progress on the primary progress bar of the main installer, you can use the following master installer code:

    #define ChildInstaller "mysetup.exe"
    
    [Files]
    Source: {#ChildInstaller}; Flags: dontcopy
    
    [Code]
    
    function SetTimer(
      Wnd: LongWord; IDEvent, Elapse: LongWord; TimerFunc: LongWord): LongWord;
      external 'SetTimer@user32.dll stdcall';
    function KillTimer(hWnd: LongWord; uIDEvent: LongWord): BOOL;
      external 'KillTimer@user32.dll stdcall';
    
    var
      ProgressFileName: string;
    
    procedure UpdateProgressProc(
      H: LongWord; Msg: LongWord; Event: LongWord; Time: LongWord);
    var
      S: AnsiString;
      Progress: Integer;
    begin
      try
        if not LoadStringFromFile(ProgressFileName, S) then
        begin
          Log(Format('Failed to read progress from file %s', [ProgressFileName]));
        end
          else
        begin
          Progress := StrToIntDef(S, -1);
          if (Progress < 0) or (Progress > 100) then
          begin
            Log(Format('Read invalid progress %s', [S]));
          end
            else
          begin
            Log(Format('Read progress %d', [Progress]));
            WizardForm.ProgressGauge.Position :=
              Progress * WizardForm.ProgressGauge.Max div 100;
          end;
        end;
      except
        Log('Exception updating progress');
      end;
    end;
    
    procedure InstallChild;
    var
      ChildInstallerPath: string;
      ChildInstallerParams: string;
      Timer: LongWord;
      ResultCode: Integer;
    begin
      ExtractTemporaryFile('{#ChildInstaller}');
    
      try
        Timer := SetTimer(0, 0, 250, CreateCallback(@UpdateProgressProc));
    
        ChildInstallerPath := ExpandConstant('{tmp}\{#ChildInstaller}');
        ProgressFileName := ExpandConstant('{tmp}\progress.txt');
        Log(Format('Expecting progress in %s', [ProgressFileName]));
        ChildInstallerParams :=
          Format('/verysilent /progress="%s"', [ProgressFileName]);
        if not Exec(ChildInstallerPath, ChildInstallerParams, '', SW_SHOW,
                    ewWaitUntilTerminated, ResultCode) then
        begin
          MsgBox('Cannot start child installer', mbError, MB_OK);
        end
          else
        if ResultCode <> 0 then
        begin
          MsgBox(Format(
            'Child installer failed with code %d', [ResultCode]), mbError, MB_OK);
        end;
      finally
        // Clean up
        KillTimer(0, Timer);
        DeleteFile(ProgressFileName);
      end;
    end;
    
    procedure CurStepChanged(CurStep: TSetupStep);
    begin
      if CurStep = ssInstall then
      begin
        InstallChild;
      end;
    end;