Reputation: 23
I am creating a setup package using Inno Setup to run my cmd script. Using the articles available on Stack Overflow, I have managed to create a custom page that displays the output of the script on the setup wizard. However, during the script execution phase, I can't move the wizard window and can't even click on it. Here is the link that I used to create the custom page.
Embedded CMD in Inno Setup installer (show command output on a custom page)
Now, I want two things:
For your reference, I am using following code in iss file.
[Code]
// Add next button on ready to install page.
procedure CurPageChanged(CurPageID: Integer);
begin
// On fresh install the last pre-install page is "Select Program Group".
// On upgrade the last pre-install page is "Read to Install"
// (forced even with DisableReadyPage)
// 1. Change text on Finish page
WizardForm.FinishedLabel.Caption := 'Setup has finished running the ResetWU script on this computer. Please restart your computer and check for Windows updates again. Click Finish to exit the setup wizard.';
WizardForm.FinishedHeadingLabel.Caption := 'Finishing the ResetWU script wizard';
// 2. Change button caption on ready-to-install and Finish pages.
if (CurPageID = wpSelectProgramGroup) or (CurPageID = wpReady) then
WizardForm.NextButton.Caption := SetupMessage(msgButtonNext)
// On the Finished page, use "Finish" caption.
else if (CurPageID = wpFinished) then
WizardForm.NextButton.Caption := SetupMessage(msgButtonFinish)
// On all other pages, use "Next" caption.
else
WizardForm.NextButton.Caption := SetupMessage(msgButtonNext);
end;
// Embed installation bat into installation progress page
var
ProgressPage: TOutputProgressWizardPage;
ProgressListBox: TNewListBox;
function SetTimer(
Wnd: LongWord; IDEvent, Elapse: LongWord; TimerFunc: LongWord): LongWord;
external '[email protected] stdcall';
function KillTimer(hWnd: LongWord; uIDEvent: LongWord): BOOL;
external '[email protected] stdcall';
var
ProgressFileName: string;
function BufferToAnsi(const Buffer: string): AnsiString;
var
W: Word;
I: Integer;
begin
SetLength(Result, Length(Buffer) * 2);
for I := 1 to Length(Buffer) do
begin
W := Ord(Buffer[I]);
Result[(I * 2)] := Chr(W shr 8); { high byte }
Result[(I * 2) - 1] := Chr(Byte(W)); { low byte }
end;
end;
procedure UpdateProgress;
var
S: AnsiString;
I, L, Max: Integer;
Buffer: string;
Stream: TFileStream;
Lines: TStringList;
begin
if not FileExists(ProgressFileName) then
begin
Log(Format('Progress file %s does not exist', [ProgressFileName]));
end
else
begin
try
{ Need shared read as the output file is locked for writting, }
{ so we cannot use LoadStringFromFile }
Stream := TFileStream.Create(ProgressFileName, fmOpenRead or fmShareDenyNone);
try
L := Stream.Size;
Max := 100*2014;
if L > Max then
begin
Stream.Position := L - Max;
L := Max;
end;
SetLength(Buffer, (L div 2) + (L mod 2));
Stream.ReadBuffer(Buffer, L);
S := BufferToAnsi(Buffer);
finally
Stream.Free;
end;
except
Log(Format('Failed to read progress from file %s - %s', [
ProgressFileName, GetExceptionMessage]));
end;
end;
if S <> '' then
begin
Log('Progress len = ' + IntToStr(Length(S)));
Lines := TStringList.Create();
Lines.Text := S;
for I := 0 to Lines.Count - 1 do
begin
if I < ProgressListBox.Items.Count then
begin
ProgressListBox.Items[I] := Lines[I];
end
else
begin
ProgressListBox.Items.Add(Lines[I]);
end
end;
ProgressListBox.ItemIndex := ProgressListBox.Items.Count - 1;
ProgressListBox.Selected[ProgressListBox.ItemIndex] := False;
Lines.Free;
end;
{ Just to pump a Windows message queue (maybe not be needed) }
ProgressPage.SetProgress(0, 1);
end;
procedure UpdateProgressProc(
H: LongWord; Msg: LongWord; Event: LongWord; Time: LongWord);
begin
UpdateProgress;
end;
procedure RunInstallBatInsideProgress;
var
ResultCode: Integer;
Timer: LongWord;
AppPath: string;
AppError: string;
Command: string;
begin
ProgressPage :=
CreateOutputProgressPage(
'Running ResetWU Script', 'Please wait until the script finishes...');
ProgressPage.Show();
ProgressListBox := TNewListBox.Create(WizardForm);
ProgressListBox.Parent := ProgressPage.Surface;
ProgressListBox.Top := 0;
ProgressListBox.Left := 0;
ProgressListBox.Width := ProgressPage.SurfaceWidth;
ProgressListBox.Height := ProgressPage.SurfaceHeight;
{ Fake SetProgress call in UpdateProgressProc will show it, }
{ make sure that user won't see it }
{ProgressPage.ProgressBar.Top := -100;}
try
Timer := SetTimer(0, 0, 250, CreateCallback(@UpdateProgressProc));
AppPath := ExpandConstant('C:\temp\myscript.cmd');
ProgressFileName := ExpandConstant('C:\Temp\install-progress.log');
Log(Format('Expecting progress in %s', [ProgressFileName]));
Command := Format('""%s" > "%s""', [AppPath, ProgressFileName]);
if not Exec(ExpandConstant('{cmd}'), '/c ' + Command + ExpandConstant(' {app}\PIPE'), '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
begin
AppError := 'Cannot start app';
end
else
if ResultCode <> 0 then
begin
AppError := Format('App failed with code %d', [ResultCode]);
end;
UpdateProgress;
finally
{ Clean up }
KillTimer(0, Timer);
ProgressPage.Hide;
DeleteFile(ProgressFileName);
ProgressPage.Free();
end;
if AppError <> '' then
begin
{ RaiseException does not work properly while TOutputProgressWizardPage is shown }
RaiseException(AppError);
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep=ssPostInstall then
begin
RunInstallBatInsideProgress;
end
end;
// Add button to custom progress page.
function GetWindowLong(Wnd: HWnd; Index: Integer): LongInt;
external '[email protected] stdcall';
function SetWindowLong(Wnd: HWnd; Index: Integer; NewLong: LongInt): LongInt;
external '[email protected] stdcall';
const
GWL_STYLE = -16;
BS_MULTILINE = $2000;
procedure StopButtonOnClick(Sender: TObject);
begin
MsgBox('This is the stop message!', mbInformation, mb_Ok);
end;
procedure InitializeWizard();
var
Button: TNewButton;
begin
Button := TNewButton.Create(WizardForm);
Button.Left := WizardForm.ClientWidth - WizardForm.CancelButton.Left - WizardForm.CancelButton.Width;
Button.Top := WizardForm.NextButton.Top;
Button.Width := WizardForm.NextButton.Width;
Button.Height := WizardForm.NextButton.Height;
Button.Parent := WizardForm.InnerPage;
SetWindowLong(Button.Handle, GWL_STYLE,
GetWindowLong(Button.Handle, GWL_STYLE) or BS_MULTILINE);
Button.Caption := 'Stop';
Button.OnClick := @StopButtonOnClick;
end;
I have tried looking for the jrsoftware documentation, but I am quite new to this and can't understand it properly.
Upvotes: 1
Views: 424
Reputation: 202534
If you do not want to block the UI while running application (and if you want to be able to manipulate with [kill] the application), you cannot run it using the blocking Exec
function.
Instead, you can use ShellExecuteEx
API to run the application and then use WaitForSingleObject
to query its status. The below code is stripped down version of Inno Setup Get progress from .NET Framework 4.5 (or higher) installer to update progress bar position.
const
WAIT_OBJECT_0 = 0;
WAIT_TIMEOUT = $00000102;
SEE_MASK_NOCLOSEPROCESS = $00000040;
function WaitForSingleObject(
Handle: THandle; Milliseconds: Cardinal): Cardinal;
external '[email protected] stdcall';
type
TShellExecuteInfo = record
cbSize: DWORD;
fMask: Cardinal;
Wnd: HWND;
lpVerb: string;
lpFile: string;
lpParameters: string;
lpDirectory: string;
nShow: Integer;
hInstApp: THandle;
lpIDList: DWORD;
lpClass: string;
hkeyClass: THandle;
dwHotKey: DWORD;
hMonitor: THandle;
hProcess: THandle;
end;
function ShellExecuteEx(var lpExecInfo: TShellExecuteInfo): BOOL;
external '[email protected] stdcall';
function TerminateProcess(Process: THandle; ExitCode: Cardinal): Boolean;
external '[email protected] stdcall';
var
ExecInfo: TShellExecuteInfo;
procedure ExecWithoutBlocking;
var
ProgressPage: TOutputProgressWizardPage;
R: Cardinal;
Completed: Boolean;
AppError: string;
begin
// Start the installer using ShellExecuteEx to get process ID
ExecInfo.cbSize := SizeOf(ExecInfo);
ExecInfo.fMask := SEE_MASK_NOCLOSEPROCESS;
ExecInfo.Wnd := 0;
ExecInfo.lpFile := 'notepad.exe';
ExecInfo.lpParameters := '';
ExecInfo.nShow := SW_SHOW;
if not ShellExecuteEx(ExecInfo) then
RaiseException('Cannot start');
Log(Format('Program started as process %x', [ExecInfo.hProcess]));
ProgressPage := CreateOutputProgressPage('Running program', '');
ProgressPage.SetProgress(0, 100);
ProgressPage.Show;
try
Completed := False;
while not Completed do
begin
// Check if the process has finished already
R := WaitForSingleObject(ExecInfo.hProcess, 0);
if R = WAIT_OBJECT_0 then
begin
Log('Program completed');
Completed := True;
end
else
if R <> WAIT_TIMEOUT then
begin
AppError := 'Error waiting for program to complete';
Completed := True;
end;
// Seemingly pointless as progress did not change,
// but it pumps a message queue as a side effect
ProgressPage.SetProgress(0, 100);
Sleep(100);
end;
finally
ProgressPage.Hide;
end;
if AppError <> '' then
begin
// RaiseException does not work properly
// while TOutputProgressWizardPage is shown
RaiseException(AppError);
end;
end;
The ShellExecuteEx
gives you a process ID, which you can use to stop/kill the process using TerminateProcess
:
procedure StopButtonOnClick(Sender: TObject);
begin
TerminateProcess(ExecInfo.hProcess, 0);
end;
If you want to combine it with output redirection to a custom page, you can do:
procedure AbortButtonClick(Sender: TObject);
begin
TerminateProcess(ExecInfo.hProcess, 0);
end;
procedure RunInstallBatInsideProgress;
var
ResultCode: Integer;
AppPath: string;
Command: string;
Completed: Boolean;
R: Integer;
AppError: string;
AbortButton: TButton;
begin
ProgressPage :=
CreateOutputProgressPage(
'Running ResetWU Script', 'Please wait until the script finishes...');
ProgressPage.Show();
AbortButton := TButton.Create(WizardForm);
AbortButton.Parent := ProgressPage.Surface;
AbortButton.Caption := 'Abort';
AbortButton.Top := ProgressPage.SurfaceHeight - AbortButton.Height - ScaleY(8);
AbortButton.Left := 0;
AbortButton.OnClick := @AbortButtonClick;
ProgressListBox := TNewListBox.Create(WizardForm);
ProgressListBox.Parent := ProgressPage.Surface;
ProgressListBox.Top := 0;
ProgressListBox.Left := 0;
ProgressListBox.Width := ProgressPage.SurfaceWidth;
ProgressListBox.Height := AbortButton.Top - ScaleY(8);
// Fake SetProgress call in UpdateProgress will show it,
// make sure that user won't see it
ProgressPage.ProgressBar.Top := -100;
try
ExtractTemporaryFile('install.bat');
AppPath := ExpandConstant('{tmp}\install.bat');
ProgressFileName := ExpandConstant('{tmp}\progress.txt');
Log(Format('Expecting progress in %s', [ProgressFileName]));
Command := Format('""%s" > "%s""', [AppPath, ProgressFileName]);
// Start the installer using ShellExecuteEx to get process ID
ExecInfo.cbSize := SizeOf(ExecInfo);
ExecInfo.fMask := SEE_MASK_NOCLOSEPROCESS;
ExecInfo.Wnd := 0;
ExecInfo.lpFile := ExpandConstant('{cmd}');
ExecInfo.lpParameters := '/c ' + Command;
ExecInfo.nShow := SW_HIDE;
if not ShellExecuteEx(ExecInfo) then
RaiseException('Cannot start');
Log(Format('Program started as process %x', [ExecInfo.hProcess]));
Completed := False;
while not Completed do
begin
// Check if the process has finished already
R := WaitForSingleObject(ExecInfo.hProcess, 0);
if R = WAIT_OBJECT_0 then
begin
Log('Program completed');
Completed := True;
end
else
if R <> WAIT_TIMEOUT then
begin
AppError := 'Error waiting for program to complete';
Completed := True;
end;
UpdateProgress;
Sleep(100);
end;
UpdateProgress;
finally
ProgressPage.Hide;
DeleteFile(ProgressFileName);
ProgressPage.Free();
end;
if AppError <> '' then
begin
// RaiseException does not work properly
// while TOutputProgressWizardPage is shown
RaiseException(AppError);
end;
end;
UpdateProgress
can be called from the while not Completed do
loop.Upvotes: -1