如何在Delphi中实时读取cygwin程序的命令行输出?

How to read command-line output of cygwin program in real-time in Delphi?

我需要阅读最初基于 Linux 的 Cygwin 程序的冗长命令行输出。它在 cmd.exe 下运行良好,每隔几秒打印一次新行。

当我在下面使用这段代码时,在 SO 上多次讨论过,ReadFile 函数在程序停止之前不会 return。然后所有输出由 ReadFile 提供并打印。

如何让 ReadFile 尽快读取该输出?

MSDN 说 ReadFile 不会 return 直到在 ENABLE_LINE_INPUT 模式下达到 CR 或缓冲区已满。该程序使用 Linux 换行符 LF,而不是 Windows CRLF。我使用了 32 字节的小缓冲区并禁用了 ENABLE_LINE_INPUT顺便问一下禁用它的正确方法是什么?)。

也许 ReadFile 不会 return 因为 Cygwin 程序本身的一些其他问题,而不仅仅是 LF 换行符?但它在 Windows cmd.exe 中工作正常,为什么在 Delphi 控制台应用程序中不行?

const
  CommandExe:string = 'iperf3.exe ';
  CommandLine:string = '-c 192.168.1.11 -u -b 1m -t 8 -p 5001 -l 8k -f m -i 2';
  WorkDir:string = 'D:\PAS\iperf3\win32';// no trailing \
