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

时间:2016-03-05 08:18:55

标签: linux windows delphi cygwin console-application

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

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

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

MSDN表示ReadFileCR模式下达到ENABLE_LINE_INPUT或缓冲区已满后才会返回。该程序使用Linux换行符LF,而不是Windows CRLF。我使用32字节的小缓冲区并禁用ENABLE_LINE_INPUT顺便说一下,禁用它的正确方法是什么?)。

由于Cygwin程序本身存在其他一些问题,可能ReadFile没有返回,而不只是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);

如果我在程序结束时关闭它,程序输出就是Ok,但是从不读取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,我会解决其余问题。

2 个答案:

答案 0 :(得分: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模式下达到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之后立即关闭此句柄?       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的大小会逐渐增加,直到它包含整个输出, 复制。

答案 1 :(得分:1)

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

许多unix出生的程序,可以在带有Cygwin软件包的Windows中启动,观察其输出的目的地。如果stdOut是控制台,则输出是EOL缓冲的。这意味着只要新线准备就绪,就会打印出来,无论它是如何分开的:CR或CR + LF。如果stdOut是管道或文件或其他东西,则输出是EOF缓冲的,因为人类没有观看屏幕。这意味着程序完成后会打印所有多行(除非我们使用&#39; flush&#39;,但可能我们没有源代码)。在这种情况下,我们会丢失所有实时信息。

使用此代码(使用最顶层的定义)很容易检查,在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(&#39;&#39;)的情况下检查下一行。如果这几行是空白的,请检查

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