我需要读取最初基于Linux的Cygwin程序的冗长命令行输出。它在cmd.exe
下运行良好,每隔几秒就打印一行。
当我使用下面的代码时,这个代码已在SO上多次讨论过,ReadFile
函数在该程序停止之前不会返回。然后所有输出都由ReadFile
提供并打印。
如何在ReadFile
可用后立即读取该输出?
MSDN表示ReadFile
在CR
模式下达到ENABLE_LINE_INPUT
或缓冲区已满后才会返回。该程序使用Linux换行符LF
,而不是Windows CRLF
。我使用32字节的小缓冲区并禁用ENABLE_LINE_INPUT
(顺便说一下,禁用它的正确方法是什么?)。
由于Cygwin程序本身存在其他一些问题,可能ReadFile
没有返回,而不只是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客户端是在“我的”代码中启动的,但它不会打印任何东西!只有在客户端完成后,“我的”代码才会打印此输出:
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,我会解决其余问题。
答案 0 :(得分:4)
如何在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
变量,其类型为字符串且未初始化。
在例程/程序的开头将其初始化为空字符串(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
的大小会逐渐增加,直到它包含整个输出,
复制。
答案 1 :(得分:1)
这是一个解决方案摘要,感谢在这里建议的专家:
许多unix出生的程序,可以在带有Cygwin软件包的Windows中启动,观察其输出的目的地。如果stdOut是控制台,则输出是EOL缓冲的。这意味着只要新线准备就绪,就会打印出来,无论它是如何分开的:CR或CR + LF。如果stdOut是管道或文件或其他东西,则输出是EOF缓冲的,因为人类没有观看屏幕。这意味着程序完成后会打印所有多行(除非我们使用&#39; flush&#39;,但可能我们没有源代码)。在这种情况下,我们会丢失所有实时信息。
使用此代码(使用最顶层的定义)很容易检查,在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(&#39;&#39;)的情况下检查下一行。如果这几行是空白的,请检查
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程序保存完整的结果。