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

K-m*_*man 5 linux windows delphi cygwin console-application

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

当我使用下面的这段代码时(这里已经讨论过很多次了),ReadFile直到该程序停止后,函数才会返回。然后所有输出由 提供ReadFile并打印。

如何使输出一ReadFile可用就被读取?

MSDN 说,直到达到模式或缓冲区已满时ReadFile才会返回。该程序使用 Linux 换行符,而不是 Windows 。我使用了 32 字节的小缓冲区并禁用了(顺便问一下,禁用它的正确方法是什么?)。CRENABLE_LINE_INPUTLFCRLFENABLE_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;
Run Code Online (Sandbox Code Playgroud)

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

CloseHandle(StdOutPipeWrite);
Run Code Online (Sandbox Code Playgroud)

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

如何测试这一切:在一个命令窗口中启动 iperf3 服务器并让它监听:

D:\PAS\iperf3\win32>iperf3.exe -s -i 2 -p 5001
-----------------------------------------------------------
Server listening on 5001
-----------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)

在另一个命令窗口中,您启动客户端,它立即连接到服务器并开始每 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.
Run Code Online (Sandbox Code Playgroud)

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

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
-----------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)

所以 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
-----------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)

这意味着 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.
Run Code Online (Sandbox Code Playgroud)

因此,cygwin 程序输出的行为有所不同,具体取决于它是在命令窗口还是 Delphi 控制台应用程序内运行。是的,我的“Line”输出处理代码并不完美,但让我们了解如何使 ReadFile 实时返回,我将修复其余部分。

qua*_*oft 4

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

问题不在您提供的代码中。它已经在实时读取输出(尽管代码还有另一个不相关的问题,请参见下文)

您可以使用以下批处理文件而不是 Cygwin 可执行文件来尝试:

测试.bat:

timeout 5
echo "1"
timeout 5
echo "2"
timeout 5
echo "3"
Run Code Online (Sandbox Code Playgroud)

以及以下 bash shell 文件:

测试.sh:

sleep 5
echo "1"
sleep 5
echo "2"
sleep 5
echo "3"
Run Code Online (Sandbox Code Playgroud)

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

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

MSDN 表示,在 ENABLE_LINE_INPUT 模式下达到 CR 或缓冲区已满之前,ReadFile 不会返回。该程序使用 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 之后立即关闭这个句柄?关闭句柄(StdOutPipeWrite);

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


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

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

  • 由于您的 UNIX 行以 结尾BufferReadFile因此除非缓冲区已满(因此包含多行),否则不会返回。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);
    ...
    
    Run Code Online (Sandbox Code Playgroud)

    除非你这样做,否则 的大小Line将逐渐增加,直到它包含整个输出,并进行复制。

  • Windows 不提供任何方法来制作冒充控制台的管道。至于写入句柄,如果将句柄副本保持打开状态,则很难判断子进程何时退出。(当管道写入端的最后一个句柄关闭时,任何正在等待的读取操作都将退出。但如果您保持句柄的副本打开,则不会发生这种情况。) (2认同)