Embedded CMD in Inno Setup installer (show command output on a custom page)

后端 未结 1 1055
南笙
南笙 2020-12-20 01:34

I created an Input page that executes a command line app using the created variables from those inputs. Naturally, the cmd window pop ups on my screen. I would

相关标签:
1条回答
  • 2020-12-20 02:09

    You can redirect the command output to a file and monitor the file for changes, loading them to list box (or maybe a memo box).

    var
      ProgressPage: TOutputProgressWizardPage;
      ProgressListBox: TNewListBox;
    
    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;
    
    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 BotonIniciarOnClick(Sender: TObject);
    var
      ResultCode: Integer;
      Timer: LongWord;
      AppPath: string;
      AppError: string;
      Command: string;
    begin
      ProgressPage :=
        CreateOutputProgressPage(
          'Installing something', 'Please wait until this 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));
    
        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]);
        if not Exec(ExpandConstant('{cmd}'), '/c ' + Command, '', 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;
    


    Above was tested with a batch file like:

    @echo off
    echo Starting
    echo Doing A...
    echo Extracting something...
    echo Doing B...
    echo Extracting something...
    timeout /t 1 > nul
    echo Doing C...
    echo Extracting something...
    echo Doing D...
    echo Extracting something...
    timeout /t 1 > nul
    echo Doing E...
    echo Extracting something...
    echo Doing F...
    echo Extracting something...
    timeout /t 1 > nul
    ...
    

    If you want to display the output as part of the installation process, instead of on a button click, see:
    Execute a batch file after installation and display its output on a custom page before Finished page in Inno Setup

    0 讨论(0)
提交回复
热议问题