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.
I do not think you need to code fancy IPC stuff for this. Just exchange the information via a temporary file.
Child installer code:
[Files]
Source: InnoCallback.dll; Flags: dontcopy
[Code]
type
TTimerProc = procedure(H: LongWord; Msg: LongWord; IdEvent: LongWord; Time: LongWord);
function SetTimer(
Wnd: LongWord; IDEvent, Elapse: LongWord; TimerFunc: LongWord): LongWord;
external 'SetTimer@user32.dll stdcall';
function WrapTimerProc(Callback: TTimerProc; ParamCount: Integer): LongWord;
external 'wrapcallback@files:innocallback.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, WrapTimerProc(@ReportProgressProc, 4));
end;
end;
Master installer code:
#define ChildInstaller "mysetup.exe"
[Files]
Source: {#ChildInstaller}; Flags: dontcopy
Source: InnoCallback.dll; Flags: dontcopy
[Code]
type
TTimerProc = procedure(H: LongWord; Msg: LongWord; IdEvent: LongWord; Time: LongWord);
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';
function WrapTimerProc(Callback: TTimerProc; ParamCount: Integer): LongWord;
external 'wrapcallback@files:innocallback.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, WrapTimerProc(@UpdateProgressProc, 4));
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.
Code is using InnoTools InnoCallback library for scheduling timers.
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):
If you need to use a standalone progress bar, you can use the following master installer code:
#define ChildInstaller "mysetup.exe"
[Files]
Source: {#ChildInstaller}; Flags: dontcopy
Source: InnoCallback.dll; Flags: dontcopy
[Code]
type
TTimerProc = procedure(H: LongWord; Msg: LongWord; IdEvent: LongWord; Time: LongWord);
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';
function WrapTimerProc(Callback: TTimerProc; ParamCount: Integer): LongWord;
external 'wrapcallback@files:innocallback.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, WrapTimerProc(@UpdateProgressProc, 4));
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;