Browse Source

Merge branch 'johnstevenson-capture'

Martijn Laan 1 year ago
parent
commit
cf2306c497

+ 43 - 3
ISHelp/isxfunc.xml

@@ -1651,7 +1651,8 @@ begin
     // handle failure if necessary; ResultCode contains the error code
   end;
 end;</pre></example>
-        <seealso><p><link topic="isxfunc_ExecAndLogOutput">ExecAndLogOutput</link><br />
+        <seealso><p><link topic="isxfunc_ExecAndCaptureOutput">ExecAndCaptureOutput</link><br />
+<link topic="isxfunc_ExecAndLogOutput">ExecAndLogOutput</link><br />
 <link topic="isxfunc_ExecAsOriginalUser">ExecAsOriginalUser</link></p></seealso>
       </function>
       <function>
@@ -2868,6 +2869,43 @@ end;</pre></example>
         <description><p>Logs the specified string in Setup's or Uninstall's log file and/or in the Compiler IDE's "Debug Output" view.</p></description>
         <remarks><p>Calls to this function are ignored if logging is not enabled (via the <link topic="setupcmdline">/LOG</link> command line parameter or the <link topic="setup_setuplogging">SetupLogging</link> [Setup] section directive or the <link topic="setup_uninstalllogging">UninstallLogging</link> [Setup] section directive or debugging from the Compiler IDE).</p></remarks>
       </function>
+<function>
+        <name>ExecAndCaptureOutput</name>
+        <prototype>function ExecAndCaptureOutput(const Filename, Params, WorkingDir: String; const ShowCmd: Integer; const Wait: TExecWait; var ResultCode: Integer; var Output: TExecOutput): Boolean;</prototype>
+        <description><p>Identical to <link topic="isxfunc_Exec">Exec</link> except:</p>
+<p>Program output from the stdout and stderr streams is captured. This is different from <link topic="isxfunc_ExecAndLogOutput">ExecAndLogOutput</link> which merges the streams.</p>
+<p>Console programs are always hidden and the ShowCmd parameter only affects GUI programs, so always using <tt>SW_SHOWNORMAL</tt> instead of <tt>SW_HIDE</tt> is recommended.</p>
+<p>An exception will be raised if there was an error setting up output redirection (which should be very rare).</p></description>
+        <remarks><p>Parameter <tt>Wait</tt> must always be set to <tt>ewWaitUntilTerminated</tt> when calling this function.</p>
+<p>TExecOutput is defined as:</p>
+<pre>
+  TExecOutput = record
+    StdOut: TArrayOfString;
+    StdErr: TArrayOfString;
+    Error: Boolean;
+  end;
+</pre>
+<p>Error will be True if there was an error reading the output (which should be very rare) or if the output is too big. The error is logged in Setup's or Uninstall's log file and/or in the Compiler IDE's "Debug Output" view. There is no further output after an error.</p>
+<p>Output is limited to a total size of 10 million bytes or a maximum of 1 million lines.</p></remarks>
+        <example><pre>var
+  ResultCode: Integer;
+  Output: TExecOutput;
+begin
+  try
+    // Get the system configuration
+    Result := ExecAndCaptureOutput(ExpandConstant('{cmd}'), '/k systeminfo', '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode, Output);
+  except
+    Result := False;
+    Log(GetExceptionMessage);
+  end;
+
+  if Result then begin
+    // Do something with the Output.StdOut array of string
+  end;
+end;</pre></example>
+        <seealso><p><link topic="isxfunc_Exec">Exec</link><br />
+<link topic="isxfunc_ExecAndLogOutput">ExecAndLogOutput</link></p></seealso>
+      </function>
       <function>
         <name>ExecAndLogOutput</name>
         <prototype>function ExecAndLogOutput(const Filename, Params, WorkingDir: String; const ShowCmd: Integer; const Wait: TExecWait; var ResultCode: Integer; const OnLog: TOnLog): Boolean;</prototype>
@@ -2880,7 +2918,8 @@ end;</pre></example>
 <p>TOnLog is defined as:</p>
 <p><tt>TOnLog = procedure(const S: String; const Error, FirstLine: Boolean);</tt></p>
 <p>Parameter S is the output line when Error is False, otherwise an error message. FirstLine is True if this is the first line of output from the program, otherwise False.</p>
-<p>Error will be True if setting up output redirection or reading the output failed, or if the output size exceeded 10mb. There is no further output after an error.</p></remarks>
+<p>Error will be True if reading the output failed (which should be very rare), or if the output is too big. There is no further output after an error.</p>
+<p>Output is limited to a total size of 10 million bytes or a maximum of 1 million lines.</p></remarks>
         <example><pre>var
   Line: String;
 
@@ -2903,7 +2942,8 @@ begin
   end;
   Result := Line;
 end;</pre></example>
-        <seealso><p><link topic="isxfunc_Exec">Exec</link></p></seealso>
+        <seealso><p><link topic="isxfunc_Exec">Exec</link><br />
+<link topic="isxfunc_ExecAndCaptureOutput">ExecAndCaptureOutput</link></p></seealso>
       </function>
     </subcategory>
   </category>

+ 65 - 61
Projects/ISPP/IsppFuncs.pas

@@ -632,8 +632,8 @@ end;
 
 function Exec(const Filename, Params: String; WorkingDir: String;
   const WaitUntilTerminated: Boolean; const ShowCmd: Integer;
-  const Preprocessor: TPreprocessor; const Log: Boolean; const LogProc: TLogProc;
-  const LogProcData: NativeInt; var ResultCode: Integer): Boolean;
+  const Preprocessor: TPreprocessor; const OutputReader: TCreateProcessOutputReader;
+  var ResultCode: Integer): Boolean;
 var
   CmdLine: String;
   WorkingDirP: PChar;
@@ -668,57 +668,51 @@ begin
     WorkingDirP := PChar(WorkingDir)
   else
     WorkingDirP := nil;
