user2864778
user2864778

Reputation: 333

CreateProcess, PowerShell and WaitForSingleObject

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

Answers (1)

Rob Kennedy
Rob Kennedy

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

Related Questions