【问题标题】:How to read command-line output of cygwin program in real-time in Delphi?如何在Delphi中实时读取cygwin程序的命令行输出?
【发布时间】:2016-06-19 02:51:45
【问题描述】:

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

当我使用下面的这段代码时,ReadFile 函数在该程序停止之前不会返回。然后所有输出由ReadFile 提供并打印出来。

如何使ReadFile 在可用时立即读取该输出?

MSDN 说 ReadFile 直到在 ENABLE_LINE_INPUT 模式下达到 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 实时返回,剩下的我会解决。

【问题讨论】:

  • 这是粘贴在别处找到的代码时发生的事情之一
  • 什么是Cygwin进程?
  • 有时,程序会检查它们所连接的输出类型。当它们连接到控制台时,它们会频繁刷新输出(就像每一行一样)。当它们检测到它们连接到其他东西时,例如管道或磁盘文件,它们会缓冲它们的输出(因为似乎没有人可以看到它)。也许程序提供了覆盖默认行为的选项。
  • 我在代码之上添加了定义。 Cygwin 程序是 iperf3.exe。请不要建议我重新编译它,我可以这样做。我相信这个问题比较笼统,并希望找到它为每个人都受益。 David Hefferman:我几乎阅读了 SO 上关于管道和 CreateProcess 的所有帖子,包括您的 cmets,并尝试了 4 种解决方案,然后才在这里发帖。 @quasoft:Zarco Gajic 解决方案仅在子程序停止后才提供输出,我在这里问的是来自 DelphiDabbler 的 UweRaabe 解决方案,Kenny 解决方案几乎相同。我将在下面提供有关 iperf3.exe 的更多信息。
  • @Rob Kennedy:如何欺骗 cygwin 程序,让它相信它已连接到控制台?我在原始问题中添加了更多信息。

标签: linux windows delphi cygwin console-application


【解决方案1】:

如何在 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 说 ReadFile 不会返回,直到在 ENABLE_LINE_INPUT 模式下达到 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 应用程序正在间接写入该管道。


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

  • 您有一个 Line 变量,它是字符串类型且未初始化。 在例程/程序的开头将其初始化为空字符串(Line := '')。

  • 由于 UNIX 行以 Buffer 结尾,ReadFile 不会返回,除非缓冲区已满,因此包含多行。 您需要将对 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 的大小会逐渐增加,直到它包含整个输出, 复制。

【讨论】:

  • 是的,问题出在 Cygwin 程序中的 C 运行时缓冲区,它不在 OPs 控制范围内。据我所知,对于孩子是 Windows 程序的情况,没有人提出过好的解决方案,但 Cygwin 可能会有所不同。顺便说一句,行尾无关紧要; ENABLE_LINE_INPUT 仅适用于控制台,不适用于管道。
  • @HarryJohnston 同意,在本期github.com/esnet/iperf/issues/299 中也确认了类似问题。
  • @K-man 似乎一个很好的解决方案是修改iperf 并强制它刷新每一行。与本期所做的类似github.com/esnet/iperf/pull/272
  • @K-man 认为我的措辞不好。此外,这里是 iperf 的修改版本,添加了在每个时间间隔强制刷新的选项:github.com/quasoft/iperf/tree/force-flush。但是你需要编译和测试它。
  • Windows 不提供任何方法来制作伪装成控制台的管道。至于写句柄,如果您保持句柄副本打开,则更难判断子进程何时退出。 (当管道写入端的最后一个句柄关闭时,任何正在等待的读取操作都将退出。但如果您保持句柄副本处于打开状态,则不会发生这种情况。)
【解决方案2】:

这是解决方案的摘要,感谢在这里提供建议的专家:

许多可以在 Windows 中使用 Cygwin 包启动的 unix 程序会监视其输出的目的地。如果 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

如果不是Console应用而是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 程序保存完整结果。

【讨论】:

  • 次要 nitpick:如果输出到控制台,则它是无缓冲的,而不是 EOL 缓冲的。 Windows 从不进行 EOL 缓冲。 (但程序在一次调用中写一整行是相当普遍的,在这种情况下,区别是无关紧要的。)
猜你喜欢
  • 2015-08-02
  • 2012-11-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-01
  • 2012-10-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多