我需要读取最初的基于Linux的Cygwin程序的冗长的命令行输出。 它在cmd.exe
下工作很好,每隔几秒钟打印一行。
当我使用下面这个在SO上多次讨论过的代码时, ReadFile
函数在程序停止前不会返回。 然后所有输出由ReadFile
提供并打印。
如何使ReadFile
读取的输出尽快可用?
MSDN说ReadFile
不会返回,直到ENABLE_LINE_INPUT
模式或缓冲区满CR
。 该程序使用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 -fm -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之后closures这个句柄? 它用于读取程序输出:
CloseHandle(StdOutPipeWrite);
如果在程序结束时closures它,程序输出是OK,但ReadLn永远不会被读取来停止程序。
如何testing所有这一切:在一个命令窗口中,启动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 -fm -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控制台应用程序中运行。 是的,我的输出处理代码与“线”并不完美,但让我们找出如何使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说ReadFile不会返回,直到ENABLE_LINE_INPUT模式或缓冲区满CR。 该程序使用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
并忽略行尾,或者使用分隔WriteLn
来分隔行。
Line
变量在写入stdout
之后应该被清除,或者应该直接接收缓冲区的值,如下所示:
... 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来看它),并且一行一行地打印。 希望不是很快,至少每秒一行。 因为你不只是在享受输出,而是快速地从控制台上抓住这些线,一个接一个地使用这个相当无聊的技术。
如果您已经开始编写程序,您可以在启动程序之前先清除控制台,尽管新控制台不需要:
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程序保持全部结果。