问题描述
我编写了一个控制台应用程序,它能够在命令行上并行执行多个命令。
我这样做主要是出于兴趣,因为我正在处理的软件项目的构建过程过度使用命令行。
目前,在我在工作线程中创建子进程之前,我创建了一个匿名管道,以捕获子进程在其生命周期中创建的所有输出。
子进程终止后,工作线程将捕获的内容推送到等待的主进程,然后将其打印出来。
这是我的流程创建和捕获:
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,@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,True,norMAL_PRIORITY_CLASS,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;
我遇到了这个话题 Capture coloured console output into WPF application 但这对我没有帮助,因为我没有通过匿名管道接收任何颜色数据,只是纯旧文本。
我尝试通过 CreateFile with 'CONOUT$' 将主进程的控制台继承给子进程,但是虽然确实保留了颜色,但如果多个进程将其内容打印到一个和同一个控制台。
我的下一个方法是使用 CreateConsoleScreenBuffer 为每个子进程创建额外的控制台缓冲区并使用 ReadConsole 读取内容,但这并不成功,因为 ReadConsole 返回系统错误 6 (ERROR_INVALID_HANDLE)。
ConsoleHandle := CreateConsoleScreenBuffer(
GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ or FILE_SHARE_WRITE,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.
我还阅读了 virtual terminal sequences 和 AllocConsole、AttachConsole 和 FreeConsole,但无法完全理解我的用例。
保存/接收子进程控制台输出的着色信息的正确/最佳方法是什么?
解决方法
我在使用 CreateConsoleScreenBuffer
时走在正确的轨道上,并为每个线程提供了自己的控制台屏幕缓冲区。
问题是 ReadConsole
没有达到我的预期。
我现在用 ReadConsoleOutput
让它工作了。
然而,应该注意的是,这种方法是传统的做法。
如果您想以“新方式”进行操作,您可能应该使用 Pseudo Console Sessions。
它的支持从 Windows 10 1809 和 Windows Server 2019 开始。
还应该注意的是,与匿名管道相比,通过控制台屏幕缓冲区读取进程/程序输出的方法有其缺陷和两个明显的缺点:
- 控制台屏幕缓冲区无法填满并阻塞进程/程序,但如果到达末尾,新行会将当前第一行推出缓冲区。
- 以快速方式向其 std 输出发送垃圾邮件的进程/程序的输出很可能会导致信息丢失,因为您将无法足够快地读取、清除和移动控制台屏幕缓冲区中的光标。
我试图通过将控制台屏幕缓冲区 y 大小组件增加到其最大可能大小(我发现它是 MAXSHORT - 1
)并等到进程/程序完成来规避两者。
这对我来说已经足够了,因为我不需要分析或处理彩色输出,而只需将其显示在控制台窗口中,控制台窗口本身仅限于 MAXSHORT - 1
行。
在其他情况下,我将使用管道并建议其他人也这样做!
这是一个没有任何错误处理的简短版本,可以在没有干扰的情况下并行执行(前提是 TStream 对象由线程拥有或线程安全):
procedure CreateProcessWithConsoleCapture(const aAppName,aCMD,aDefaultDir: PChar;
const CapturedOutput: TStream);
const
CONSOLE_SCREEN_BUFFER_SIZE_Y = MAXSHORT - 1;
var
SecurityAttributes: TSecurityAttributes;
ConsoleHandle: THandle;
StartupInfo: TStartupInfo;
ProcessInformation: TProcessInformation;
CharsRead: Cardinal;
BufferSize,Origin: TCoord;
ConsoleScreenBufferInfo: TConsoleScreenBufferInfo;
Buffer: array of TCharInfo;
ReadRec: TSmallRect;
begin
SecurityAttributes.nLength := SizeOf(SecurityAttributes);
SecurityAttributes.lpSecurityDescriptor := Nil;
SecurityAttributes.bInheritHandle := True;
ConsoleHandle := CreateConsoleScreenBuffer(
GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ or FILE_SHARE_WRITE,@SecurityAttributes,CONSOLE_TEXTMODE_BUFFER,nil);
try
GetConsoleScreenBufferInfo(ConsoleHandle,ConsoleScreenBufferInfo);
BufferSize.X := ConsoleScreenBufferInfo.dwSize.X;
BufferSize.Y := CONSOLE_SCREEN_BUFFER_SIZE_Y;
SetConsoleScreenBufferSize(ConsoleHandle,BufferSize);
Origin.X := 0;
Origin.Y := 0;
FillConsoleOutputCharacter(ConsoleHandle,#0,BufferSize.X * BufferSize.Y,Origin,CharsRead);
SetStdHandle(STD_OUTPUT_HANDLE,ConsoleHandle);
FillChar(StartupInfo,Sizeof(StartupInfo),0);
StartupInfo.cb := SizeOf(StartupInfo);
StartupInfo.hStdOutput := ConsoleHandle;
StartupInfo.hStdError := ConsoleHandle;
StartupInfo.hStdInput := GetStdHandle(STD_INPUT_HANDLE);
StartupInfo.dwFlags := STARTF_USESTDHANDLES or STARTF_FORCEOFFFEEDBACK;
CreateProcess(aAppName,True,NORMAL_PRIORITY_CLASS,nil,aDefaultDir,StartupInfo,ProcessInformation);
try
WaitForSingleObject(ProcessInformation.hProcess,INFINITE);
GetConsoleScreenBufferInfo(ConsoleHandle,ConsoleScreenBufferInfo);
BufferSize.X := ConsoleScreenBufferInfo.dwSize.X;
BufferSize.Y := ConsoleScreenBufferInfo.dwCursorPosition.Y;
if ConsoleScreenBufferInfo.dwCursorPosition.X > 0 then
begin
Inc(BufferSize.Y);
end;
ReadRec.Left := 0;
ReadRec.Top := 0;
ReadRec.Right := BufferSize.X - 1;
ReadRec.Bottom := BufferSize.Y - 1;
SetLength(Buffer,BufferSize.X * BufferSize.Y);
ReadConsoleOutput(ConsoleHandle,@Buffer[0],BufferSize,ReadRec);
CharsRead := SizeOf(TCharInfo) * (ReadRec.Right - ReadRec.Left + 1) * (ReadRec.Bottom - ReadRec.Top + 1);
if CharsRead > 0 then
begin
CapturedOutput.Size := CapturedOutput.Size + CharsRead;
CapturedOutput.WriteBuffer(Buffer[0],CharsRead);
end;
finally
CloseHandle(ProcessInformation.hProcess);
CloseHandle(ProcessInformation.hThread);
end;
finally
CloseHandle(ConsoleHandle);
end;
end;