From volax, 10 Years ago, written in Plain Text.
Embed
  1. unit Utils.ConsoleRedirector;
  2.  
  3. interface
  4.  
  5. uses
  6.   Windows;
  7.  
  8. type
  9.  
  10.   TConsoleRedirector = class;
  11.  
  12.   TConsoleFinishedEvent = procedure(Sender: TConsoleRedirector) of object;
  13.  
  14.   TConsoleTextEvent = procedure(Sender: TConsoleRedirector; Text: string) of object;
  15.  
  16.   TConsoleRedirector = class
  17.   private
  18.     FWorkingThread: THandle; // thread to receive the output of the child process
  19.     FStopEvent: THandle; // event to notify the redir thread to exit
  20.     FWorkingThreadId: Cardinal; // id of the redir thread
  21.     FWaitTime: Cardinal;
  22.     FOnStdoutText: TConsoleTextEvent;
  23.     FOnStderrText: TConsoleTextEvent;
  24.     FOnFinished: TConsoleFinishedEvent; // wait time to check the status of the child process
  25.     FStdInWritePipe: THandle; // write end of child's stdin pipe
  26.     FStdOutReadPipe: THandle; // read end of child's stdout pipe
  27.     FChildProcess: THandle;
  28.   protected
  29.     function LaunchChild(CommandLine: string; StdOutHandle, StdInHandle, StdErrHandle: THandle): Boolean;
  30.     function RedirectStdout: Integer;
  31.     procedure DestroyHandle(var Handle: THandle);
  32.     procedure DoStdOutEvent(Text: AnsiString); virtual;
  33.     procedure DoStdErrorEvent(Text: AnsiString); virtual;
  34.     procedure DoFinishedEvent; virtual;
  35.   public
  36.     constructor Create;
  37.     destructor Destroy; override;
  38.     function Open(CommandLine: string): Boolean;
  39.     procedure Close; virtual;
  40.     function Write(Text: AnsiString): Boolean;
  41.     property WaitTime: Cardinal read FWaitTime write FWaitTime;
  42.     property OnStdoutText: TConsoleTextEvent read FOnStdoutText write FOnStdoutText;
  43.     property OnStderrText: TConsoleTextEvent read FOnStderrText write FOnStderrText;
  44.     property OnFinished: TConsoleFinishedEvent read FOnFinished write FOnFinished;
  45.   end;
  46.  
  47. implementation
  48.  
  49. uses
  50.   SysUtils,
  51.   Math;
  52.  
  53. { TConsoleRedirector }
  54.  
  55. procedure TConsoleRedirector.Close;
  56. begin
  57.   if FWorkingThread <> 0 then
  58.   begin
  59.     // this function might be called from redir thread
  60.     if GetCurrentThreadId <> FWorkingThreadId then
  61.     begin
  62.       SetEvent(FStopEvent);
  63.       if WaitForSingleObject(FWorkingThread, 5000) = WAIT_TIMEOUT then
  64.       begin
  65.         DoStdErrorEvent('The redir thread is dead');
  66.         TerminateThread(FWorkingThread, Cardinal(-2));
  67.       end;
  68.     end;
  69.     DestroyHandle(FWorkingThread);
  70.   end;
  71.  
  72.   DestroyHandle(FStopEvent);
  73.   DestroyHandle(FChildProcess);
  74.   DestroyHandle(FStdInWritePipe);
  75.   DestroyHandle(FStdOutReadPipe);
  76.   FWorkingThreadId := 0;
  77. end;
  78.  
  79. constructor TConsoleRedirector.Create;
  80. begin
  81.   inherited Create;
  82.   FStdInWritePipe := 0;
  83.   FStdOutReadPipe := 0;
  84.   FChildProcess := 0;
  85.   FWorkingThread := 0;
  86.   FStopEvent := 0;
  87.   FWorkingThreadId := 0;
  88.   FWaitTime := 100;
  89. end;
  90.  
  91. destructor TConsoleRedirector.Destroy;
  92. begin
  93.   Close;
  94.   inherited Destroy;
  95. end;
  96.  
  97. procedure TConsoleRedirector.DestroyHandle(var Handle: THandle);
  98. begin
  99.   if Handle <> 0 then
  100.   begin
  101.     CloseHandle(Handle);
  102.     Handle := 0;
  103.   end;
  104. end;
  105.  
  106. function TConsoleRedirector.LaunchChild(CommandLine: string; StdOutHandle, StdInHandle, StdErrHandle: THandle): Boolean;
  107. var
  108.   vPI: PROCESS_INFORMATION;
  109.   vSI: STARTUPINFO;
  110. begin
  111.   // Set up the start up info struct.
  112.   ZeroMemory(@vSI, SizeOf(vSI));
  113.   vSI.cb := SizeOf(vSI);
  114.   vSI.hStdOutput := StdOutHandle;
  115.   vSI.hStdInput := StdInHandle;
  116.   vSI.hStdError := StdErrHandle;
  117.   vSI.wShowWindow := SW_HIDE;
  118.   vSI.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
  119.  
  120.   // Note that dwFlags must include STARTF_USESHOWWINDOW if we
  121.   // use the wShowWindow flags. This also assumes that the
  122.   // CreateProcess() call will use CREATE_NEW_CONSOLE.
  123.  
  124.   // Launch the child process.
  125.   if not CreateProcess(nil, PChar(CommandLine), nil, nil, True, CREATE_NEW_CONSOLE, nil, nil, vSI, vPI) then
  126.     Result := False
  127.   else
  128.   begin
  129.     FChildProcess := vPI.hProcess;
  130.     // Close any unuseful handles
  131.     CloseHandle(vPI.hThread);
  132.     Result := True;
  133.   end;
  134. end;
  135.  
  136. // thread to receive output of the child process
  137. function OutputThread(lpvThreadParam: Pointer): Cardinal; stdcall;
  138. var
  139.   vWaitHandles: array [0 .. 1] of THandle;
  140.   vRedirector: TConsoleRedirector;
  141.   vWaitResult: Cardinal;
  142.   vRedirectResult: Integer;
  143. begin
  144.   vRedirector := TConsoleRedirector(lpvThreadParam);
  145.   vWaitHandles[0] := vRedirector.FChildProcess;
  146.   vWaitHandles[1] := vRedirector.FStopEvent;
  147.  
  148.   // redirect stdout till there's no more data.
  149.   vRedirectResult := vRedirector.RedirectStdout;
  150.   while vRedirectResult > 0 do
  151.   begin
  152.     // check if the child process has terminated.
  153.     vWaitResult := WaitForMultipleObjects(2, @vWaitHandles, False, vRedirector.FWaitTime);
  154.     case vWaitResult of
  155.       WAIT_OBJECT_0: // the child process ended
  156.       begin
  157.         vRedirectResult := vRedirector.RedirectStdout;
  158.         if vRedirectResult > 0 then
  159.           vRedirectResult := 0;
  160.         Break;
  161.       end;
  162.       WAIT_OBJECT_0 + 1: // m_hEvtStop was signalled
  163.       begin
  164.         vRedirectResult := 1; // cancelled
  165.         Break;
  166.       end;
  167.     end;
  168.  
  169.     // Next redirect:
  170.     vRedirectResult := vRedirector.RedirectStdout;
  171.   end;
  172.  
  173.   // close handles
  174.   vRedirector.Close;
  175.   Result := vRedirectResult;
  176. end;
  177.  
  178. function TConsoleRedirector.Open(CommandLine: string): Boolean;
  179. var
  180.   vStdOutReadPipe: THandle; // parent stdout read handle
  181.   vStdOutWritePipe, vStdErrWritePipe: THandle; // child stdout write handle
  182.   vStdInWritePipe: THandle; // parent stdin write handle
  183.   vStdInReadPipe: THandle; // child stdin read handle
  184.   vSA: SECURITY_ATTRIBUTES;
  185. begin
  186.   Close;
  187.   Result := False;
  188.   vStdOutReadPipe := 0;
  189.   vStdOutWritePipe := 0;
  190.   vStdErrWritePipe := 0;
  191.   vStdInWritePipe := 0;
  192.   vStdInReadPipe := 0;
  193.  
  194.   // Set up the security attributes struct.
  195.   vSA.nLength := SizeOf(SECURITY_ATTRIBUTES);
  196.   vSA.lpSecurityDescriptor := nil;
  197.   vSA.bInheritHandle := True;
  198.  
  199.   try
  200.     // Create a child stdout pipe.
  201.     if not CreatePipe(vStdOutReadPipe, vStdOutWritePipe, @vSA, 0) then
  202.       RaiseLastOSError;
  203.  
  204.     // Create a duplicate of the stdout write handle for the std
  205.     // error write handle. This is necessary in case the child
  206.     // application closes one of its std output handles.
  207.     if not DuplicateHandle(GetCurrentProcess, vStdOutWritePipe, GetCurrentProcess, @vStdErrWritePipe, 0, True, DUPLICATE_SAME_ACCESS) then
  208.       RaiseLastOSError;
  209.  
  210.     // Create a child stdin pipe.
  211.     if not CreatePipe(vStdInReadPipe, vStdInWritePipe, @vSA, 0) then
  212.       RaiseLastOSError;
  213.  
  214.     // Create new stdout read handle and the stdin write handle.
  215.     // Set the inheritance properties to FALSE. Otherwise, the child
  216.     // inherits the these handles; resulting in non-closeable
  217.     // handles to the pipes being created.
  218.     if not DuplicateHandle(GetCurrentProcess, vStdOutReadPipe, GetCurrentProcess, @FStdOutReadPipe, 0, False {make it uninheritable}, DUPLICATE_SAME_ACCESS) then
  219.       RaiseLastOSError;
  220.  
  221.     if not DuplicateHandle(GetCurrentProcess, vStdInWritePipe, GetCurrentProcess, @FStdInWritePipe, 0, False {make it uninheritable}, DUPLICATE_SAME_ACCESS) then
  222.       RaiseLastOSError;
  223.  
  224.     // Close inheritable copies of the handles we do not want to
  225.     // be inherited.
  226.     DestroyHandle(vStdOutReadPipe);
  227.     DestroyHandle(vStdInWritePipe);
  228.  
  229.     // launch the child process
  230.     if not LaunchChild(CommandLine, vStdOutWritePipe, vStdInReadPipe, vStdErrWritePipe) then
  231.       RaiseLastOSError;
  232.  
  233.     // Child is launched. Close the parents copy of those pipe
  234.     // handles that only the child should have open.
  235.     // Make sure that no handles to the write end of the stdout pipe
  236.     // are maintained in this process or else the pipe will not
  237.     // close when the child process exits and ReadFile will hang.
  238.     DestroyHandle(vStdOutWritePipe);
  239.     DestroyHandle(vStdInReadPipe);
  240.     DestroyHandle(vStdErrWritePipe);
  241.  
  242.     // Launch a thread to receive output from the child process.
  243.     FStopEvent := CreateEvent(nil, True, False, nil);
  244.     FWorkingThread := Windows.CreateThread(nil, 0, @OutputThread, self, 0, FWorkingThreadId);
  245.     if FWorkingThread = 0 then
  246.       RaiseLastOSError;
  247.  
  248.     Result := True;
  249.   except
  250.     on E: Exception do
  251.     begin
  252.       DestroyHandle(vStdOutReadPipe);
  253.       DestroyHandle(vStdOutWritePipe);
  254.       DestroyHandle(vStdErrWritePipe);
  255.       DestroyHandle(vStdInWritePipe);
  256.       DestroyHandle(vStdInReadPipe);
  257.       Close;
  258.       DoStdErrorEvent(AnsiString(E.Message));
  259.       //DONE: raise;
  260.     end;
  261.   end;
  262. end;
  263.  
  264. // redirect the child process's stdout:
  265. // return: 1: no more data, 0: child terminated, -1: os error
  266. function TConsoleRedirector.RedirectStdout: Integer;
  267. const
  268.   cBufferSize = 255;
  269. var
  270.   vBytesAvailable, vBytesRead, vErrorCode: Cardinal;
  271.   vBuffer: array [0 .. cBufferSize] of AnsiChar;
  272. begin
  273.   // Проверяем количество доступных байт в пайпе:
  274.   while PeekNamedPipe(FStdOutReadPipe, nil, 0, nil, @vBytesAvailable, nil) do
  275.   begin
  276.     // Если чтение успешно, но байт больше нет, то программа завершилась, выходим:
  277.     if vBytesAvailable = 0 then
  278.     begin
  279.       Result := 1; // not data available
  280.       Exit;
  281.     end;
  282.  
  283.     vBytesRead := 0;
  284.     if not ReadFile(FStdOutReadPipe, vBuffer, Min(cBufferSize, vBytesAvailable), vBytesRead, nil) or (vBytesRead = 0) then
  285.       Break; // error, the child might ended
  286.  
  287.     vBuffer[vBytesRead] := #0;
  288.     OemToAnsi(vBuffer, vBuffer);
  289.     DoStdOutEvent(vBuffer);
  290.   end;
  291.  
  292.   // pipe has been ended or pipe closing in progress:
  293.   vErrorCode := GetLastError;
  294.   if (vErrorCode = ERROR_BROKEN_PIPE) or (vErrorCode = ERROR_NO_DATA) then
  295.   begin
  296.     DoFinishedEvent;
  297.     Result := 0; // child process ended
  298.     Exit;
  299.   end;
  300.  
  301.   DoStdErrorEvent('Read stdout pipe error' + #13#10);
  302.   Result := -1; // os error
  303. end;
  304.  
  305. function TConsoleRedirector.Write(Text: AnsiString): Boolean;
  306. var
  307.   vBytesWritten: Cardinal;
  308. begin
  309.   if FStdInWritePipe <> 0 then
  310.     if Text <> '' then
  311.       Result := WriteFile(FStdInWritePipe, Text[1], Length(Text), vBytesWritten, nil)
  312.     else
  313.       Result := True
  314.   else
  315.     Result := False;
  316. end;
  317.  
  318. procedure TConsoleRedirector.DoFinishedEvent;
  319. begin
  320.   if Assigned(FOnFinished) then
  321.     FOnFinished(self);
  322. end;
  323.  
  324. procedure TConsoleRedirector.DoStdErrorEvent(Text: AnsiString);
  325. begin
  326.   if Assigned(FOnStderrText) then
  327.     FOnStderrText(self, string(Text));
  328. end;
  329.  
  330. procedure TConsoleRedirector.DoStdOutEvent(Text: AnsiString);
  331. begin
  332.   if Assigned(FOnStdoutText) then
  333.     FOnStdoutText(self, string(Text));
  334. end;
  335.  
  336. end.