Search code examples
inno-setupipcmemory-mapped-filespascalscript

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


I am currently installing .NET Framework 4.6.2 as a prerequisite in the PrepareToInstall event function so that I can obtain the exit code, set the NeedsReboot status, or abort if installation fails. My code is below and this is all working fine.

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;

At the moment, I am setting the installation type to either silent or unattended using /q or /passive to control the amount of visible GUI the .NET Framework installer displays, depending on how Inno Setup is running and using a Marquee style progress bar to indicate that something is happening. However, from the Microsoft documentation here, it appears that it is possible to get the .NET Framework installer to report it's install progress back, using the /pipe switch, which might allow it to interactively update a normal style progress bar on the actual progress. This would mean that the .NET Framework installer could be hidden completely and Inno Setup used to indicate the relative progress, which is a much tidier solution. Unfortunately, I do not know C++ and am only a novice programmer. Therefore, can anyone confirm if this is possible to do with Inno Setup and, if so, how it might be attempted?


Solution

  • The following shows Pascal Script implementation of the code from
    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 = $00000102;
      FILE_MAP_WRITE = $0002;
      E_PENDING = $8000000A;
      S_OK = 0;
      MMIO_V45 = 1;
      MAX_PATH = 260;
      SEE_MASK_NOCLOSEPROCESS = $00000040;
      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 '[email protected] stdcall';
    
    function CreateEvent(
      EventAttributes: Cardinal; ManualReset: Boolean; InitialState: Boolean;
      Name: string): THandle;
      external '[email protected] stdcall';
    
    function CreateMutex(
      MutexAttributes: Cardinal; InitialOwner: Boolean; Name: string): THandle;
      external '[email protected] stdcall';
    
    function WaitForSingleObject(
      Handle: THandle; Milliseconds: Cardinal): Cardinal;
      external '[email protected] stdcall';
    
    function MapViewOfFile(
      FileMappingObject: THandle; DesiredAccess: Cardinal; FileOffsetHigh: Cardinal;
      FileOffsetLow: Cardinal; NumberOfBytesToMap: Cardinal): Cardinal;
      external '[email protected] stdcall';
    
    function ReleaseMutex(Mutex: THandle): Boolean;
      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 GetExitCodeProcess(Process: THandle; var ExitCode: Cardinal): Boolean;
      external '[email protected] stdcall';
    
    procedure CopyPointerToData(
      var Destination: TMmioDataStructure; Source: Cardinal; Length: Cardinal);
      external '[email protected] stdcall';
    
    procedure CopyDataToPointer(
      Destination: Cardinal; var Source: TMmioDataStructure; Length: Cardinal);
      external '[email protected] 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)] := #$00;
      
      // 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;
    

    You can use it 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
          InstallNetFramework;
        except
          MsgBox(GetExceptionMessage, mbError, MB_OK);
          Result := False;
        end;
      end;
    end;
    

    The following screenshot shows how the "progress page" in Inno Setup is linked to the .NET framework installer (of course the .NET framework installer is hidden by the /q switch, it was just temporarily shown for purposes of obtaining the screenshot).

    enter image description here


    I've successfully tested the code on

    • dotnetfx45_full_x86_x64.exe (.NET framework 4.5 - off-line installer)
    • NDP462-KB3151800-x86-x64-AllOS-ENU.exe (.NET framework 4.6.2 - off-line installer)

    Note that the code takes into account the InstallSoFar only as both installers above are off-line. For on-line installers, DownloadSoFar should be taken into account too. And actually even off-line installers do sometime download something.


    The ShellExecuteEx code taken from Inno Setup Exec() function Wait for a limited time.