简体   繁体   English

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

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

I need to read lengthy command-line output of originally Linux-based Cygwin program. 我需要阅读最初基于Linux的Cygwin程序的冗长命令行输出。 It works great under cmd.exe , printing new line every few seconds. 它在cmd.exe下运行良好,每几秒打印一行。

When I use this code below, which was discussed many times here on SO, ReadFile function does not return until that program stops. 当我使用下面的代码时,在SO上多次讨论过, ReadFile函数在该程序停止之前不会返回。 Then all output is provided by ReadFile and printed. 然后所有输出都由ReadFile提供并打印。

How to make that output read by ReadFile as soon as it is available? 如何在ReadFile可用时立即读取该输出?

MSDN says that ReadFile doesn't return until CR is reached in ENABLE_LINE_INPUT mode, or buffer full. MSDN表示,在ENABLE_LINE_INPUT模式下达到CR或缓冲区已满时, ReadFile不会返回。 That progam uses Linux line breaks LF , not Windows CRLF . 该程序使用Linux换行符LF ,而不是Windows CRLF I used small buffer 32 bytes and disabled ENABLE_LINE_INPUT ( By the way what's the right way of disabling it? ). 我使用32字节的小缓冲区并禁用了ENABLE_LINE_INPUT顺便说一下,什么是禁用它的正确方法? )。

Maybe ReadFile doesn't return because of some other issue with Cygwin program itself, not just LF line breaks? 也许ReadFile不会因为Cygwin程序本身的其他问题而返回,而不仅仅是LF换行? But it works fine in Windows cmd.exe , why not in Delphi console application? 但它在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;

Also: why do we have to close this handle right after CreateProcess? 另外:为什么我们必须在CreateProcess之后立即关闭此句柄? It is used to read program output: 它用于读取程序输出:

CloseHandle(StdOutPipeWrite);

If I close it at the end of program, program output is Ok, but ReadLn is never read to stop the program. 如果我在程序结束时关闭它,程序输出就是Ok,但是从不读取ReadLn来停止程序。

HOW TO test all this: In one command window you start iperf3 server and let it listen: 如何测试所有这些:在一个命令窗口中启动iperf3服务器并让它监听:

D:\PAS\iperf3\win32>iperf3.exe -s -i 2 -p 5001
-----------------------------------------------------------
Server listening on 5001
-----------------------------------------------------------

In another command window you start the client, which immediately connects to server and start printing output every 2 sec: 在另一个命令窗口中,启动客户端,该客户端立即连接到服务器并每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.

Server prints output as well, together with client: 服务器也与客户端一起打印输出:

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

So iperf3 client works great in the command window. 因此,iperf3客户端在命令窗口中运行良好。 Now let's start "my" code in client mode, while iperf3 server is still listening. 现在让我们在客户端模式下启动“我的”代码,而iperf3服务器仍在监听。 Server accepts the connection and start printing output 服务器接受连接并开始打印输出

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

it means iperf3 client is started inside of 'my' code, but it is not printing anything! 这意味着iperf3客户端是在“我的”代码中启动的,但它不会打印任何东西! Only after client finished, 'my' code prints this output: 只有在客户端完成后,'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.

So, cygwin program output behaves differently, depending if it runs inside command window or Delphi console application. 因此,cygwin程序输出的行为会有所不同,具体取决于它是在命令窗口还是在Delphi控制台应用程序中运行。 And yes, my output handling code with 'Line' is not perfect, but let's find out how to make ReadFile return in real-time, I'll fix the rest. 是的,我的输出处理代码与'Line'并不完美,但让我们找出如何使ReadFile实时返回,我将解决其余问题。

How to make that output read by ReadFile as soon as it is available? 如何在ReadFile可用时立即读取该输出?

The problem is not in the code you provided. 问题不在您提供的代码中。 It is already reading output in realtime (Although there is another problem with the code that is not related, see below) . 它已经实时读取输出(尽管代码中存在另一个与之无关的问题,请参见下文)

You can try it with the following batch file instead of Cygwin executable: 您可以使用以下批处理文件而不是Cygwin可执行文件来尝试:

test.bat: test.bat的:

timeout 5
echo "1"
timeout 5
echo "2"
timeout 5
echo "3"

and the following bash shell file: 和以下bash shell文件:

test.sh: test.sh:

sleep 5
echo "1"
sleep 5
echo "2"
sleep 5
echo "3"

It works in realtime and outputs text to console as soon as it is available. 它可以实时工作,并在文本可用时立即将文本输出到控制台。

So if the problem is not in the Delphi code, it is related to the Cygwin program. 因此,如果问题不在Delphi代码中,则它与Cygwin程序有关。 We need more information about your Cygwin program to help you further. 我们需要有关您的Cygwin计划的更多信息,以帮助您进一步。

