I wrote a console application that is able to execute multiple commands on the command line in parallel.
Primarily I did this out of interest and because the build processes of the software projects I am working on make excessive use of the command line.
Currently, before I create a child process in a worker thread, I create an anonymous pipe in order to capture all the output the child process creates during its lifetime.
After the child process terminates, the worker thread pushes the captured content to the waiting main process that then prints it out.
Here's my process creations and capturing:
procedure ReadPipe(const ReadHandle: THandle; const Output: TStream);
var
Buffer: TMemoryStream;
BytesRead, BytesToRead: DWord;
begin
Buffer := TMemoryStream.Create;
try
BytesRead := 0;
BytesToRead := 0;
if PeekNamedPipe(ReadHandle, nil, 0, nil, @BytesToRead, nil) then
begin
if BytesToRead > 0 then
begin
Buffer.Size := BytesToRead;
ReadFile(ReadHandle, Buffer.Memory^, Buffer.Size, BytesRead, nil);
if Buffer.Size <> BytesRead then
begin
Buffer.Size := BytesRead;
end;
if Buffer.Size > 0 then
begin
Output.Size := Output.Size + Buffer.Size;
Output.WriteBuffer(Buffer.Memory^, Buffer.Size);
end;
end;
end;
finally
Buffer.Free;
end;
end;
function CreateProcessWithRedirectedOutput(const AppName, CMD, DefaultDir: PChar; out CapturedOutput: String): Cardinal;
const
TIMEOUT_UNTIL_NEXT_PIPEREAD = 100;
var
SecurityAttributes: TSecurityAttributes;
ReadHandle, WriteHandle: THandle;
StartupInfo: TStartupInfo;
ProcessInformation: TProcessInformation;
ProcessStatus: Cardinal;
Output: TStringStream;
begin
Result := 0;
CapturedOutput := '';
Output := TStringStream.Create;
try
SecurityAttributes.nLength := SizeOf(SecurityAttributes);
SecurityAttributes.lpSecurityDescriptor := nil;
SecurityAttributes.bInheritHandle := True;
if CreatePipe(ReadHandle, WriteHandle, @SecurityAttributes, 0) then
begin
try
FillChar(StartupInfo, Sizeof(StartupInfo), 0);
StartupInfo.cb := SizeOf(StartupInfo);
StartupInfo.hStdOutput := WriteHandle;
StartupInfo.hStdError := WriteHandle;
StartupInfo.hStdInput := GetStdHandle(STD_INPUT_HANDLE);
StartupInfo.dwFlags := STARTF_USESTDHANDLES;
if CreateProcess(AppName, CMD,
@SecurityAttributes, @SecurityAttributes,
True, NORMAL_PRIORITY_CLASS,
nil, DefaultDir,
StartupInfo, ProcessInformation)
then
begin
try
repeat
ProcessStatus := WaitForSingleObject(ProcessInformation.hProcess, TIMEOUT_UNTIL_NEXT_PIPEREAD);
ReadPipe(ReadHandle, Output);
until ProcessStatus <> WAIT_TIMEOUT;
if not Windows.GetExitCodeProcess(ProcessInformation.hProcess, Result) then
begin
Result := GetLastError;
end;
finally
Windows.CloseHandle(ProcessInformation.hProcess);
Windows.CloseHandle(ProcessInformation.hThread);
end;
end
else
begin
Result := GetLastError;
end;
finally
Windows.CloseHandle(ReadHandle);
Windows.CloseHandle(WriteHandle);
end;
end
else
begin
Result := GetLastError;
end;
CapturedOutput := Output.DataString;
finally
Output.Free;
end;
end;
My problem now:
This method doesn't preserve potential coloring of the captured output!
I came accross this topic Capture coloured console output into WPF application but that didn't help me out, as I don't receive any color data through the anonymous pipe, just plain old text.
I experimented with inheriting the console of the main process to the child processes via CreateFile with 'CONOUT$', but while the colors are indeed preserved, you probably can guess that its pure mayhem if more than one process prints out its contents into one and the same console.
My next approach was to create additional console buffers with CreateConsoleScreenBuffer for each child process and read the contents with ReadConsole, but that wasn't successful as ReadConsole returns with System Error 6 (ERROR_INVALID_HANDLE).
ConsoleHandle := CreateConsoleScreenBuffer(
GENERIC_READ or GENERIC_WRITE,
FILE_SHARE_READ or FILE_SHARE_WRITE,
@SecurityAttributes,
CONSOLE_TEXTMODE_BUFFER,
nil);
//...
StartupInfo.hStdOutput := ConsoleHandle;
StartupInfo.hStdError := ConsoleHandle;
//...
ConsoleOutput := TMemoryStream.Create
ConsoleOutput.Size := MAXWORD;
ConsoleOutput.Position := 0;
ReadConsole(ConsoleHandle, ConsoleOutput.Memory, ConsoleOutput.Size, CharsRead, nil) // Doesn't read anything and returns with System Error Code 6.
I also read up on virtual terminal sequences and AllocConsole, AttachConsole and FreeConsole, but can't quite wrap my head around it for my use case.
What is the right/best way to preserve/receive coloring information of the console output of a child process?