繁体   English   中英

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

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

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

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

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

MSDN表示,在ENABLE_LINE_INPUT模式下达到CR或缓冲区已满时, ReadFile不会返回。 该程序使用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);

如果我在程序结束时关闭它,程序输出就是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客户端是在“我的”代码中启动的,但它不会打印任何东西! 只有在客户端完成后,'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程序输出的行为会有所不同,具体取决于它是在命令窗口还是在Delphi控制台应用程序中运行。 是的,我的输出处理代码与'Line'并不完美,但让我们找出如何使ReadFile实时返回,我将解决其余问题。

如何在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变量,其类型为string,并且未初始化。 在例程/程序的开头将其初始化为空字符串( 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的大小将逐渐增加,直到它包含整个输出,重复。

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

许多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

显然这有用:

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM