[英]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.