- unit Utils.ConsoleRedirector;
- interface
- uses
- Windows;
- type
- TConsoleRedirector = class;
- TConsoleFinishedEvent = procedure(Sender: TConsoleRedirector) of object;
- TConsoleTextEvent = procedure(Sender: TConsoleRedirector; Text: string) of object;
- TConsoleRedirector = class
- private
- FWorkingThread: THandle; // thread to receive the output of the child process
- FStopEvent: THandle; // event to notify the redir thread to exit
- FWorkingThreadId: Cardinal; // id of the redir thread
- FWaitTime: Cardinal;
- FOnStdoutText: TConsoleTextEvent;
- FOnStderrText: TConsoleTextEvent;
- FOnFinished: TConsoleFinishedEvent; // wait time to check the status of the child process
- FStdInWritePipe: THandle; // write end of child's stdin pipe
- FStdOutReadPipe: THandle; // read end of child's stdout pipe
- FChildProcess: THandle;
- protected
- function LaunchChild(CommandLine: string; StdOutHandle, StdInHandle, StdErrHandle: THandle): Boolean;
- function RedirectStdout: Integer;
- procedure DestroyHandle(var Handle: THandle);
- procedure DoStdOutEvent(Text: AnsiString); virtual;
- procedure DoStdErrorEvent(Text: AnsiString); virtual;
- procedure DoFinishedEvent; virtual;
- public
- constructor Create;
- destructor Destroy; override;
- function Open(CommandLine: string): Boolean;
- procedure Close; virtual;
- function Write(Text: AnsiString): Boolean;
- property WaitTime: Cardinal read FWaitTime write FWaitTime;
- property OnStdoutText: TConsoleTextEvent read FOnStdoutText write FOnStdoutText;
- property OnStderrText: TConsoleTextEvent read FOnStderrText write FOnStderrText;
- property OnFinished: TConsoleFinishedEvent read FOnFinished write FOnFinished;
- end;
- implementation
- uses
- SysUtils,
- Math;
- { TConsoleRedirector }
- procedure TConsoleRedirector.Close;
- begin
- if FWorkingThread <> 0 then
- begin
- // this function might be called from redir thread
- if GetCurrentThreadId <> FWorkingThreadId then
- begin
- SetEvent(FStopEvent);
- if WaitForSingleObject(FWorkingThread, 5000) = WAIT_TIMEOUT then
- begin
- DoStdErrorEvent('The redir thread is dead');
- TerminateThread(FWorkingThread, Cardinal(-2));
- end;
- end;
- DestroyHandle(FWorkingThread);
- end;
- DestroyHandle(FStopEvent);
- DestroyHandle(FChildProcess);
- DestroyHandle(FStdInWritePipe);
- DestroyHandle(FStdOutReadPipe);
- FWorkingThreadId := 0;
- end;
- constructor TConsoleRedirector.Create;
- begin
- inherited Create;
- FStdInWritePipe := 0;
- FStdOutReadPipe := 0;
- FChildProcess := 0;
- FWorkingThread := 0;
- FStopEvent := 0;
- FWorkingThreadId := 0;
- FWaitTime := 100;
- end;
- destructor TConsoleRedirector.Destroy;
- begin
- Close;
- inherited Destroy;
- end;
- procedure TConsoleRedirector.DestroyHandle(var Handle: THandle);
- begin
- if Handle <> 0 then
- begin
- CloseHandle(Handle);
- Handle := 0;
- end;
- end;
- function TConsoleRedirector.LaunchChild(CommandLine: string; StdOutHandle, StdInHandle, StdErrHandle: THandle): Boolean;
- var
- vPI: PROCESS_INFORMATION;
- vSI: STARTUPINFO;
- begin
- // Set up the start up info struct.
- ZeroMemory(@vSI, SizeOf(vSI));
- vSI.cb := SizeOf(vSI);
- vSI.hStdOutput := StdOutHandle;
- vSI.hStdInput := StdInHandle;
- vSI.hStdError := StdErrHandle;
- vSI.wShowWindow := SW_HIDE;
- vSI.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
- // Note that dwFlags must include STARTF_USESHOWWINDOW if we
- // use the wShowWindow flags. This also assumes that the
- // CreateProcess() call will use CREATE_NEW_CONSOLE.
- // Launch the child process.
- if not CreateProcess(nil, PChar(CommandLine), nil, nil, True, CREATE_NEW_CONSOLE, nil, nil, vSI, vPI) then
- Result := False
- else
- begin
- FChildProcess := vPI.hProcess;
- // Close any unuseful handles
- CloseHandle(vPI.hThread);
- Result := True;
- end;
- end;
- // thread to receive output of the child process
- function OutputThread(lpvThreadParam: Pointer): Cardinal; stdcall;
- var
- vWaitHandles: array [0 .. 1] of THandle;
- vRedirector: TConsoleRedirector;
- vWaitResult: Cardinal;
- vRedirectResult: Integer;
- begin
- vRedirector := TConsoleRedirector(lpvThreadParam);
- vWaitHandles[0] := vRedirector.FChildProcess;
- vWaitHandles[1] := vRedirector.FStopEvent;
- // redirect stdout till there's no more data.
- vRedirectResult := vRedirector.RedirectStdout;
- while vRedirectResult > 0 do
- begin
- // check if the child process has terminated.
- vWaitResult := WaitForMultipleObjects(2, @vWaitHandles, False, vRedirector.FWaitTime);
- case vWaitResult of
- WAIT_OBJECT_0: // the child process ended
- begin
- vRedirectResult := vRedirector.RedirectStdout;
- if vRedirectResult > 0 then
- vRedirectResult := 0;
- Break;
- end;
- WAIT_OBJECT_0 + 1: // m_hEvtStop was signalled
- begin
- vRedirectResult := 1; // cancelled
- Break;
- end;
- end;
- // Next redirect:
- vRedirectResult := vRedirector.RedirectStdout;
- end;
- // close handles
- vRedirector.Close;
- Result := vRedirectResult;
- end;
- function TConsoleRedirector.Open(CommandLine: string): Boolean;
- var
- vStdOutReadPipe: THandle; // parent stdout read handle
- vStdOutWritePipe, vStdErrWritePipe: THandle; // child stdout write handle
- vStdInWritePipe: THandle; // parent stdin write handle
- vStdInReadPipe: THandle; // child stdin read handle
- vSA: SECURITY_ATTRIBUTES;
- begin
- Close;
- Result := False;
- vStdOutReadPipe := 0;
- vStdOutWritePipe := 0;
- vStdErrWritePipe := 0;
- vStdInWritePipe := 0;
- vStdInReadPipe := 0;
- // Set up the security attributes struct.
- vSA.nLength := SizeOf(SECURITY_ATTRIBUTES);
- vSA.lpSecurityDescriptor := nil;
- vSA.bInheritHandle := True;
- try
- // Create a child stdout pipe.
- if not CreatePipe(vStdOutReadPipe, vStdOutWritePipe, @vSA, 0) then
- RaiseLastOSError;
- // Create a duplicate of the stdout write handle for the std
- // error write handle. This is necessary in case the child
- // application closes one of its std output handles.
- if not DuplicateHandle(GetCurrentProcess, vStdOutWritePipe, GetCurrentProcess, @vStdErrWritePipe, 0, True, DUPLICATE_SAME_ACCESS) then
- RaiseLastOSError;
- // Create a child stdin pipe.
- if not CreatePipe(vStdInReadPipe, vStdInWritePipe, @vSA, 0) then
- RaiseLastOSError;
- // Create new stdout read handle and the stdin write handle.
- // Set the inheritance properties to FALSE. Otherwise, the child
- // inherits the these handles; resulting in non-closeable
- // handles to the pipes being created.
- if not DuplicateHandle(GetCurrentProcess, vStdOutReadPipe, GetCurrentProcess, @FStdOutReadPipe, 0, False {make it uninheritable}, DUPLICATE_SAME_ACCESS) then
- RaiseLastOSError;
- if not DuplicateHandle(GetCurrentProcess, vStdInWritePipe, GetCurrentProcess, @FStdInWritePipe, 0, False {make it uninheritable}, DUPLICATE_SAME_ACCESS) then
- RaiseLastOSError;
- // Close inheritable copies of the handles we do not want to
- // be inherited.
- DestroyHandle(vStdOutReadPipe);
- DestroyHandle(vStdInWritePipe);
- // launch the child process
- if not LaunchChild(CommandLine, vStdOutWritePipe, vStdInReadPipe, vStdErrWritePipe) then
- RaiseLastOSError;
- // Child is launched. Close the parents copy of those pipe
- // handles that only the child should have open.
- // Make sure that no handles to the write end of the stdout pipe
- // are maintained in this process or else the pipe will not
- // close when the child process exits and ReadFile will hang.
- DestroyHandle(vStdOutWritePipe);
- DestroyHandle(vStdInReadPipe);
- DestroyHandle(vStdErrWritePipe);
- // Launch a thread to receive output from the child process.
- FStopEvent := CreateEvent(nil, True, False, nil);
- FWorkingThread := Windows.CreateThread(nil, 0, @OutputThread, self, 0, FWorkingThreadId);
- if FWorkingThread = 0 then
- RaiseLastOSError;
- Result := True;
- except
- on E: Exception do
- begin
- DestroyHandle(vStdOutReadPipe);
- DestroyHandle(vStdOutWritePipe);
- DestroyHandle(vStdErrWritePipe);
- DestroyHandle(vStdInWritePipe);
- DestroyHandle(vStdInReadPipe);
- Close;
- DoStdErrorEvent(AnsiString(E.Message));
- //DONE: raise;
- end;
- end;
- end;
- // redirect the child process's stdout:
- // return: 1: no more data, 0: child terminated, -1: os error
- function TConsoleRedirector.RedirectStdout: Integer;
- const
- cBufferSize = 255;
- var
- vBytesAvailable, vBytesRead, vErrorCode: Cardinal;
- vBuffer: array [0 .. cBufferSize] of AnsiChar;
- begin
- // Проверяем количество доступных байт в пайпе:
- while PeekNamedPipe(FStdOutReadPipe, nil, 0, nil, @vBytesAvailable, nil) do
- begin
- // Если чтение успешно, но байт больше нет, то программа завершилась, выходим:
- if vBytesAvailable = 0 then
- begin
- Result := 1; // not data available
- Exit;
- end;
- vBytesRead := 0;
- if not ReadFile(FStdOutReadPipe, vBuffer, Min(cBufferSize, vBytesAvailable), vBytesRead, nil) or (vBytesRead = 0) then
- Break; // error, the child might ended
- vBuffer[vBytesRead] := #0;
- OemToAnsi(vBuffer, vBuffer);
- DoStdOutEvent(vBuffer);
- end;
- // pipe has been ended or pipe closing in progress:
- vErrorCode := GetLastError;
- if (vErrorCode = ERROR_BROKEN_PIPE) or (vErrorCode = ERROR_NO_DATA) then
- begin
- DoFinishedEvent;
- Result := 0; // child process ended
- Exit;
- end;
- DoStdErrorEvent('Read stdout pipe error' + #13#10);
- Result := -1; // os error
- end;
- function TConsoleRedirector.Write(Text: AnsiString): Boolean;
- var
- vBytesWritten: Cardinal;
- begin
- if FStdInWritePipe <> 0 then
- if Text <> '' then
- Result := WriteFile(FStdInWritePipe, Text[1], Length(Text), vBytesWritten, nil)
- else
- Result := True
- else
- Result := False;
- end;
- procedure TConsoleRedirector.DoFinishedEvent;
- begin
- if Assigned(FOnFinished) then
- FOnFinished(self);
- end;
- procedure TConsoleRedirector.DoStdErrorEvent(Text: AnsiString);
- begin
- if Assigned(FOnStderrText) then
- FOnStderrText(self, string(Text));
- end;
- procedure TConsoleRedirector.DoStdOutEvent(Text: AnsiString);
- begin
- if Assigned(FOnStdoutText) then
- FOnStdoutText(self, string(Text));
- end;
- end.