Inno Setup 从 .NET Framework 4.5(或更高版本)安装程序获取进度以更新进度条位置

Inno Setup Get progress from .NET Framework 4.5 (or higher) installer to update progress bar position

我目前正在安装 .NET Framework 4.6.2 作为 PrepareToInstall 事件函数的先决条件,以便我可以获得退出代码、设置 NeedsReboot 状态,或者在安装失败时中止.我的代码在下面,一切正常。

var
  PrepareToInstallLabel: TNewStaticText;
  PrepareToInstallProgressBar: TNewProgressBar;
  intDotNetResultCode: Integer;
  CancelWithoutPrompt, AbortInstall: Boolean;

function InitializeSetup(): Boolean;
begin
  Result := True;
  OverwriteDB := False;
  CancelWithoutPrompt := False;
  AbortInstall := False;
end;

function PrepareToInstall(var NeedsRestart: Boolean): String;
var
  intResultCode: Integer;
  strInstallType: String;
begin
  if not IsDotNet45Installed and IsWindows7Sp1OrAbove then
    begin
      HidePrepareToInstallGuiControls;
      PrepareToInstallLabel.Caption := 'Installing Microsoft .NET Framework 4.6.2...';
      ShowPrepareToInstallGuiControls;
      ExtractTemporaryFile('NDP462-KB3151800-x86-x64-AllOS-ENU.exe');
      if WizardSilent = True then
        begin
          strInstallType := '/q';
        end
      else
        begin
          strInstallType := '/passive';
        end;
      Exec(ExpandConstant('{tmp}\NDP462-KB3151800-x86-x64-AllOS-ENU.exe'), strInstallType + ' /norestart', '', SW_SHOW,
        ewWaitUntilTerminated, intDotNetResultCode);
      if (intDotNetResultCode = 0) or (intDotNetResultCode = 1641) or (intDotNetResultCode = 3010) then 
        begin
          Log('Microsoft .NET Framework 4.6.2 installed successfully.' + #13#10 + 'Exit Code: ' + IntToStr(intDotNetResultCode));
          CancelWithoutPrompt := False;
          AbortInstall := False;
        end
      else
        begin
          if WizardSilent = True then
            begin
              Log('Microsoft .NET Framework 4.6.2 failed to install.' + #13#10 + 'Exit Code: ' + IntToStr(intDotNetResultCode) + #13#10 + 'Setup aborted.');
            end
          else
            begin
              MsgBox('Microsoft .NET Framework 4.6.2 failed to install.' + #13#10 + #13#10 +
                'Exit Code: ' + IntToStr(intDotNetResultCode) + #13#10 + #13#10 +
                'Setup aborted. Click Next or Cancel to exit, or Back to try again.',
                mbCriticalError, MB_OK);
            end;
          PrepareToInstallProgressBar.Visible := False;
          PrepareToInstallLabel.Caption := 'Microsoft .NET Framework 4.6.2 failed to install.' + #13#10 + #13#10 + 'Exit Code: ' + IntToStr(intDotNetResultCode) + #13#10 + #13#10 + 'Setup aborted. Click Next or Cancel to exit, or Back to try again.';
          CancelWithoutPrompt := True;
          AbortInstall := True;
          Abort;
        end;
    end;
end;

procedure InitializeWizard();
begin
//Define the label for the Preparing to Install page
  PrepareToInstallLabel := TNewStaticText.Create(WizardForm);
  with PrepareToInstallLabel do
    begin
      Visible := False;
      Parent := WizardForm.PreparingPage;
      Left := WizardForm.StatusLabel.Left;
      Top := WizardForm.StatusLabel.Top;
    end;
//Define Progress Bar for the Preparing to Install Page
  PrepareToInstallProgressBar := TNewProgressBar.Create(WizardForm);
  with PrepareToInstallProgressBar do
    begin
      Visible := False;
      Parent := WizardForm.PreparingPage;
      Left := WizardForm.ProgressGauge.Left;
      Top := WizardForm.ProgressGauge.Top;
      Width := WizardForm.ProgressGauge.Width;
      Height := WizardForm.ProgressGauge.Height;
      PrepareToInstallProgressBar.Style := npbstMarquee;
    end;
end;

procedure CurStepChanged(CurStep: TSetupStep);
begin
  if CurStep = ssInstall then
    begin
      if AbortInstall = True then
        begin
          Abort;
        end;
    end;
end;

目前,我使用 /q/passive 将安装类型设置为静默或无人值守,以控制 .NET Framework 安装程序显示的可见 GUI 数量,具体取决于 Inno设置为 运行 并使用 Marquee 风格的进度条来指示正在发生的事情。但是,从 Microsoft 文档 here 看来,可以使用 /pipe 开关让 .NET Framework 安装程序报告其安装进度,这可能允许它以交互方式更新正常的实际进度上的样式进度条。这意味着 .NET Framework 安装程序可以完全隐藏,Inno Setup 用于指示相对进度,这是一个更简洁的解决方案。不幸的是,我不懂 C++,只是一个新手程序员。因此,任何人都可以确认这是否可以通过 Inno Setup 进行,如果可以,如何尝试?

以下显示了来自
的代码的 Pascal Script 实现 How to: Get Progress from the .NET Framework 4.5 Installer

[Files]
Source: "NDP462-KB3151800-x86-x64-AllOS-ENU.exe"; Flags: dontcopy

[Code]

// Change to unique names
const
  SectionName = 'MyProgSetup';
  EventName = 'MyProgSetupEvent';

const
  INFINITE = 65535;
  WAIT_OBJECT_0 = 0;
  WAIT_TIMEOUT = [=10=]000102;
  FILE_MAP_WRITE = [=10=]02;
  E_PENDING = 00000A;
  S_OK = 0;
  MMIO_V45 = 1;
  MAX_PATH = 260;
  SEE_MASK_NOCLOSEPROCESS = [=10=]000040;
  INVALID_HANDLE_VALUE = -1;
  PAGE_READWRITE = 4;
  MMIO_SIZE = 65536;
  
type
  TMmioDataStructure = record
    DownloadFinished: Boolean; // download done yet?
    InstallFinished: Boolean; // install done yet?
    DownloadAbort: Boolean; // set downloader to abort
    InstallAbort: Boolean; // set installer to abort
    DownloadFinishedResult: Cardinal; // resultant HRESULT for download
    InstallFinishedResult: Cardinal; // resultant HRESULT for install
    InternalError: Cardinal;
    CurrentItemStep: array[0..MAX_PATH-1] of WideChar;
    DownloadSoFar: Byte; // download progress 0 - 255 (0 to 100% done)
    InstallSoFar: Byte; // install progress 0 - 255 (0 to 100% done)
    // event that chainer 'creates' and chainee 'opens'to sync communications
    EventName: array[0..MAX_PATH-1] of WideChar; 

    Version: Byte; // version of the data structure, set by chainer.
                   // 0x0 : .Net 4.0
                   // 0x1 : .Net 4.5

    // current message being sent by the chainee, 0 if no message is active
    MessageCode: Cardinal; 
    // chainer's response to current message, 0 if not yet handled
    MessageResponse: Cardinal; 
    // length of the m_messageData field in bytes
    MessageDataLength: Cardinal; 
    // variable length buffer, content depends on m_messageCode
    MessageData: array[0..MMIO_SIZE] of Byte; 
  end;

function CreateFileMapping(
  File: THandle; Attributes: Cardinal; Protect: Cardinal;
  MaximumSizeHigh: Cardinal; MaximumSizeLow: Cardinal; Name: string): THandle;
  external 'CreateFileMappingW@kernel32.dll stdcall';

function CreateEvent(
  EventAttributes: Cardinal; ManualReset: Boolean; InitialState: Boolean;
  Name: string): THandle;
  external 'CreateEventW@kernel32.dll stdcall';

function CreateMutex(
  MutexAttributes: Cardinal; InitialOwner: Boolean; Name: string): THandle;
  external 'CreateMutexW@kernel32.dll stdcall';

function WaitForSingleObject(
  Handle: THandle; Milliseconds: Cardinal): Cardinal;
  external 'WaitForSingleObject@kernel32.dll stdcall';

function MapViewOfFile(
  FileMappingObject: THandle; DesiredAccess: Cardinal; FileOffsetHigh: Cardinal;
  FileOffsetLow: Cardinal; NumberOfBytesToMap: Cardinal): Cardinal;
  external 'MapViewOfFile@kernel32.dll stdcall';

function ReleaseMutex(Mutex: THandle): Boolean;
  external 'ReleaseMutex@kernel32.dll 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 'ShellExecuteExW@shell32.dll stdcall';

function GetExitCodeProcess(Process: THandle; var ExitCode: Cardinal): Boolean;
  external 'GetExitCodeProcess@kernel32.dll stdcall';

procedure CopyPointerToData(
  var Destination: TMmioDataStructure; Source: Cardinal; Length: Cardinal);
  external 'RtlMoveMemory@kernel32.dll stdcall';

procedure CopyDataToPointer(
  Destination: Cardinal; var Source: TMmioDataStructure; Length: Cardinal);
  external 'RtlMoveMemory@kernel32.dll stdcall';

var
  FileMapping: THandle;
  EventChaineeSend: THandle;
  EventChainerSend: THandle;
  Mutex: THandle;
  Data: TMmioDataStructure;
  View: Cardinal;

procedure LockDataMutex;
var
  R: Cardinal;
begin
  R := WaitForSingleObject(Mutex, INFINITE);
  Log(Format('WaitForSingleObject = %d', [Integer(R)]));
  if R <> WAIT_OBJECT_0 then
    RaiseException('Error waiting for mutex');
end;

procedure UnlockDataMutex;
var
  R: Boolean;
begin
  R := ReleaseMutex(Mutex);
  Log(Format('ReleaseMutex = %d', [Integer(R)]));
  if not R then
    RaiseException('Error releasing waiting for mutex');
end;

procedure ReadData;
begin
  CopyPointerToData(Data, View, MMIO_SIZE);
end;

procedure WriteData;
begin
  CopyDataToPointer(View, Data, MMIO_SIZE);
end;

procedure InitializeChainer;
var
  I: Integer;
begin
  Log('Initializing chainer');  

  FileMapping :=
    CreateFileMapping(
      INVALID_HANDLE_VALUE, 0, PAGE_READWRITE, 0, MMIO_SIZE, SectionName);
  Log(Format('FileMapping = %d', [Integer(FileMapping)]));
  if FileMapping = 0 then
    RaiseException('Error creating file mapping'); 

  EventChaineeSend := CreateEvent(0, False, False, EventName);
  Log(Format('EventChaineeSend = %d', [Integer(EventChaineeSend)]));
  if EventChaineeSend = 0 then
    RaiseException('Error creating chainee event'); 

  EventChainerSend := CreateEvent(0, False, False, EventName + '_send');
  Log(Format('EventChainerSend = %d', [Integer(EventChainerSend)]));
  if EventChainerSend = 0 then
    RaiseException('Error creating chainer event'); 

  Mutex := CreateMutex(0, False, EventName + '_mutex');
  Log(Format('Mutex = %d', [Integer(Mutex)]));
  if Mutex = 0 then
    RaiseException('Error creating mutex'); 
  
  View :=
    MapViewOfFile(FileMapping, FILE_MAP_WRITE, 0, 0, 0);
  if View = 0 then
    RaiseException('Cannot map data view');
  Log('Mapped data view');

  LockDataMutex;

  ReadData;

  Log('Initializing data');  
  for I := 1 to Length(EventName) do
    Data.EventName[I - 1] := EventName[I];
  Data.EventName[Length(EventName)] := #[=10=];
  
  // Download specific data
  Data.DownloadFinished := False;
  Data.DownloadSoFar := 0;
  Data.DownloadFinishedResult := E_PENDING;
  Data.DownloadAbort := False;

  // Install specific data
  Data.InstallFinished := False;
  Data.InstallSoFar := 0;
  Data.InstallFinishedResult := E_PENDING;
  Data.InstallAbort := False;
  
  Data.InternalError := S_OK;

  Data.Version := MMIO_V45;
  Data.MessageCode := 0;
  Data.MessageResponse := 0;
  Data.MessageDataLength := 0;

  Log('Initialized data');  

  WriteData;

  UnlockDataMutex;

  Log('Initialized chainer');  
end;
  
var
  ProgressPage: TOutputProgressWizardPage;

procedure InstallNetFramework;
var
  R: Cardinal;
  ExecInfo: TShellExecuteInfo;
  ExitCode: Cardinal;
  InstallError: string;
  Completed: Boolean;
  Progress: Integer;
begin
  ExtractTemporaryFile('NDP462-KB3151800-x86-x64-AllOS-ENU.exe');
  
  // Start the installer using ShellExecuteEx to get process ID
  ExecInfo.cbSize := SizeOf(ExecInfo);
  ExecInfo.fMask := SEE_MASK_NOCLOSEPROCESS;
  ExecInfo.Wnd := 0;
  ExecInfo.lpFile :=
    ExpandConstant('{tmp}\NDP462-KB3151800-x86-x64-AllOS-ENU.exe');
  ExecInfo.lpParameters :=
    '/pipe ' + SectionName + ' /chainingpackage mysetup /q';
  ExecInfo.nShow := SW_HIDE;

  if not ShellExecuteEx(ExecInfo) then
    RaiseException('Cannot start .NET framework installer');

  Log(Format('.NET framework installer started as process %x', [
    ExecInfo.hProcess]));
  
  Progress := 0;
  ProgressPage.SetProgress(Progress, 100);
  ProgressPage.Show;
  try
    Completed := False;

    while not Completed do
    begin
      // Check if the installer process has finished already
      R := WaitForSingleObject(ExecInfo.hProcess, 0);
      if R = WAIT_OBJECT_0 then
      begin
        Log('.NET framework installer completed');
        Completed := True;
        if not GetExitCodeProcess(ExecInfo.hProcess, ExitCode) then
        begin
          InstallError := 'Cannot get .NET framework installer exit code';
        end
          else
        begin
          Log(Format('Exit code: %d', [Integer(ExitCode)]));
          if ExitCode <> 0 then
          begin
            InstallError :=
              Format('.NET framework installer failed with exit code %d', [
                ExitCode]);
          end;
        end;
      end
        else
      if R <> WAIT_TIMEOUT then
      begin
        InstallError := 'Error waiting for .NET framework installer to complete';
        Completed := True;
      end
        else
      begin
        // Check if the installer process has signaled progress event
        R := WaitForSingleObject(EventChaineeSend, 0);
        if R = WAIT_OBJECT_0 then
        begin  
          Log('Got event from the installer');
          { Read progress data }
          LockDataMutex;
          ReadData;
          Log(Format(
            'DownloadSoFar = %d, InstallSoFar = %d', [
              Data.DownloadSoFar, Data.InstallSoFar]));
          Progress := Integer(Data.InstallSoFar) * 100 div 255;
          Log(Format('Progress = %d', [Progress]));
          UnlockDataMutex;
          ProgressPage.SetProgress(Progress, 100);
        end
          else
        if R <> WAIT_TIMEOUT then
        begin
          InstallError := 'Error waiting for .NET framework installer event';
          Completed := True;
        end
          else
        begin
          // Seemingly pointless as progress did not change,
          // but it pumps a message queue as a side effect
          ProgressPage.SetProgress(Progress, 100);
          Sleep(100);
        end;
      end;
    end;
  finally
    ProgressPage.Hide;
  end;
 
  if InstallError <> '' then
  begin 
    // RaiseException does not work properly 
    // while TOutputProgressWizardPage is shown
    RaiseException(InstallError);
  end;
end;

function InitializeSetup(): Boolean;
begin
  InitializeChainer;
 
  Result := True;
end;

procedure InitializeWizard();
begin
  ProgressPage := CreateOutputProgressPage('Installing .NET framework', '');
end;

您可以像下面那样使用它,也可以在安装程序的任何其他地方使用它。

function NextButtonClick(CurPageID: Integer): Boolean;
begin
  Result := True;

  if CurPageID = wpReady then
  begin
    try
      InstallNetFramework;
    except
      MsgBox(GetExceptionMessage, mbError, MB_OK);
      Result := False;
    end;
  end;
end;

下面的截图显示了 Inno Setup 中的“进度页面”是如何链接到 .NET framework 安装程序的(当然 .NET framework 安装程序被 /q 开关隐藏了,它只是暂时显示用于获取屏幕截图)。


我已经在

上成功测试了代码
  • dotnetfx45_full_x86_x64.exe(.NET Framework 4.5 - 离线安装程序)
  • NDP462-KB3151800-x86-x64-AllOS-ENU.exe(.NET Framework 4.6.2 - 离线安装程序)

请注意,代码只考虑了 InstallSoFar,因为上面的两个安装程序都处于离线状态。对于在线安装程序,也应考虑 DownloadSoFar。实际上,即使是离线安装程序有时也会下载一些东西。


ShellExecuteEx 代码取自 Inno Setup Exec() function Wait for a limited time