-    
-  var OutputReader: TCreateProcessOutputReader := nil;
+
+  var InheritHandles := False;
+  var dwCreationFlags: DWORD := CREATE_DEFAULT_ERROR_MODE;
+
+  if (OutputReader <> nil) and WaitUntilTerminated then begin
+    OutputReader.UpdateStartupInfo(StartupInfo);
+    InheritHandles := True;
+    dwCreationFlags := dwCreationFlags or CREATE_NO_WINDOW;
+  end;
+
+  Result := CreateProcess(nil, PChar(CmdLine), nil, nil, InheritHandles,
+    dwCreationFlags, nil, WorkingDirP, StartupInfo, ProcessInfo);
+  if not Result then begin
+    ResultCode := GetLastError;
+    Exit;
+  end;
+
+  { Don't need the thread handle, so close it now }
+  CloseHandle(ProcessInfo.hThread);
+  if OutputReader <> nil then
+    OutputReader.NotifyCreateProcessDone;
+
   try
-    var InheritHandles := False;
-    var dwCreationFlags: DWORD := CREATE_DEFAULT_ERROR_MODE;
-
-    if Log and Assigned(LogProc) and WaitUntilTerminated then begin
-      OutputReader := TCreateProcessOutputReader.Create(LogProc, LogProcData);
-      OutputReader.UpdateStartupInfo(StartupInfo);
-      InheritHandles := True;
-      dwCreationFlags := dwCreationFlags or CREATE_NO_WINDOW;
-    end;
-
-    Result := CreateProcess(nil, PChar(CmdLine), nil, nil, InheritHandles,
-      dwCreationFlags, nil, WorkingDirP, StartupInfo, ProcessInfo);
-    if not Result then begin
-      ResultCode := GetLastError;
-      Exit;
-    end;
-    
-    { Don't need the thread handle, so close it now }
-    CloseHandle(ProcessInfo.hThread);
-    if OutputReader <> nil then
-      OutputReader.NotifyCreateProcessDone;
-      
-    try
-      if WaitUntilTerminated then begin
-        while True do begin
-          case WaitForSingleObject(ProcessInfo.hProcess, 50) of
-            WAIT_OBJECT_0: Break;
-            WAIT_TIMEOUT:
-              begin
-                if OutputReader <> nil then
-                  OutputReader.Read(False);
-                Preprocessor.CallIdleProc; { Doesn't allow an Abort }
-              end;
-          else
-            Preprocessor.RaiseError('Exec: WaitForSingleObject failed');
-          end;
+    if WaitUntilTerminated then begin
+      while True do begin
+        case WaitForSingleObject(ProcessInfo.hProcess, 50) of
+          WAIT_OBJECT_0: Break;
+          WAIT_TIMEOUT:
+            begin
+              if OutputReader <> nil then
+                OutputReader.Read(False);
+              Preprocessor.CallIdleProc; { Doesn't allow an Abort }
+            end;
+        else
+          Preprocessor.RaiseError('Exec: WaitForSingleObject failed');
         end;
-        if OutputReader <> nil then
-          OutputReader.Read(True);
       end;
-      { Get the exit code. Will be set to STILL_ACTIVE if not yet available }
-      if not GetExitCodeProcess(ProcessInfo.hProcess, DWORD(ResultCode)) then
-        ResultCode := -1;  { just in case }
-    finally
-      CloseHandle(ProcessInfo.hProcess);
+      if OutputReader <> nil then
+        OutputReader.Read(True);
     end;
+    { Get the exit code. Will be set to STILL_ACTIVE if not yet available }
+    if not GetExitCodeProcess(ProcessInfo.hProcess, DWORD(ResultCode)) then
+      ResultCode := -1;  { just in case }
   finally
-    OutputReader.Free;
+    CloseHandle(ProcessInfo.hProcess);
   end;
 end;
 
@@ -751,12 +745,17 @@ begin
       if (GetCount > 4) and (Get(4).Typ <> evNull) then ShowCmd := Get(4).AsInt;
       var Preprocessor := TPreprocessor(Ext);
       var ResultCode: Integer;
-      var Success := Exec(Get(0).AsStr, ParamsS, WorkingDir, WaitUntilTerminated,
-        ShowCmd, Preprocessor, True, ExecLog, NativeInt(Preprocessor), ResultCode);
-      if not WaitUntilTerminated then
-        MakeBool(ResPtr^, Success)
-      else
-        MakeInt(ResPtr^, ResultCode);
+      var OutputReader := TCreateProcessOutputReader.Create(ExecLog, NativeInt(Preprocessor));
+      try
+        var Success := Exec(Get(0).AsStr, ParamsS, WorkingDir, WaitUntilTerminated,
+          ShowCmd, Preprocessor, OutputReader, ResultCode);
+        if not WaitUntilTerminated then
+          MakeBool(ResPtr^, Success)
+        else
+          MakeInt(ResPtr^, ResultCode);
+      finally
+        OutputReader.Free;
+      end;
     end;
   except
     on E: Exception do
@@ -800,13 +799,18 @@ begin
       Data.Preprocessor := TPreprocessor(Ext);
       Data.Line := '';
       var ResultCode: Integer;
-      var Success := Exec(Get(0).AsStr, ParamsS, WorkingDir, True,
-        SW_SHOWNORMAL, Data.Preprocessor, True, ExecAndGetFirstLineLog, NativeInt(@Data), ResultCode);
-      if Success then
-        MakeStr(ResPtr^, Data.Line)
-      else begin
-        Data.Preprocessor.WarningMsg('CreateProcess failed (%d).', [ResultCode]);
-        ResPtr^.Typ := evNull;
+      var OutputReader := TCreateProcessOutputReader.Create(ExecAndGetFirstLineLog, NativeInt(@Data));
+      try
+        var Success := Exec(Get(0).AsStr, ParamsS, WorkingDir, True,
+          SW_SHOWNORMAL, Data.Preprocessor, OutputReader, ResultCode);
+        if Success then
+          MakeStr(ResPtr^, Data.Line)
+        else begin
+          Data.Preprocessor.WarningMsg('CreateProcess failed (%d).', [ResultCode]);
+          ResPtr^.Typ := evNull;
+        end;
+      finally
+        OutputReader.Free;
       end;
     end;
   except

+ 138 - 57
Projects/Src/CmnFunc2.pas

@@ -14,7 +14,7 @@ unit CmnFunc2;
 interface
 
 uses
-  Windows, SysUtils;
+  Windows, SysUtils, Classes;
 
 const
   KEY_WOW64_64KEY = $0100;
@@ -34,29 +34,41 @@ type
   end;
 
   TLogProc = procedure(const S: String; const Error, FirstLine: Boolean; const Data: NativeInt);
+  TOutputMode = (omLog, omCapture);
 
   TCreateProcessOutputReader = class
   private
     FOKToRead: Boolean;
     FMaxTotalBytesToRead: Cardinal;
+    FMaxTotalLinesToRead: Cardinal;
     FTotalBytesRead: Cardinal;
+    FTotalLinesRead: Cardinal;
     FStdInNulDevice: THandle;
     FStdOutPipeRead: THandle;
     FStdOutPipeWrite: THandle;
+    FStdErrPipeRead: THandle;
+    FStdErrPipeWrite: THandle;
     FLogProc: TLogProc;
     FLogProcData: NativeInt;
-    FReadBuffer: AnsiString;
+    FReadOutBuffer: AnsiString;
+    FReadErrBuffer: AnsiString;
     FNextLineIsFirstLine: Boolean;
-    FLastLogErrorMessage: String;
+    FMode: TOutputMode;
+    FCaptureOutList: TStringList;
+    FCaptureErrList: TStringList;
+    FCaptureError: Boolean;
     procedure CloseAndClearHandle(var Handle: THandle);
-    procedure LogErrorFmt(const S: String; const Args: array of const);
+    procedure HandleAndLogErrorFmt(const S: String; const Args: array of const);
+    procedure DoRead(var PipeRead: THandle; var Buffer: AnsiString; const LastRead: Boolean);
   public
-    constructor Create(const ALogProc: TLogProc; const ALogProcData: NativeInt);
+    constructor Create(const ALogProc: TLogProc; const ALogProcData: NativeInt; AMode: TOutputMode = omLog);
     destructor Destroy; override;
     procedure UpdateStartupInfo(var StartupInfo: TStartupInfo);
     procedure NotifyCreateProcessDone;
     procedure Read(const LastRead: Boolean);
-    property MaxTotalBytesToRead: Cardinal read FMaxTotalBytesToRead write FMaxTotalBytesToRead;
+    property CaptureOutList: TStringList read FCaptureOutList;
+    property CaptureErrList: TStringList read FCaptureErrList;
+    property CaptureError: Boolean read FCaptureError;
   end;
 
   TRegView = (rvDefault, rv32Bit, rv64Bit);
@@ -1597,14 +1609,40 @@ end;
 { TCreateProcessOutputReader }
 
 constructor TCreateProcessOutputReader.Create(const ALogProc: TLogProc;
-  const ALogProcData: NativeInt);
+  const ALogProcData: NativeInt; AMode: TOutputMode = omLog);
+
+  procedure CreatePipeAndSetHandleInformation(var Read, Write: THandle; SecurityAttr: TSecurityAttributes);
+  begin
+    { CreatePipe docs say no assumptions should be made about the output
+      parameter contents (the two handles) when it fails. So specify local
+      variables for the output parameters, and only copy the handles into
+      the "var" parameters when CreatePipe is successful. That way, if it
+      does fail, the "var" parameters will still have their original 0
+      values (which is important because the destructor closes all
+      non-zero handles). }
+    var TempReadPipe, TempWritePipe: THandle;
+    if not CreatePipe(TempReadPipe, TempWritePipe, @SecurityAttr, 0) then
+      raise Exception.CreateFmt('Output redirection error: CreatePipe failed (%d)', [GetLastError]);
+    Read := TempReadPipe;
+    Write := TempWritePipe;
+
+    if not SetHandleInformation(TempReadPipe, HANDLE_FLAG_INHERIT, 0) then
+      raise Exception.CreateFmt('Output redirection error: SetHandleInformation failed (%d)', [GetLastError]);
+  end;
+
 begin
   if not Assigned(ALogProc) then
     raise Exception.Create('ALogProc is required');
 
+  if AMode = omCapture then begin
+    FCaptureOutList := TStringList.Create;
+    FCaptureErrList := TStringList.Create;
+  end;
+
+  FMode := AMode;
   FLogProc := ALogProc;
-  FNextLineIsFirstLine := True;
   FLogProcData := ALogProcData;
+  FNextLineIsFirstLine := True;
 
   var SecurityAttributes: TSecurityAttributes;
   SecurityAttributes.nLength := SizeOf(SecurityAttributes);
@@ -1614,30 +1652,32 @@ begin
   var NulDevice := CreateFile('\\.\NUL', GENERIC_READ,
     FILE_SHARE_READ or FILE_SHARE_WRITE, @SecurityAttributes,
     OPEN_EXISTING, 0, 0);
-  if NulDevice = INVALID_HANDLE_VALUE then
-    LogErrorFmt('CreateFile failed (%d).', [GetLastError])
-  else begin
+  { In case the NUL device is missing (which it inexplicably seems to
+    be for some users, per web search), don't treat it as a fatal
+    error. Just leave FStdInNulDevice at 0. It's not ideal, but the
+    child process likely won't even attempt to access stdin anyway. }
+  if NulDevice <> INVALID_HANDLE_VALUE then
     FStdInNulDevice := NulDevice;
-    var PipeRead, PipeWrite: THandle;
-    if not CreatePipe(PipeRead, PipeWrite, @SecurityAttributes, 0) then
-      LogErrorFmt('CreatePipe failed (%d).', [GetLastError])
-    else if not SetHandleInformation(PipeRead, HANDLE_FLAG_INHERIT, 0) then
-      LogErrorFmt('SetHandleInformation failed (%d).', [GetLastError])
-    else begin
-      FStdOutPipeRead := PipeRead;
-      FStdOutPipeWrite := PipeWrite;
 
-      FOKToRead := True;
-      FMaxTotalBytesToRead := 10*1024*1024;
-    end;
-  end;
+  CreatePipeAndSetHandleInformation(FStdOutPipeRead, FStdOutPipeWrite, SecurityAttributes);
+
+  if FMode = omCapture then
+    CreatePipeAndSetHandleInformation(FStdErrPipeRead, FStdErrPipeWrite, SecurityAttributes);
+
+  FOkToRead := True;
+  FMaxTotalBytesToRead := 10*1000*1000;
+  FMaxTotalLinesToRead := 1000*1000;
 end;
 
 destructor TCreateProcessOutputReader.Destroy;
 begin
+  CloseAndClearHandle(FStdInNulDevice);
   CloseAndClearHandle(FStdOutPipeRead);
   CloseAndClearHandle(FStdOutPipeWrite);
-  CloseAndClearHandle(FStdInNulDevice);
+  CloseAndClearHandle(FStdErrPipeRead);
+  CloseAndClearHandle(FStdErrPipeWrite);
+  FCaptureOutList.Free;
+  FCaptureErrList.Free;
   inherited;
 end;
 
@@ -1649,30 +1689,42 @@ begin
   end;
 end;
 
-procedure TCreateProcessOutputReader.LogErrorFmt(const S: String; const Args: array of const);
+procedure TCreateProcessOutputReader.HandleAndLogErrorFmt(const S: String; const Args: array of const);
 begin
-  FLastLogErrorMessage := Format(S, Args);
-  FLogProc('OutputReader: ' + FLastLogErrorMessage, True, False, FLogProcData);
+  FLogProc('OutputReader: ' + Format(S, Args), True, False, FLogProcData);
+
+  if FMode = omCapture then
+    FCaptureError := True;
 end;
 
 procedure TCreateProcessOutputReader.UpdateStartupInfo(var StartupInfo: TStartupInfo);
 begin
-  if not FOKToRead then
-    raise Exception.Create(Format('Output redirection error: %s', [FLastLogErrorMessage]));
-
   StartupInfo.dwFlags := StartupInfo.dwFlags or STARTF_USESTDHANDLES;
   StartupInfo.hStdInput := FStdInNulDevice;
   StartupInfo.hStdOutput := FStdOutPipeWrite;
-  StartupInfo.hStdError := FStdOutPipeWrite;
+
+  if FMode = omLog then
+    StartupInfo.hStdError := FStdOutPipeWrite
+  else
+    StartupInfo.hStdError := FStdErrPipeWrite;
 end;
 
 procedure TCreateProcessOutputReader.NotifyCreateProcessDone;
 begin
-  CloseAndClearHandle(FStdOutPipeWrite);
   CloseAndClearHandle(FStdInNulDevice);
+  CloseAndClearHandle(FStdOutPipeWrite);
+  CloseAndClearHandle(FStdErrPipeWrite);
 end;
 
 procedure TCreateProcessOutputReader.Read(const LastRead: Boolean);
+begin
+  DoRead(FStdOutPipeRead, FReadOutBuffer, LastRead);
+  if FMode = omCapture then
+    DoRead(FStdErrPipeRead, FReadErrBuffer, LastRead);
+end;
+
+procedure TCreateProcessOutputReader.DoRead(var PipeRead: THandle;
+ var Buffer: AnsiString; const LastRead: Boolean);
 
   function FindNewLine(const S: AnsiString; const LastRead: Boolean): Integer;
   begin
@@ -1687,63 +1739,92 @@ procedure TCreateProcessOutputReader.Read(const LastRead: Boolean);
     Result := 0;
   end;
 
-  procedure LogLine(const S: AnsiString);
+  procedure LogLine(const FromPipe: THandle; const S: AnsiString);
   begin
-    FLogProc(UTF8ToString(S), False, FNextLineIsFirstLine, FLogProcData);
-    FNextLineIsFirstLine := False;
+    var UTF8S := UTF8ToString(S);
+    if FMode = omLog then begin
+      FLogProc(UTF8S, False, FNextLineIsFirstLine, FLogProcData);
+      FNextLineIsFirstLine := False;
+    end else if FromPipe = FStdOutPipeRead then
+      FCaptureOutList.Add(UTF8S)
+    else
+      FCaptureErrList.Add(UTF8S);
   end;
 
 begin
   if FOKToRead then begin
     var TotalBytesAvail: DWORD;
-    FOKToRead := PeekNamedPipe(FStdOutPipeRead, nil, 0, nil, @TotalBytesAvail, nil);
+    FOKToRead := PeekNamedPipe(PipeRead, nil, 0, nil, @TotalBytesAvail, nil);
     if not FOKToRead then begin
       var LastError := GetLastError;
       if LastError <> ERROR_BROKEN_PIPE then
-        LogErrorFmt('PeekNamedPipe failed (%d).', [LastError]);
+        HandleAndLogErrorFmt('PeekNamedPipe failed (%d).', [LastError]);
     end else if TotalBytesAvail > 0 then begin
       { Don't read more than our read limit }
       if TotalBytesAvail > FMaxTotalBytesToRead - FTotalBytesRead then
         TotalBytesAvail := FMaxTotalBytesToRead - FTotalBytesRead;
       { Append newly available data to the incomplete line we might already have }
-      var TotalBytesHave: DWORD := Length(FReadBuffer);
-      SetLength(FReadBuffer, TotalBytesHave+TotalBytesAvail);
+      var TotalBytesHave: DWORD := Length(Buffer);
+      SetLength(Buffer, TotalBytesHave+TotalBytesAvail);
       var BytesRead: DWORD;
-      FOKToRead := ReadFile(FStdOutPipeRead, FReadBuffer[TotalBytesHave+1],
+      FOKToRead := ReadFile(PipeRead, Buffer[TotalBytesHave+1],
         TotalBytesAvail, BytesRead, nil);
-      if not FOKToRead then
-        LogErrorFmt('ReadFile failed (%d).', [GetLastError])
-      else if BytesRead > 0 then begin
+      if not FOKToRead then begin
+        HandleAndLogErrorFmt('ReadFile failed (%d).', [GetLastError]);
+        { Restore back to original size }
+        SetLength(Buffer, TotalBytesHave);
+      end else begin
         { Correct length if less bytes were read than requested }
-        SetLength(FReadBuffer, TotalBytesHave+BytesRead);
+        SetLength(Buffer, TotalBytesHave+BytesRead);
 
         { Check for completed lines thanks to the new data }
-        var P := FindNewLine(FReadBuffer, LastRead);
-        while P <> 0 do begin
-          LogLine(Copy(FReadBuffer, 1, P-1));
-          if (FReadBuffer[P] = #13) and (P < Length(FReadBuffer)) and (FReadBuffer[P+1] = #10) then
+        while FTotalLinesRead < FMaxTotalLinesToRead do begin
+          var P := FindNewLine(Buffer, LastRead);
+          if P = 0 then
+            Break;
+          LogLine(PipeRead, Copy(Buffer, 1, P-1));
+          Inc(FTotalLinesRead);
+          if (Buffer[P] = #13) and (P < Length(Buffer)) and (Buffer[P+1] = #10) then
             Inc(P);
-          Delete(FReadBuffer, 1, P);
-          P := FindNewLine(FReadBuffer, LastRead);
+          Delete(Buffer, 1, P);
         end;
 
         Inc(FTotalBytesRead, BytesRead);
-        if FTotalBytesRead >= FMaxTotalBytesToRead then begin
+        if (FTotalBytesRead >= FMaxTotalBytesToRead) or
+           (FTotalLinesRead >= FMaxTotalLinesToRead) then begin
           { Read limit reached: break the pipe, throw away the incomplete line, and log an error }
           FOKToRead := False;
-          FReadBuffer := '';
-          LogErrorFmt('Maximum output length (%d) reached, ignoring remainder.', [FMaxTotalBytesToRead]);
+          if FMode = omLog then
+            Buffer := ''
+          else begin
+            { Bit of a hack: the Buffer parameter points to either FReadOutBuffer or FReadErrBuffer.
+              We want both cleared and must do this now because won't get here again. So just access
+              both directly. }
+            FReadOutBuffer := '';
+            FReadErrBuffer := '';
+          end;
+
+          if FTotalBytesRead >= FMaxTotalBytesToRead then
+            HandleAndLogErrorFmt('Maximum output length (%d) reached, ignoring remainder.', [FMaxTotalBytesToRead])
+          else
+            HandleAndLogErrorFmt('Maximum output lines (%d) reached, ignoring remainder.', [FMaxTotalLinesToRead]);
         end;
       end;
     end;
 
     { Unblock the child process's write, and cause further writes to fail immediately }
-    if not FOkToRead then
-      CloseAndClearHandle(FStdOutPipeRead);
+    if not FOkToRead then begin
+      if FMode = omLog then
+        CloseAndClearHandle(PipeRead)
+      else begin
+        CloseAndClearHandle(FStdOutPipeRead);
+        CloseAndClearHandle(FStdErrPipeRead);
+      end;
+    end;
   end;
 
-  if LastRead and (FReadBuffer <> '') then
-    LogLine(FReadBuffer);
+  if LastRead and (Buffer <> '') then
+    LogLine(PipeRead, Buffer);
 end;
 
 end.

+ 4 - 5
Projects/Src/Compile.pas

@@ -7367,10 +7367,6 @@ procedure TSetupCompiler.SignCommand(const AName, ACommand, AParams, AExeFilenam
 
   procedure InternalSignCommand(const AFormattedCommand: String;
     const Delay: Cardinal);
-  var
-    StartupInfo: TStartupInfo;
-    ProcessInfo: TProcessInformation;
-    LastError, ExitCode: DWORD;
   begin
     {Also see IsppFuncs' Exec }
 
@@ -7382,6 +7378,7 @@ procedure TSetupCompiler.SignCommand(const AName, ACommand, AParams, AExeFilenam
 
     LastSignCommandStartTick := GetTickCount;
 
+    var StartupInfo: TStartupInfo;
     FillChar(StartupInfo, SizeOf(StartupInfo), 0);
     StartupInfo.cb := SizeOf(StartupInfo);
     StartupInfo.dwFlags := STARTF_USESHOWWINDOW;
@@ -7393,9 +7390,10 @@ procedure TSetupCompiler.SignCommand(const AName, ACommand, AParams, AExeFilenam
       var dwCreationFlags: DWORD := CREATE_DEFAULT_ERROR_MODE or CREATE_NO_WINDOW;
       OutputReader.UpdateStartupInfo(StartupInfo);
 
+      var ProcessInfo: TProcessInformation;
       if not CreateProcess(nil, PChar(AFormattedCommand), nil, nil, InheritHandles,
          dwCreationFlags, nil, PChar(CompilerDir), StartupInfo, ProcessInfo) then begin
-        LastError := GetLastError;
+        var LastError := GetLastError;
         AbortCompileFmt(SCompilerSignToolCreateProcessFailed, [LastError,
           Win32ErrorString(LastError)]);
       end;
@@ -7418,6 +7416,7 @@ procedure TSetupCompiler.SignCommand(const AName, ACommand, AParams, AExeFilenam
           end;
         end;
         OutputReader.Read(True);
+        var ExitCode: DWORD;
         if not GetExitCodeProcess(ProcessInfo.hProcess, ExitCode) then
           AbortCompile('Sign: GetExitCodeProcess failed');
         if ExitCode <> 0 then

+ 24 - 30
Projects/Src/InstFunc.pas

@@ -90,8 +90,8 @@ procedure IncrementSharedCount(const RegView: TRegView; const Filename: String;
   const AlreadyExisted: Boolean);
 function InstExec(const DisableFsRedir: Boolean; const Filename, Params: String;
   WorkingDir: String; const Wait: TExecWait; const ShowCmd: Integer;
-  const ProcessMessagesProc: TProcedure; const Log: Boolean; const LogProc: TLogProc;
-  const LogProcData: NativeInt; var ResultCode: Integer): Boolean;
+  const ProcessMessagesProc: TProcedure; const OutputReader: TCreateProcessOutputReader;
+  var ResultCode: Integer): Boolean;
 function InstShellExec(const Verb, Filename, Params: String; WorkingDir: String;
   const Wait: TExecWait; const ShowCmd: Integer;
   const ProcessMessagesProc: TProcedure; var ResultCode: Integer): Boolean;
@@ -905,8 +905,8 @@ end;
 
 function InstExec(const DisableFsRedir: Boolean; const Filename, Params: String;
   WorkingDir: String; const Wait: TExecWait; const ShowCmd: Integer;
-  const ProcessMessagesProc: TProcedure; const Log: Boolean; const LogProc: TLogProc;
-  const LogProcData: NativeInt; var ResultCode: Integer): Boolean;
+  const ProcessMessagesProc: TProcedure; const OutputReader: TCreateProcessOutputReader;
+  var ResultCode: Integer): Boolean;
 var
   CmdLine: String;
   StartupInfo: TStartupInfo;
@@ -946,35 +946,29 @@ begin
   if WorkingDir = '' then
     WorkingDir := GetSystemDir;
 
-  var OutputReader: TCreateProcessOutputReader := nil;
-  try
-    var InheritHandles := False;
-    var dwCreationFlags: DWORD := CREATE_DEFAULT_ERROR_MODE;
-
-    if Log and Assigned(LogProc) and (Wait = ewWaitUntilTerminated) then begin
-      OutputReader := TCreateProcessOutputReader.Create(LogProc, LogProcData);
-      OutputReader.UpdateStartupInfo(StartupInfo);
-      InheritHandles := True;
-      dwCreationFlags := dwCreationFlags or CREATE_NO_WINDOW;
-    end;
+  var InheritHandles := False;
+  var dwCreationFlags: DWORD := CREATE_DEFAULT_ERROR_MODE;
 
-    Result := CreateProcessRedir(DisableFsRedir, nil, PChar(CmdLine), nil, nil,
-      InheritHandles, dwCreationFlags, nil, PChar(WorkingDir),
-      StartupInfo, ProcessInfo);
-    if not Result then begin
-      ResultCode := GetLastError;
-      Exit;
-    end;
+  if (OutputReader <> nil) and (Wait = ewWaitUntilTerminated) then begin
+    OutputReader.UpdateStartupInfo(StartupInfo);
+    InheritHandles := True;
+    dwCreationFlags := dwCreationFlags or CREATE_NO_WINDOW;
+  end;
 
-    { Don't need the thread handle, so close it now }
-    CloseHandle(ProcessInfo.hThread);
-    if OutputReader <> nil then
-      OutputReader.NotifyCreateProcessDone;
-    HandleProcessWait(ProcessInfo.hProcess, Wait, ProcessMessagesProc,
-      OutputReader, ResultCode);
-  finally
-    OutputReader.Free;
+  Result := CreateProcessRedir(DisableFsRedir, nil, PChar(CmdLine), nil, nil,
+    InheritHandles, dwCreationFlags, nil, PChar(WorkingDir),
+    StartupInfo, ProcessInfo);
+  if not Result then begin
+    ResultCode := GetLastError;
+    Exit;
   end;
+
+  { Don't need the thread handle, so close it now }
+  CloseHandle(ProcessInfo.hThread);
+  if OutputReader <> nil then
+    OutputReader.NotifyCreateProcessDone;
+  HandleProcessWait(ProcessInfo.hProcess, Wait, ProcessMessagesProc,
+    OutputReader, ResultCode);
 end;
 
 function InstShellExec(const Verb, Filename, Params: String; WorkingDir: String;

+ 15 - 9
Projects/Src/Main.pas

@@ -3921,15 +3921,21 @@ begin
       DisableFsRedir := ShouldDisableFsRedirForRunEntry(RunEntry);
       if not(roSkipIfDoesntExist in RunEntry.Options) or
          NewFileExistsRedir(DisableFsRedir, ExpandedFilename) then begin
-        if not InstExecEx(RunAsOriginalUser, DisableFsRedir, ExpandedFilename,
-           ExpandedParameters, ExpandConst(RunEntry.WorkingDir),
-           Wait, RunEntry.ShowCmd, ProcessMessagesProc, GetLogActive and (roLogOutput in RunEntry.Options),
-           RunExecLog, 0, ErrorCode) then
-          raise Exception.Create(FmtSetupMessage1(msgErrorExecutingProgram, ExpandedFilename) +
-            SNewLine2 + FmtSetupMessage(msgErrorFunctionFailedWithMessage,
-            ['CreateProcess', IntToStr(ErrorCode), Win32ErrorString(ErrorCode)]));
-        if Wait = ewWaitUntilTerminated then
-          Log(Format('Process exit code: %u', [ErrorCode]));
+        var OutputReader: TCreateProcessOutputReader := nil;
+        try
+          if GetLogActive and (roLogOutput in RunEntry.Options) then
+            OutputReader := TCreateProcessOutputReader.Create(RunExecLog, 0);
+          if not InstExecEx(RunAsOriginalUser, DisableFsRedir, ExpandedFilename,
+             ExpandedParameters, ExpandConst(RunEntry.WorkingDir),
+             Wait, RunEntry.ShowCmd, ProcessMessagesProc, OutputReader, ErrorCode) then
+            raise Exception.Create(FmtSetupMessage1(msgErrorExecutingProgram, ExpandedFilename) +
+              SNewLine2 + FmtSetupMessage(msgErrorFunctionFailedWithMessage,
+              ['CreateProcess', IntToStr(ErrorCode), Win32ErrorString(ErrorCode)]));
+          if Wait = ewWaitUntilTerminated then
+            Log(Format('Process exit code: %u', [ErrorCode]));
+        finally
+          OutputReader.Free;
+        end;
       end
       else
         Log('File doesn''t exist. Skipping.');

+ 2 - 1
Projects/Src/ScriptFunc.pas

@@ -133,7 +133,7 @@ const
   );
 
   { InstFunc }
-  InstFuncTable: array [0..31] of AnsiString =
+  InstFuncTable: array [0..32] of AnsiString =
   (
     'function CheckForMutexes(Mutexes: String): Boolean;',
     'function DecrementSharedCount(const Is64Bit: Boolean; const Filename: String): Boolean;',
@@ -158,6 +158,7 @@ const
     //function GrantPermissionOnKey(const RootKey: HKEY; const Subkey: String; const Entries: TGrantPermissionEntry; const EntryCount: Integer): Boolean;
     'procedure IncrementSharedCount(const Is64Bit: Boolean; const Filename: String; const AlreadyExisted: Boolean);',
     'function Exec(const Filename, Params, WorkingDir: String; const ShowCmd: Integer; const Wait: TExecWait; var ResultCode: Integer): Boolean;',
+    'function ExecAndCaptureOutput(const Filename, Params, WorkingDir: String; const ShowCmd: Integer; const Wait: TExecWait; var ResultCode: Integer; var Output: TExecOutput): Boolean;',
     'function ExecAndLogOutput(const Filename, Params, WorkingDir: String; const ShowCmd: Integer; const Wait: TExecWait; var ResultCode: Integer; const OnLog: TOnLog): Boolean;',
     'function ExecAsOriginalUser(const Filename, Params, WorkingDir: String; const ShowCmd: Integer; const Wait: TExecWait; var ResultCode: Integer): Boolean;',
     'function ShellExec(const Verb, Filename, Params, WorkingDir: String; const ShowCmd: Integer; const Wait: TExecWait; var ErrorCode: Integer): Boolean;',

+ 8 - 1
Projects/Src/ScriptFunc_C.pas

@@ -116,7 +116,14 @@ begin
   RegisterRealEnum('TDotNetVersion', TypeInfo(TDotNetVersion));
 
   RegisterType('TExecWait', '(ewNoWait, ewWaitUntilTerminated, ewWaitUntilIdle)');
-  
+
+  RegisterType('TExecOutput',
+    'record' +
+    '  StdOut: TArrayOfString;' +
+    '  StdErr: TArrayOfString;' +
+    '  Error: Boolean;' +
+    'end');
+
   RegisterType('TFindRec',
     'record' +
     '  Name: String;' +

+ 67 - 40
Projects/Src/ScriptFunc_R.pas

@@ -806,14 +806,42 @@ begin
 end;
 
 type
+  { These must keep this in synch with ScriptFunc_C }
   TOnLog = procedure(const S: String; const Error, FirstLine: Boolean) of object;
 
+  TExecOutput = record
+    StdOut: PPSVariantIFC;
+    StdErr: PPSVariantIFC;
+    Error: Boolean;
+  end;
+
 procedure ExecAndLogOutputLogCustom(const S: String; const Error, FirstLine: Boolean; const Data: NativeInt);
 begin
   var OnLog := TOnLog(PMethod(Data)^);
   OnLog(S, Error, FirstLine);
 end;
 
+procedure ExecAndCaptureFinalize(StackData: Pointer; const OutputReader: TCreateProcessOutputReader);
+begin
+  { StdOut - 0 }
+  var Item := NewTPSVariantRecordIFC(StackData, 0);
+  var List := OutputReader.CaptureOutList;
+  PSDynArraySetLength(Pointer(Item.Dta^), Item.aType, List.Count);
+  for var I := 0 to List.Count - 1 do
+    VNSetString(PSGetArrayField(Item, I), List[I]);
+
+  { StdErr - 1 }
+  Item := NewTPSVariantRecordIFC(StackData, 1);
+  List := OutputReader.CaptureErrList;
+  PSDynArraySetLength(Pointer(Item.Dta^), Item.aType, List.Count);
+  for var I := 0 to List.Count - 1 do
+    VNSetString(PSGetArrayField(Item, I), List[I]);
+
+  { Error - 2 }
+  Item := NewTPSVariantRecordIFC(StackData, 2);
+  VNSetInt(Item, OutputReader.CaptureError.ToInteger);
+end;
+
 function InstFuncProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: TPSStack): Boolean;
 var
   PStart: Cardinal;
@@ -895,50 +923,49 @@ begin
     else
       IncrementSharedCount(rv32Bit, Stack.GetString(PStart-1), Stack.GetBool(PStart-2));
   end else if (Proc.Name = 'EXEC') or (Proc.Name = 'EXECASORIGINALUSER') or
-              (Proc.Name = 'EXECANDLOGOUTPUT') then begin
+              (Proc.Name = 'EXECANDLOGOUTPUT') or (Proc.Name = 'EXECANDCAPTUREOUTPUT') then begin
     var RunAsOriginalUser := Proc.Name = 'EXECASORIGINALUSER';
-    var LogOutput: Boolean;
-    var LogProc: TLogProc := nil;
-    var LogProcData: NativeInt := 0;
     var Method: TMethod;
-    if Proc.Name = 'EXECANDLOGOUTPUT' then begin
-      var P: PPSVariantProcPtr := Stack.Items[PStart-7];
-      { ProcNo 0 means nil was passed by the script }
-      if P.ProcNo <> 0 then begin
-        LogOutput := True;
-        LogProc := ExecAndLogOutputLogCustom;
-        Method := Caller.GetProcAsMethod(P.ProcNo); { This is a TOnLog }
-        LogProcData := NativeInt(@Method);
+    var OutputReader: TCreateProcessOutputReader := nil;
+    try
+      if Proc.Name = 'EXECANDLOGOUTPUT' then begin
+        var P: PPSVariantProcPtr := Stack.Items[PStart-7];
+        { ProcNo 0 means nil was passed by the script }
+        if P.ProcNo <> 0 then begin
+          Method := Caller.GetProcAsMethod(P.ProcNo); { This is a TOnLog }
+          OutputReader := TCreateProcessOutputReader.Create(ExecAndLogOutputLogCustom, NativeInt(@Method));
+        end else if GetLogActive then
+          OutputReader := TCreateProcessOutputReader.Create(ExecAndLogOutputLog, 0);
+      end else if Proc.Name = 'EXECANDCAPTUREOUTPUT' then
+        OutputReader := TCreateProcessOutputReader.Create(ExecAndLogOutputLog, 0, omCapture);
+      var ExecWait := TExecWait(Stack.GetInt(PStart-5));
+      if IsUninstaller and RunAsOriginalUser then
+        NoUninstallFuncError(Proc.Name)
+      else if (OutputReader <> nil) and (ExecWait <> ewWaitUntilTerminated) then
+        InternalError(Format('Must call "%s" function with Wait = ewWaitUntilTerminated', [Proc.Name]));
+
+      Filename := Stack.GetString(PStart-1);
+      if PathCompare(Filename, SetupLdrOriginalFilename) <> 0 then begin
+        { Disable windows so the user can't utilize our UI during the InstExec
+          call }
+        WindowDisabler := TWindowDisabler.Create;
+        try
+          Stack.SetBool(PStart, InstExecEx(RunAsOriginalUser,
+            ScriptFuncDisableFsRedir, Filename, Stack.GetString(PStart-2),
+            Stack.GetString(PStart-3), ExecWait,
+            Stack.GetInt(PStart-4), ProcessMessagesProc, OutputReader, ResultCode));
+        finally
+          WindowDisabler.Free;
+        end;
+        Stack.SetInt(PStart-6, ResultCode);
+        if Proc.Name = 'EXECANDCAPTUREOUTPUT' then
+          ExecAndCaptureFinalize(Stack[PStart-7], OutputReader);
       end else begin
-        LogOutput := GetLogActive;
-        LogProc := ExecAndLogOutputLog;
-      end;
-    end else
-      LogOutput := False;
-    var ExecWait := TExecWait(Stack.GetInt(PStart-5));
-    if IsUninstaller and RunAsOriginalUser then
-      NoUninstallFuncError(Proc.Name)
-    else if LogOutput and (ExecWait <> ewWaitUntilTerminated) then
-      InternalError(Format('Must call "%s" function with Wait = ewWaitUntilTerminated', [Proc.Name]));
-
-    Filename := Stack.GetString(PStart-1);
-    if PathCompare(Filename, SetupLdrOriginalFilename) <> 0 then begin
-      { Disable windows so the user can't utilize our UI during the InstExec
-        call }
-      WindowDisabler := TWindowDisabler.Create;
-      try
-        Stack.SetBool(PStart, InstExecEx(RunAsOriginalUser,
-          ScriptFuncDisableFsRedir, Filename, Stack.GetString(PStart-2),
-          Stack.GetString(PStart-3), ExecWait,
-          Stack.GetInt(PStart-4), ProcessMessagesProc, LogOutput,
-          LogProc, LogProcData, ResultCode));
-      finally
-        WindowDisabler.Free;
+        Stack.SetBool(PStart, False);
+        Stack.SetInt(PStart-6, ERROR_ACCESS_DENIED);
       end;
-      Stack.SetInt(PStart-6, ResultCode);
-    end else begin
-      Stack.SetBool(PStart, False);
-      Stack.SetInt(PStart-6, ERROR_ACCESS_DENIED);
+    finally
+      OutputReader.Free;
     end;
   end else if (Proc.Name = 'SHELLEXEC') or (Proc.Name = 'SHELLEXECASORIGINALUSER') then begin
     var RunAsOriginalUser := Proc.Name = 'SHELLEXECASORIGINALUSER';

+ 5 - 5
Projects/Src/SpawnClient.pas

@@ -21,8 +21,8 @@ procedure InitializeSpawnClient(const AServerWnd: HWND);
 function InstExecEx(const RunAsOriginalUser: Boolean;
   const DisableFsRedir: Boolean; const Filename, Params, WorkingDir: String;
   const Wait: TExecWait; const ShowCmd: Integer;
-  const ProcessMessagesProc: TProcedure; const Log: Boolean; const LogProc: TLogProc;
-  const LogProcData: NativeInt; var ResultCode: Integer): Boolean;
+  const ProcessMessagesProc: TProcedure; const OutputReader: TCreateProcessOutputReader;
+  var ResultCode: Integer): Boolean;
 function InstShellExecEx(const RunAsOriginalUser: Boolean;
   const Verb, Filename, Params, WorkingDir: String;
   const Wait: TExecWait; const ShowCmd: Integer;
@@ -138,14 +138,14 @@ end;
 function InstExecEx(const RunAsOriginalUser: Boolean;
   const DisableFsRedir: Boolean; const Filename, Params, WorkingDir: String;
   const Wait: TExecWait; const ShowCmd: Integer;
-  const ProcessMessagesProc: TProcedure; const Log: Boolean; const LogProc: TLogProc;
-  const LogProcData: NativeInt; var ResultCode: Integer): Boolean;
+  const ProcessMessagesProc: TProcedure; const OutputReader: TCreateProcessOutputReader;
+  var ResultCode: Integer): Boolean;
 var
   M: TMemoryStream;
 begin
   if not RunAsOriginalUser or not SpawnServerPresent then begin
     Result := InstExec(DisableFsRedir, Filename, Params, WorkingDir,
-      Wait, ShowCmd, ProcessMessagesProc, Log, LogProc, LogProcData, ResultCode);
+      Wait, ShowCmd, ProcessMessagesProc, OutputReader, ResultCode);
     Exit;
   end;
 

+ 1 - 1
Projects/Src/SpawnServer.pas

@@ -380,7 +380,7 @@ begin
       end
       else begin
         ExecResult := InstExec(EDisableFsRedir <> 0, EFilename, EParams, EWorkingDir,
-          TExecWait(EWait), EShowCmd, ProcessMessagesProc, False, nil, 0, FResultCode);
+          TExecWait(EWait), EShowCmd, ProcessMessagesProc, nil, FResultCode);
       end;
       if ExecResult then
         FCallStatus := SPAWN_STATUS_RETURNED_TRUE

+ 16 - 10
Projects/Src/Undo.pas

@@ -814,16 +814,22 @@ begin
                   Log('Running Exec parameters: ' + CurRecData[1]);
                 if (CurRec^.ExtraData and utRun_SkipIfDoesntExist = 0) or
                    NewFileExistsRedir(CurRec^.ExtraData and utRun_DisableFsRedir <> 0, CurRecData[0]) then begin
-                  if not InstExec(CurRec^.ExtraData and utRun_DisableFsRedir <> 0,
-                     CurRecData[0], CurRecData[1], CurRecData[2], Wait,
-                     ShowCmd, ProcessMessagesProc, GetLogActive and (CurRec^.ExtraData and utRun_LogOutput <> 0),
-                     RunExecLog, 0, ErrorCode) then begin
-                    LogFmt('CreateProcess failed (%d).', [ErrorCode]);
-                    Result := False;
-                  end
-                  else begin
-                    if Wait = ewWaitUntilTerminated then
-                      LogFmt('Process exit code: %u', [ErrorCode]);
+                  var OutputReader: TCreateProcessOutputReader := nil;
+                  try
+                    if GetLogActive and (CurRec^.ExtraData and utRun_LogOutput <> 0) then
+                      OutputReader := TCreateProcessOutputReader.Create(RunExecLog, 0);
+                    if not InstExec(CurRec^.ExtraData and utRun_DisableFsRedir <> 0,
+                       CurRecData[0], CurRecData[1], CurRecData[2], Wait,
+                       ShowCmd, ProcessMessagesProc, OutputReader, ErrorCode) then begin
+                      LogFmt('CreateProcess failed (%d).', [ErrorCode]);
+                      Result := False;
+                    end
+                    else begin
+                      if Wait = ewWaitUntilTerminated then
+                        LogFmt('Process exit code: %u', [ErrorCode]);
+                    end;
+                  finally
+                    OutputReader.Free;
                   end;
                 end else
                   Log('File doesn''t exist. Skipping.');

+ 2 - 1
whatsnew.htm

@@ -67,7 +67,8 @@ For conditions of distribution and use, see <a href="files/is/license.txt">LICEN
   <li>Added shortcuts to select a tab (Ctrl+1 through Ctrl+9).</li>
   <li>Added new <i>Word Wrap</i> menu item to the <i>View</i> menu (Alt+Z).</li>
   <li>Added shortcut to the <i>Options</i> menu item in the <i>Tools</i> menu (Ctrl+,).</li>
-  <li>Output logging now raises an exception if there was an error setting up output redirection (which should be very rare). The <i>PowerShell.iss</i> example script has been updated to catch the exception.</li>
+  <li>Output logging now raises an exception if there was an error setting up output redirection (which should be very rare). The <i>PowerShell.iss</i> example script has been updated to catch the exception.</li>  
+  <li>Pascal Scripting change: Added new <tt>ExecAndCaptureOutput</tt> support function to execute a program or batch file and capture its <i>stdout</i> and <i>stderr</i> output.</li>
   <li>Fixed an issue when the <i>Auto indent mode</i> and <i>Allow cursor to move beyond end of lines</i> options are both enabled.</li>  
   <li>Various tweaks and documentation improvements.</li>
 </ul>