var
  SA: TSecurityAttributes;
  SI: TStartupInfo;
  PI: TProcessInformation;
  StdOutPipeRead, StdOutPipeWrite: THandle;
  WasOK,CreateOk: Boolean;
  Buffer: array[0..255] of AnsiChar;//  31 is Ok
  BytesRead: Cardinal;
  Line:ansistring;

  try// except
  with SA do begin
    nLength := SizeOf(SA);
    bInheritHandle := True;
    lpSecurityDescriptor := nil;
  end;
  CreatePipe(StdOutPipeRead, StdOutPipeWrite, @SA, 0);
  try
    with SI do
    begin
      FillChar(SI, SizeOf(SI), 0);
      cb := SizeOf(SI);
      dwFlags := STARTF_USESHOWWINDOW or STARTF_USESTDHANDLES;
      wShowWindow := SW_HIDE;
      hStdInput := GetStdHandle(STD_INPUT_HANDLE); // don't redirect stdin
      hStdOutput := StdOutPipeWrite;
      hStdError := StdOutPipeWrite;
    end;
    Writeln(WorkDir+'\'+CommandExe+' ' + CommandLine);
    CreateOk := CreateProcess(nil, PChar(WideString(WorkDir+'\'+CommandExe+' ' + CommandLine)),
                              @SA, @SA, True,// nil, nil,
                              CREATE_SUSPENDED or CREATE_NEW_PROCESS_GROUP or NORMAL_PRIORITY_CLASS or CREATE_DEFAULT_ERROR_MODE,// 0,
                              nil,
                              PChar(WideString(WorkDir)), SI, PI);
    CloseHandle(StdOutPipeWrite);// must be closed here otherwise ReadLn further doesn't work
    ResumeThread(PI.hThread);
    if CreateOk then
      try// finally
        repeat
          WasOK := ReadFile(StdOutPipeRead, Buffer, SizeOf(Buffer), BytesRead, nil);
          if BytesRead > 0 then
          begin
            Buffer[BytesRead] := #0;
            Line := Line + Buffer;
            Writeln(Line);
          end;
        until not WasOK or (BytesRead = 0);
        ReadLn;
        WaitForSingleObject(PI.hProcess, INFINITE);
      finally
        CloseHandle(PI.hThread);
        CloseHandle(PI.hProcess);
      end;
  finally
    CloseHandle(StdOutPipeRead);
  end;
  except
    on E: Exception do
      Writeln('Exception '+E.ClassName, ': ', E.Message);
  end;

另外:为什么我们必须在 CreateProcess 之后立即关闭这个句柄?用于读取程序输出:

CloseHandle(StdOutPipeWrite);

如果我在程序结束时关闭它,程序输出正常,但永远不会读取 ReadLn 来停止程序。

如何测试所有这些: 在一个命令 window 中启动 iperf3 服务器并让它监听:

D:\PAS\iperf3\win32>iperf3.exe -s -i 2 -p 5001
-----------------------------------------------------------
Server listening on 5001
-----------------------------------------------------------

在另一个命令 window 中启动客户端,它会立即连接到服务器并开始每 2 秒打印一次输出:

D:\PAS\iperf3\win32>iperf3.exe -c 192.168.1.11 -u -b 1m -t 8 -p 5001 -l 8k -f m -i 2
Connecting to host 192.168.1.11, port 5001
[  4] local 192.168.1.11 port 52000 connected to 192.168.1.11 port 5001
[ ID] Interval           Transfer     Bandwidth       Total Datagrams
[  4]   0.00-2.00   sec   240 KBytes  0.98 Mbits/sec  30
[  4]   2.00-4.00   sec   240 KBytes  0.98 Mbits/sec  30
[  4]   4.00-6.00   sec   248 KBytes  1.02 Mbits/sec  31
[  4]   6.00-8.00   sec   240 KBytes  0.98 Mbits/sec  30
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bandwidth       Jitter    Lost/Total Datagrams
[  4]   0.00-8.00   sec   968 KBytes  0.99 Mbits/sec  0.074 ms  0/121 (0%)
[  4] Sent 121 datagrams
iperf Done.

服务器也与客户端一起打印输出:

Accepted connection from 192.168.1.11, port 36719
[  5] local 192.168.1.11 port 5001 connected to 192.168.1.11 port 52000
[ ID] Interval           Transfer     Bandwidth       Jitter    Lost/Total Datagrams
[  5]   0.00-2.00   sec   240 KBytes   983 Kbits/sec  0.052 ms  0/30 (0%)
[  5]   2.00-4.00   sec   240 KBytes   983 Kbits/sec  0.072 ms  0/30 (0%)
[  5]   4.00-6.00   sec   248 KBytes  1.02 Mbits/sec  0.077 ms  0/31 (0%)
[  5]   6.00-8.00   sec   240 KBytes   983 Kbits/sec  0.074 ms  0/30 (0%)
[  5]   8.00-8.00   sec  0.00 Bytes  0.00 bits/sec  0.074 ms  0/0 (nan%)
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bandwidth       Jitter    Lost/Total Datagrams
[  5]   0.00-8.00   sec  0.00 Bytes  0.00 bits/sec  0.074 ms  0/121 (0%)
-----------------------------------------------------------
Server listening on 5001
-----------------------------------------------------------

因此 iperf3 客户端在命令 window 中运行良好。现在让我们在客户端模式下启动 "my" 代码,同时 iperf3 服务器仍在侦听。服务器接受连接并开始打印输出

Accepted connection from 192.168.1.11, port 36879
[  5] local 192.168.1.11 port 5001 connected to 192.168.1.11 port 53069
[ ID] Interval           Transfer     Bandwidth       Jitter    Lost/Total Datagrams
[  5]   0.00-2.00   sec   240 KBytes   983 Kbits/sec  0.033 ms  0/30 (0%)
[  5]   2.00-4.00   sec   240 KBytes   983 Kbits/sec  0.125 ms  0/30 (0%)
[  5]   4.00-6.00   sec   248 KBytes  1.02 Mbits/sec  0.106 ms  0/31 (0%)
[  5]   6.00-8.00   sec   240 KBytes   983 Kbits/sec  0.109 ms  0/30 (0%)
[  5]   8.00-8.00   sec  0.00 Bytes  0.00 bits/sec  0.109 ms  0/0 (nan%)
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bandwidth       Jitter    Lost/Total Datagrams
[  5]   0.00-8.00   sec  0.00 Bytes  0.00 bits/sec  0.109 ms  0/121 (0%)
-----------------------------------------------------------
Server listening on 5001
-----------------------------------------------------------

这意味着 iperf3 客户端在 'my' 代码中启动,但它没有打印任何东西!仅在客户端完成后,'my' 代码打印此输出:

Connecting to host 192.168.1.11, port 5001
[  4] local 192.168.1.11 port 53069 connected to 192.168.1.11 port 5001
[ ID] Interval           Transfer     Bandwidth       Total Datagrams
[  4]   0.00-2.00   sec   240 KBytes  0.98 Mbits/sec  30
[  4]   2.00-4.00   sec   240 KBytes  0.98 Mbits/sec  30
[  4]   4.00-6.00   sec   248 KBytes  1.02 Mbits/sec  31
[  4]   6.00-8.00   sec   240 KBytes  0.98 Mbits/sec  30
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bandwidth       Jitter    Lost/Total Datagrams
[  4]   0.00-8.00   sec   968 KBytes  0.99 Mbits/sec  0.109 ms  0/121 (0%)
[  4] Sent 121 datagrams
iperf Done.

因此,cygwin 程序输出的行为不同,这取决于它是在命令 window 还是 Delphi 控制台应用程序中运行。 是的,我使用 'Line' 的输出处理代码并不完美,但让我们找出如何实时制作 ReadFile return,我会修复其余部分。

How to make that output read by ReadFile as soon as it is available?

问题不在您提供的代码中。它已经在实时读取输出(虽然还有一个与代码无关的问题,见下文).

您可以尝试使用以下批处理文件代替 Cygwin 可执行文件:

test.bat:

timeout 5
echo "1"
timeout 5
echo "2"
timeout 5
echo "3"

和以下 bash shell 文件:

test.sh:

sleep 5
echo "1"
sleep 5
echo "2"
sleep 5
echo "3"

它实时工作并在可用时立即将文本输出到控制台。

所以如果问题不在Delphi代码中,则与Cygwin程序有关。 我们需要有关您的 Cygwin 程序的更多信息以进一步帮助您。

MSDN says that ReadFile doesn't return until CR is reached in ENABLE_LINE_INPUT mode, or buffer full. That progam uses linux line breaks LF, not Windows CR LF. I used small buffer 32 bytes, disabled ENABLE_LINE_INPUT - btw what's the right way of disabling it?

您不需要禁用它。

如果您已将缓冲区设置为 32 字节,那么一旦缓冲区已满,ReadFile 函数应该 return 那 32 字节,即使是 UNIX 行结尾。

Maybe ReadFile doesn't return because of some other issue with cygwin program itself, not just LF line breaks?

这就是我的猜测。可能的原因我不想去猜测,但是跟行尾的不同没有关系。

是的,非Windows 行结尾可以使命令等待整个缓冲区被填满,但不会导致 ReadFile 阻塞。

But it works fine in Windows cmd.exe, why not in Delphi console application?

好问题,这很奇怪。在我这边,它在 Delphi 和 cmd 中都有效。 这就是为什么我认为问题与 Cygwin 应用程序有关。

Also: why do we have to close this handle right after CreateProcess? CloseHandle(StdOutPipeWrite);

这是管道的写入端。我们不需要写入句柄,因为我们没有写入管道,我们只是从中读取。 您的 Cygwin 应用程序正在间接写入该管道。


另外,代码中还有两个问题需要注意:

  • 您有一个 Line 变量,它是字符串类型并且没有被初始化。 在 routine/program.

  • 的开头将其初始化为空字符串 (Line := '')
  • 因为您有以 Buffer 结尾的 UNIX 行,ReadFile 不会 return 除非缓冲区已满,因此包含多行。 您需要将对 WriteLn 例程的调用更改为 Write 并忽略行结尾,或者使用分隔行的解析器。

  • Line 变量应该在写入 stdout 后被清除,或者应该直接接收 Buffer 的值,像这样:

    ...
    Buffer[BytesRead] := #0;
    Line := Buffer; // <- Assign directly to Line, do not concatenate
    
    // TODO: Use a parser to separate the multiple lines
    //       in `Line` and output then with `WriteLn` or
    //       ignore line endings altogether and just use `Write`
    
    Write(Line);
    ...
    

    除非你这样做 Line 的大小会逐渐增加直到它包含整个输出, 复制.

这是解决方案的总结,在此感谢专家们的建议:

许多 unix 诞生的程序,可以在 Windows 中使用 Cygwin 包启动,注意它们输出的目的地。如果 stdOut 是到控制台,则输出是 EOL 缓冲的。这意味着一旦新行准备就绪,它就会被打印出来,不管它是如何分隔的:CR 或 CR+LF。如果 stdOut 是管道或文件或其他东西,输出是 EOF 缓冲的,因为人没有看屏幕。这意味着程序完成时会打印所有多行(除非我们使用 'flush',但大概我们没有源代码)。在这种情况下,我们会丢失所有实时信息。

检查这段代码很容易(从最上面开始定义),把它放在 CreateProcess 之后:

    case GetFileType(SI.hStdInput) of
     FILE_TYPE_UNKNOWN:Lines.Add('Input Unknown') ;
     FILE_TYPE_DISK:Lines.Add('Input from a File') ;
     FILE_TYPE_CHAR:Lines.Add('Input from a Console') ;
     FILE_TYPE_PIPE:Lines.Add('Input from a Pipe') ;
    end;
    case GetFileType(SI.hStdOutput) of
     FILE_TYPE_UNKNOWN:Lines.Add('Output Unknown') ;
     FILE_TYPE_DISK:Lines.Add('Output to a File') ;
     FILE_TYPE_CHAR:Lines.Add('Output to a Console') ;
     FILE_TYPE_PIPE:Lines.Add('Output to a Pipe') ;
   end;

如果您这样设置控制台 I/O:

  hStdInput := GetStdHandle(STD_INPUT_HANDLE);
  hStdOutput := GetStdHandle(STD_OUTPUT_HANDLE);
  hStdError := GetStdHandle(STD_OUTPUT_HANDLE);

输出到控制台。如果你这样设置:

  hStdInput :=GetStdHandle(STD_INPUT_HANDLE);
  hStdOutput:=StdOutPipeWrite;
  hStdError :=StdOutPipeWrite;

输出到管道。不要忘记关闭此端:

 CloseHandle(StdOutPipeWrite);

出于上述专家解释的原因,它效果很好。没有它程序无法退出。

我更喜欢稍微自定义一下控制台,以了解确切的大小:

  Rect: TSmallRect;
  Coord: TCoord;
  Rect.Left:=0; Rect.Top:=0; Rect.Right:=80; Rect.Bottom:=30;
  Coord.X:=Rect.Right+1-Rect.Left; Coord.Y:=Rect.Bottom+1-Rect.Top;
  SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE),Coord);
  SetConsoleWindowInfo(GetStdHandle(STD_OUTPUT_HANDLE),True,Rect);
//  SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_RED OR BACKGROUND_BLUE);// for maniacs

如果不是控制台应用程序而是GUI,控制台可以通过

创建
AllocConsole();
SetConsoleTitle('Console TITLE');
ShowWindow(GetConsoleWindow(),SW_SHOW);// or SW_HIDE - it will blink

还是回到正题:如何读取第三方程序的实时输出?如果幸运的话,该程序会逐行打印到附加的管道,一旦它们准备就绪,您只需像上面那样用

阅读它们
ReadOk := ReadFile(StdOutPipeRead, Buffer, BufferSize, BytesRead, nil);

如果程序不合作,而是等到最后才填满管道,你别无选择,只能按照上面的方法将其保留在控制台输出中。这种方式程序相信有人正在观看它的输出(你真的可以用 SW_SHOW 观看它),并逐行打印。希望不是很快,至少每秒 1 行。因为你不只是享受输出,而是快速从控制台抓取这些行,使用这种非常无聊的技术一条一条地..

您可以先清除控制台,然后再启动程序,如果您已经处理过它,尽管新控制台不需要这样做:

 Hcwnd:=GetStdHandle(STD_OUTPUT_HANDLE);
 Coord.X:=0; Coord.Y:=0;
 CharsWritten:=0;
 ClearChar:=#0;
 GetConsoleScreenBufferInfo(Hcwnd,BufInfo);
 ConScreenBufSize := BufInfo.dwSize.X * BufInfo.dwSize.Y;// size of the console screen buffer
 FillConsoleOutputCharacter(Hcwnd,           // Handle to console screen buffer
                            Char(ClearChar), // Character to write to the buffer
                            ConScreenBufSize,// Number of cells to write
                            Coord,           // Coordinates of first cell
                            CharsWritten);   // Receive number of characters written
 ResumeThread(PI.hThread);// if it was started with CREATE_SUSPENDED

显然这有效:

   BufInfo: _CONSOLE_SCREEN_BUFFER_INFO;
   LineBuf,Line:string;
   SetLength(LineBuf, BufInfo.dwMaximumWindowSize.X);// one horizontal line
   iX:=0; iY:=0;
   repeat
    Coord.X:=0; Coord.Y:=iY;
    ReadOk:=ReadConsoleOutputCharacter(Hcwnd,PChar(LineBuf),BufInfo.dwMaximumWindowSize.X,Coord,CharsRead);
    if ReadOk then begin// ReadOk
       if CharsRead > 0 then Line:=Trim(Copy(LineBuf,1,CharsRead)); else Line:='';

并且您正在进入重复读取同一行的可怕编程,直到它不是空白,并检查下一行,以防程序执行 WriteLn('')。如果那几行是空白的,检查

if WaitForSingleObject(PI.hProcess,10) <> WAIT_TIMEOUT then QuitReading:=true;

以防程序在控制台中间完成。如果输出到达控制台的底部,您将重复阅读该行。如果相同,则检查 WaitForSingleObject。如果不是,则更糟 - 您必须返回几行以找到您的上一行,以确保程序不会太快吐出几行以致您错过了它们。程序喜欢在完成之前这样做。

这个框架里面有很多乱七八糟的代码,尤其是像我这样的烂程序员:

    if iY < (BufInfo.dwMaximumWindowSize.Y-1-1) then begin// not last line
       if (length(Line)>0) then begin// not blank
                                . . .
                                end// not blank
                           else begin// blank
                                . . .
                                end;// blank
                                                     end// not last line
                                                else begin// last line
       if (length(Line)>0) then begin// not blank
                                . . .
                                end// not blank
                           else begin// blank
                                . . .
                                end;// blank
                                                     end;// last line
    Sleep(200);
   until QuitReading;

但它有效!它惊人地向控制台打印实时数据(如果你没有 SW_HIDE 它),同时你的 GUI 程序打印从控制台抓取的相同行并按你想要的方式处理它们。当外部程序完成时,控制台消失,GUI 程序保留完整结果。