Reputation: 333
I'm working with pipes to get the cmd.exe output inside my program. Sometimes, I noted that if the cmd.exe ask for user input (I create hidden cmd window), the program hangs, because nobody will put the input in the window, and the cmd will just stay. So I implemented WaitForSingleObject to avoid hang on the cases where cmd asks for user input or just hang for another reason. The problem comes when I try to execute powershell commands, because it looks unresponsive for WaitForSingleObject, and I always reach the timeout. The function is:
function GetDosOutput(const Exe, Param: string): string;
const
InheritHandleSecurityAttributes: TSecurityAttributes =
(nLength: SizeOf(TSecurityAttributes); bInheritHandle: True);
var
hReadStdout, hWriteStdout: THandle;
si: TStartupInfo;
pi: TProcessInformation;
WaitTimeout, BytesRead: DWord;
lReadFile: boolean;
Buffer: array[0..255] of AnsiChar;
begin
Result:= '';
if CreatePipe(hReadStdout, hWriteStdout, @InheritHandleSecurityAttributes, 0) then
begin
try
si:= Default(TStartupInfo);
si.cb:= SizeOf(TStartupInfo);
si.dwFlags:= STARTF_USESTDHANDLES;
si.hStdOutput:= hWriteStdout;
si.hStdError:= hWriteStdout;
if CreateProcess(Nil, PChar(Exe + ' ' + Param), Nil, Nil, True, CREATE_NO_WINDOW,
Nil, PChar(ExtractFilePath(ParamStr(0))), si, pi) then
begin
CloseHandle(hWriteStdout);
while True do
begin
try
WaitTimeout:= WaitForSingleObject(pi.hProcess, 20000);
if WaitTimeout = WAIT_TIMEOUT then
begin
Result:= 'No result available';
break;
end
else
begin
repeat
lReadFile:= ReadFile(hReadStdout, Buffer, SizeOf(Buffer) - 1, BytesRead, nil);
if BytesRead > 0 then
begin
Buffer[BytesRead]:= #0;
OemToAnsi(Buffer, Buffer);
Result:= Result + String(Buffer);
end;
until not (lReadFile) or (BytesRead = 0);
end;
if WaitTimeout = WAIT_OBJECT_0 then
break;
finally
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
end;
end;
end;
finally
CloseHandle(hReadStdout);
end;
end;
end;
If I call this function passing:
cmd.exe /C dir c:\
It goes alright. But if I call using:
powershell dir c:\ or cmd.exe /C powershell dir c:\
The WaitForSingleObject reaches the timeout, and nothing happens. Any help on this one?
Upvotes: 1
Views: 2521
Reputation: 163257
The pipe's buffer is probably full. The child process is blocked, waiting for your process to read from the pipe and make room for more output. However, your program is also blocked, waiting for the child process to complete. Thus, deadlock.
You need to keep reading from the pipe, but the problem is that if you call ReadFile
and the process hangs for some other reason than a full pipe buffer, then your program hangs, too. ReadFile
doesn't offer a timeout parameter.
ReadFile
doesn't have a timeout parameter because asynchronous reads are done instead using overlapped I/O. You pass to ReadFile
a TOverlapped
record that includes a Windows event handle. ReadFile
will return immediately, and it will signal the event when the read has finished. Use WaitForMultipleObjects
to wait on not only the process handle but also this new event handle.
There's a snag, though. CreatePipe
creates anonymous pipes, and anonymous pipes don't support overlapped I/O. Therefore, you'll have to use CreateNamedPipe
instead. Generate a unique name for the pipe at run time so it won't interfere with any other programs (including additional instances of your program).
Here's a sketch of how the code could go:
var
Overlap: TOverlapped;
WaitHandles: array[0..1] of THandle;
begin
hReadStdout := CreateNamedPipe('\\.\pipe\unique-pipe-name-here',
Pipe_Access_Inbound, File_Flag_First_Pipe_Instance or File_Flag_Overlapped,
Pipe_Type_Byte or Pipe_Readmode_Byte, 1, x, y, 0, nil);
Win32Check(hReadStdout <> Invalid_Handle_Value);
try
hWriteStdout := CreateFile('\\.\pipe\unique-pipe-name-here', Generic_Write,
@InheritHandleSecurityAttributes, ...);
Win32Check(hWriteStdout <> Invalid_Handle_Value);
try
si.hStdOutput := hWriteStdout;
si.hStdError := hWriteStdout;
Win32Check(CreateProcess(...));
finally
CloseHandle(hWriteStdout);
end;
try
Overlap := Default(TOverlapped);
Overlap.hEvent := CreateEvent(nil, True, False, nil);
Win32Check(Overlap.hEvent <> 0);
try
WaitHandles[0] := Overlap.hEvent;
WaitHandles[1] := pi.hProcess;
repeat
ReadResult := ReadFile(hReadStdout, ..., @Overlap);
if ReadResult then begin
// We read some data without waiting. Process it and go around again.
SetString(NewResult, Buffer, BytesRead div SizeOf(Char));
Result := Result + NewResult;
continue;
end;
Win32Check(GetLastError = Error_IO_Pending);
// We're reading asynchronously.
WaitResult := WaitForMultipleObjects(Length(WaitHandles),
@WaitHandles[0], False, 20000);
case WaitResult of
Wait_Object_0: begin
// Something happened with the pipe.
ReadResult := GetOverlappedResult(hReadStdout, @Overlap, @BytesRead, True);
// May need to check for EOF or broken pipe here.
Win32Check(ReadResult);
SetString(NewResult, Buffer, BytesRead div SizeOf(Char));
Result := Result + NewBuffer;
ResetEvent(Overlap.hEvent);
end;
Wait_Object_0 + 1: begin
// The process terminated. Cancel the I/O request and move on,
// returning any data already in Result. (There's no further data
// in the pipe, because if there were, WaitForMultipleObjects would
// have returned Wait_Object_0 instead. The first signaled handle
// determines the return value.
CancelIO(hReadStdout);
break;
end;
Wait_Timeout: begin
// Timeout elapsed without receiving any more data.
Result := 'no result available';
break;
end;
Wait_Failed: Win32Check(False);
else Assert(False);
end;
until False;
finally
CloseHandle(Overlap.hEvent);
end;
finally
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
end;
finally
CloseHandle(hReadStdout);
end;
end;
Note that in the above code, any new output from the program will essentially reset the 20-second timeout you allotted for the process to finish. That might be acceptable behavior, but if not, then you'll have to keep track of how much time has already elapsed and adjust the timeout value prior to calling WaitForMultipleObjects
(and perhaps prior to calling ReadFile
, too, in case the OS opts to handle ReadFile
non-overlapped, which it might do if there's already data available when you call it).
Upvotes: 2