MSDN says that ReadFile doesn't return until CR is reached in ENABLE_LINE_INPUT mode, or buffer full. MSDN表示,在ENABLE_LINE_INPUT模式下达到CR或缓冲区已满时,ReadFile不会返回。 That progam uses linux line breaks LF, not Windows CR LF. 该程序使用linux换行符LF,而不是Windows CR LF。 I used small buffer 32 bytes, disabled ENABLE_LINE_INPUT - btw what's the right way of disabling it? 我用了32个字节的小缓冲区,禁用了ENABLE_LINE_INPUT - 顺便说一下禁用它的正确方法是什么?

You don't need to disable it. 您无需禁用它。

If you've set the buffer to 32 bytes, then as soon as the buffer is full, ReadFile function should return those 32 bytes, even with UNIX line endings. 如果您将缓冲区设置为32个字节,那么只要缓冲区已满, ReadFile函数就应该返回这32个字节,即使使用UNIX行结尾也是如此。

Maybe ReadFile doesn't return because of some other issue with cygwin program itself, not just LF line breaks? 也许ReadFile不会因为cygwin程序本身的其他问题而返回,而不仅仅是LF换行?

This is what I suppose. 这就是我想的。 I don't want to guess the possible reasons, but they are not related to difference of line endings. 我不想猜测可能的原因,但它们与行结尾的差异无关。

Yes, non-Windows line endings can make the command wait for a whole buffer to be filled, but cannot cause the ReadFile to block. 是的,非Windows行结尾可以使命令等待填充整个缓冲区,但不能导致ReadFile阻塞。

But it works fine in Windows cmd.exe, why not in Delphi console application? 但它在Windows cmd.exe中工作正常,为什么不在Delphi控制台应用程序中呢?

Good question, this is strange. 好问题,这很奇怪。 On my side it works both in Delphi and cmd. 就我而言,它在Delphi和cmd中都有效。 That is why I suppose the problem is related to Cygwin application. 这就是为什么我认为这个问题与Cygwin应用程序有关。

Also: why do we have to close this handle right after CreateProcess? 另外:为什么我们必须在CreateProcess之后立即关闭此句柄? CloseHandle(StdOutPipeWrite); CloseHandle的(StdOutPipeWrite);

This is the writing end of the pipe. 这是管道的书写结束。 We don't need the write handle, because we are not writing to the pipe, we are only reading from it. 我们不需要写句柄,因为我们不是写入管道,我们只是从它读取。 Your Cygwin application is indirectly writing to that pipe. 您的Cygwin应用程序间接写入该管道。


Also, there are two problems in the code that have to be noted: 此外,代码中还有两个问题需要注意:

  • You have a Line variable that is of type string and is not initialized. 您有一个Line变量,其类型为string,并且未初始化。 Initialize that to empty string ( Line := '' ) at the beginning of the routine/program. 在例程/程序的开头将其初始化为空字符串( Line := '' )。

  • As you have UNIX line ending in Buffer , ReadFile will not return unless the buffer is full, thus containing multiple lines. 由于UNIX行以Buffer结尾,因此除非缓冲区已满,否则ReadFile不会返回,因此包含多行。 You need to either change the call to WriteLn routine to Write and ignore line endings, or use a parser that separates the lines. 您需要WriteLn例程的调用更改为Write并忽略行结尾,或者使用分隔行的解析器。

  • Line variable should either be cleared after being written to stdout or should directly receive the value of Buffer, like that: 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); ... 

    Unless you do that the size of Line will increase progressively until it contains the whole output, duplicating. 除非你这样做,否则Line的大小将逐渐增加,直到它包含整个输出,重复。

this is a Summary of solution, thanks to experts who advised here: 这是一个解决方案摘要,感谢在此建议的专家:

Many unix-born programs, which can be launched in Windows with Cygwin package, watch the destination of their output. 许多unix出生的程序,可以在带有Cygwin软件包的Windows中启动,观察其输出的目的地。 If stdOut is to a console, output is EOL-buffered. 如果stdOut是控制台,则输出是EOL缓冲的。 It means that as soon as new line is ready, it is printed, no matter how it is separated: CR or CR+LF. 这意味着只要新线准备就绪,就会打印出来,无论它是如何分开的:CR或CR + LF。 If stdOut is to a pipe or file or something else, output is EOF-buffered, because human is not watching the screen. 如果stdOut是管道或文件或其他东西,则输出是EOF缓冲的,因为人类没有观看屏幕。 It means that all multiple lines are printed when program finished (unless we use 'flush', but presumably we don't have the source code). 这意味着程序完成后会打印所有多行(除非我们使用'flush',但可能我们没有源代码)。 In this case we loose all real-time information. 在这种情况下,我们会丢失所有实时信息。

It's easy to check with this code (with definitions from the very top), put it in right after CreateProcess: 使用此代码(使用最顶层的定义)很容易检查,在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;

If you set your console I/O like this: 如果您将控制台I / O设置为:

  hStdInput := GetStdHandle(STD_INPUT_HANDLE);
  hStdOutput := GetStdHandle(STD_OUTPUT_HANDLE);
  hStdError := GetStdHandle(STD_OUTPUT_HANDLE);

output will be to the console. 输出将到控制台。 If you set it like this: 如果你这样设置:

  hStdInput :=GetStdHandle(STD_INPUT_HANDLE);
  hStdOutput:=StdOutPipeWrite;
  hStdError :=StdOutPipeWrite;

output will be to the pipe. 输出将是管道。 Don't forget to close this end: 别忘了关闭这个目的:

 CloseHandle(StdOutPipeWrite);

for reasons, explained by experts above, it works great. 由于上述专家解释的原因,它的效果很好。 Without it the program can't exit. 没有它,程序无法退出。

I prefer to customize the console a bit, to know exact size: 我更喜欢自定义控制台,以了解确切的大小:

  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

If it is not a Console application but GUI, the console can be created by 如果它不是控制台应用程序而是GUI,则可以通过创建控制台

AllocConsole();
SetConsoleTitle('Console TITLE');
ShowWindow(GetConsoleWindow(),SW_SHOW);// or SW_HIDE - it will blink

Still, back to the main problem: how to read the real-time output of the third-party program? 不过,回到主要问题:如何读取第三方程序的实时输出? If you are lucky, and that program prints out to the attached pipe line by line, as soon as they are ready, you just read them as above with 如果你很幸运,并且该程序逐行打印到连接的管道,一旦它们准备就绪,你只需按照上面的方式阅读它们

ReadOk := ReadFile(StdOutPipeRead, Buffer, BufferSize, BytesRead, nil);

If program doesn't cooperate, but waits to the very end to fill up the pipe, you have no choice, but to leave it with the console output, as per above. 如果程序不合作,但等到最后填满管道,你别无选择,只能将它与控制台输出一起保留,如上所述。 This way program believes that somebody is watching its output (and you really may watch it with SW_SHOW), and prints line by line. 这种方式程序认为有人正在观察它的输出(你真的可以用SW_SHOW观看它),并逐行打印。 Hopefully not very fast, at least 1 line per second. 希望不是很快,每秒至少1行。 Because you are not just enjoying output, but quickly grabbing those lines from the console, one by one using this quite boring technique.. 因为你不仅仅是享受输出,而是从控制台迅速抓住这些线条,一个接一个地使用这种相当无聊的技术。

You may clear console first, before starting program, if you already worked on it, although it's not necessary for the new console: 您可以在启动程序之前先清除控制台,如果您已经使用它,尽管新控制台不需要:

 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:='';

and you are getting into horrible programming of repeated reading of the same line, until it is not blank, checking the next line(s) as well in case that program does WriteLn(''). 并且你正在进行重复读取相同行的可怕编程,直到它不是空白,在程序执行WriteLn('')的情况下检查下一行。 If those few lines are blank, check 如果这几行是空白的,请检查

if WaitForSingleObject(PI.hProcess,10) <> WAIT_TIMEOUT then QuitReading:=true;

in case program finished in the middle of console. 如果程序在控制台中间完成。 If output got to the bottom of the console, you repeatedly read that line. 如果输出到达控制台的底部,则重复读取该行。 If it is the same, check WaitForSingleObject. 如果是相同的,请检查WaitForSingleObject。 If it is not, even worse - you have to go back few lines up to find your previous line, to ensure that program didn't spit few lines too quickly so you missed them. 如果不是,更糟糕的是 - 你必须回到几行找到你的前一行,以确保程序没有太快吐出几行,所以你错过了它们。 Programs love to do that before finish. 程序喜欢在完成之前这样做。

There is a lot of messy code inside this skeleton, especially for such a lousy programmer like me: 这个骨架里面有很多乱码,特别是像我这样糟糕的程序员:

    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;

But it works! 但它的确有效! It is amazingly printing to the console the real-time data (if you didn't SW_HIDE it), and at the same time your GUI program prints the same lines grabbed from the console and processes them the way you want. 令人惊讶地向控制台打印实时数据(如果你没有SW_HIDE它),同时你的GUI程序打印从控制台抓取的相同行并按照你想要的方式处理它们。 When external program finishes, console disappears, and GUI program holds full results. 外部程序完成后,控制台消失,GUI程序保存完整的结果。

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

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