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