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

5
我需要阅读原本基于Linux的Cygwin程序的大量命令行输出。在cmd.exe下运行良好,每几秒钟打印一次新行。
当我使用下面的代码时,在SO上讨论了很多次,ReadFile函数不会返回,直到该程序停止。然后由ReadFile提供并打印所有输出。
如何使ReadFile在可用时立即读取该输出?
MSDN表示,在ENABLE_LINE_INPUT模式下,ReadFile不会返回,直到达到CR或缓冲区已满。该程序使用Linux换行符LF,而不是Windows的CRLF。我使用了小缓冲区32字节,并禁用了ENABLE_LINE_INPUT顺便问一下,禁用它的正确方法是什么?)。
也许ReadFile之所以不返回,是因为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永远不会被读取以停止程序。
如何测试所有这些: 在一个命令窗口中启动iperf3服务器并让它监听:
D:\PAS\iperf3\win32>iperf3.exe -s -i 2 -p 5001
-----------------------------------------------------------
Server listening on 5001
-----------------------------------------------------------

在另一个命令窗口中,您启动客户端,它会立即连接到服务器并开始每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客户端在命令窗口中运行良好。现在让我们在客户端模式下启动“我的”代码,而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客户端在“我的”代码中启动,但它没有打印任何内容!只有在客户端完成后,“我的”代码才会打印此输出。
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程序输出的行为取决于它是在命令窗口还是Delphi控制台应用程序中运行。我的“Line”输出处理代码并不完美,但让我们找出如何使ReadFile实时返回,我会修复其余部分。


这是当你复制粘贴从其他地方找到的代码时会发生的事情之一。 - David Heffernan
什么是Cygwin进程? - quasoft
3
有些程序会检查它们连接的输出类型。当它们连接到控制台时,它们会频繁地刷新其输出(例如每行)。当它们检测到自己连接到其他设备,如管道或磁盘文件时,它们会缓存输出(因为似乎没有人能看到它)。可能该程序提供一种选项来覆盖默认行为。 - Rob Kennedy
我在代码顶部添加了定义。Cygwin程序是iperf3.exe。请不要建议我重新编译它,我可以自己做。我相信问题更普遍,并希望为每个人找到解决方法。David Hefferman:我几乎阅读了SO上关于管道和CreateProcess的所有帖子,包括您的评论,并尝试了4种解决方案,然后才在这里发布。@quasoft:Zarco Gajic的解决方案仅在子程序停止后提供输出,DelphiDabbler的UweRaabe解决方案是我在这里询问的,Kenny的解决方案几乎相同。我将在下面提供有关iperf3.exe的更多信息。 - K-man
@Rob Kennedy:如何欺骗Cygwin程序,使其认为已连接到控制台?我在原问题中添加了更多信息。 - K-man
显示剩余2条评论
2个回答

4
如何让ReadFile一旦有输出就立即读取呢?
问题并不在您提供的代码中。它已经实时读取了输出(尽管代码还存在另一个与此无关的问题,请参见下文)。
您可以尝试使用以下批处理文件而不是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表示,在ENABLE_LINE_INPUT模式下,ReadFile不会返回,直到CR到达或缓冲区已满。 该程序使用Linux换行符LF,而不是Windows CR LF。 我使用了小缓冲区32字节,禁用了ENABLE_LINE_INPUT - 顺便问一下,禁用它的正确方法是什么?

您不需要禁用它。

如果您将缓冲区设置为32字节,则一旦缓冲区已满,ReadFile函数应该返回这32个字节,即使使用UNIX换行符。

也许ReadFile之所以不返回是因为cygwin程序本身存在其他问题,而不仅仅是LF换行符?

这就是我认为的。我不想猜测可能的原因,但它们与行结束符的差异无关。

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

但是它在Windows cmd.exe中运行良好,为什么不在Delphi控制台应用程序中运行?

很奇怪,这是个好问题。在我的电脑上,它在Delphi和cmd中都可以工作。 这就是为什么我认为问题与Cygwin应用程序有关。

另外:为什么我们必须在CreateProcess之后立即关闭此句柄? CloseHandle(StdOutPipeWrite);

这是管道的写入端。我们不需要写入句柄,因为我们不会向管道写入任何内容,我们只从中读取内容。 您的Cygwin应用程序间接地向该管道写入内容。


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

  • You have a Line variable that is of type string and is not initialized. Initialize that to empty string (Line := '') at the beginning of the routine/program.

  • As you have UNIX line ending in Buffer, ReadFile will not return unless the buffer is full, thus containing multiple lines. You need to either change the call to WriteLn routine to Write and ignore line endings, or use a parser that separates the lines.

  • Line variable should either be cleared after being written to stdout or should directly receive the value of Buffer, like that:

    ...
    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);
    ...
    

    Unless you do that the size of Line will increase progressively until it contains the whole output, duplicating.


1
@HarryJohnston 同意,在此问题 https://github.com/esnet/iperf/issues/299 中也得到了确认,存在类似的问题。 - quasoft
1
@K-man 看起来一个好的解决方案是修改 iperf 并强制它每行都刷新。类似于此问题中所做的 https://github.com/esnet/iperf/pull/272 - quasoft
1
@K-man,这是我措辞不当的问题。此外,这里有一个修改版的iperf,增加了每个间隔强制刷新的选项:https://github.com/quasoft/iperf/tree/force-flush。但你需要编译和测试它。 - quasoft
2
Windows没有提供任何方法来创建一个假装是控制台的管道。至于写入句柄,如果您保持句柄的副本打开,那么很难确定子进程何时退出。(当关闭管道写端的最后一个句柄时,任何等待的读取操作都将退出。但是,如果您保持句柄的副本打开,这将不会发生。) - Harry Johnston
1
通常情况下,程序不会询问“我是否有一个控制台”来决定如何进行缓冲,而是会询问“标准输出是否为控制台”。因此,如果你重定向输出,它将无法工作。你可以让它输出到一个真正的控制台并读取内容(查找ReadConsoleOutput函数)。缺点是:(a)这样做相当笨拙,(b)在你看到输出之前,无法确保没有任何输出滚动出窗口...尽管在这种情况下(b)可能不是一个问题,如果你知道输出速度足够慢的话。 - Harry Johnston
显示剩余11条评论

1
这是解决方案的摘要,感谢在此提供建议的专家: 许多Unix衍生程序可以使用Cygwin包在Windows中启动,它们会监视其输出目标。如果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

Apparently this works:

   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程序保留完整的结果。

1
小细节:如果输出到控制台,则为无缓冲,而不是EOL缓冲。Windows从不进行EOL缓冲。(但通常程序仍会在单个调用中写入整行,在这种情况下区别是无关紧要的。) - Harry Johnston

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接