簡體   English   中英

從多個來源捕獲彩色控制台 output

[英]Capture coloured console output from multiple sources

我編寫了一個控制台應用程序,它能夠在命令行上並行執行多個命令。
我這樣做主要是出於興趣,因為我正在處理的軟件項目的構建過程過度使用命令行。

目前,在工作線程中創建子進程之前,我創建了一個匿名 pipe 以捕獲子進程在其生命周期內創建的所有 output。
子進程終止后,工作線程將捕獲的內容推送到等待的主進程,然后將其打印出來。

這是我的流程創建和捕獲:

    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;

我現在的問題:
此方法不會保留捕獲輸出的潛在顏色!

我遇到了這個主題將彩色控制台 output 捕獲到 WPF 應用程序中,但這對我沒有幫助,因為我沒有通過匿名 Z20826A3CB51D6C7D9C219C7F4BFZBFZ 接收任何顏色數據,只是純舊文本。

我嘗試通過帶有 'CONOUT$' 的 CreateFile將主進程的控制台繼承給子進程,但是雖然確實保留了 colors,但如果多個進程將其內容打印成一個並且同一個控制台。

我的下一個方法是使用CreateConsoleScreenBuffer為每個子進程創建額外的控制台緩沖區,並使用ReadConsole讀取內容,但這並不成功,因為 ReadConsole 返回系統錯誤 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.

我還閱讀了虛擬終端序列AllocConsoleAttachConsoleFreeConsole ,但對於我的用例,我無法完全理解它。

保存/接收子進程控制台 output 的着色信息的正確/最佳方法是什么?

我使用CreateConsoleScreenBuffer走在正確的軌道上,並為每個線程提供了自己的控制台屏幕緩沖區。
問題是ReadConsole沒有達到我的預期。
我現在可以使用ReadConsoleOutput了。

但是應該注意的是,這種方法是傳統的方法。 如果您想以“新方式”進行操作,您可能應該使用Pseudo Console Sessions
它的支持從 Windows 10 1809 和 Windows Server 2019 開始。

還應該注意的是,與匿名管道相比,通過控制台屏幕緩沖區讀取進程/程序的 output 的方法有其缺陷和兩個明顯的缺點:

  1. 控制台屏幕緩沖區無法填滿並阻塞進程/程序,但如果到達末尾,新行會將當前第一行推出緩沖區。
  2. Output 來自以快速方式向其標准 output 發送垃圾郵件的進程/程序的 Output 很可能會導致信息丟失,因為您將無法足夠快地讀取、清除和移動控制台屏幕緩沖區中的 Z1791A97A8403730EE0760489

我嘗試通過將控制台屏幕緩沖區 y 大小組件增加到其最大可能大小(我發現它是MAXSHORT - 1 )來規避這兩種情況,然后等到進程/程序完成。
這對我來說已經足夠好了,因為我不需要分析或處理彩色 output,只需將其顯示在控制台 window 中,它本身僅限於MAXSHORT - 1行。
在所有其他情況下,我將使用管道並建議其他人也這樣做!

這是一個沒有任何錯誤處理的簡短版本,可以在沒有干擾的情況下並行執行(假設 TStream object 由線程擁有或線程安全):

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, aCMD,
      @SecurityAttributes, @SecurityAttributes,
      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, Origin, 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;

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM