Reading stdout with WinAPI



  • Dear :wtf:, is there anyone with WinAPI knowledge? I have the following problem:

    I have a Windows C++ applicaton. Let's call it Caller. Caller needs to call child.exe and receive all of its standard output, as well as check its exit code. For the most part I have copied the example from MSDN. Here it is in all of its glory (yes, I'm aware I'm leaking handles with the early returns, I'll worry about that later):

    static bool CreateProcessGetStdout(char* cmdBytes, std::ostream& out)
    {
      HANDLE stdoutReadHandle;
      HANDLE stdoutWriteHandle;
      SECURITY_ATTRIBUTES saAttr;
      // Set the bInheritHandle flag so pipe handles are inherited.
      saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
      saAttr.bInheritHandle = TRUE;
      saAttr.lpSecurityDescriptor = nullptr;
      // Create a pipe for the child process's STDOUT.
      if (!CreatePipe(&stdoutReadHandle, &stdoutWriteHandle, &saAttr, 0))
        return false;
      if (!SetHandleInformation(stdoutReadHandle, HANDLE_FLAG_INHERIT, 0))
        return false;
    
      PROCESS_INFORMATION piProcInfo;
      ZeroMemory( &piProcInfo, sizeof(PROCESS_INFORMATION) );
    
      STARTUPINFO siStartInfo;
      ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) );
      siStartInfo.cb = sizeof(STARTUPINFO);
      siStartInfo.hStdError = nullptr;
      siStartInfo.hStdOutput = stdoutWriteHandle;
      siStartInfo.hStdInput = nullptr;
      siStartInfo.dwFlags |= STARTF_USESTDHANDLES;
    
      // Create the child process.
      BOOL success = CreateProcess(nullptr,
        cmdBytes,     // command line
        nullptr,       // process security attributes
        nullptr,       // primary thread security attributes
        TRUE,          // handles are inherited
        0,             // creation flags
        nullptr,       // use parent's environment
        nullptr,       // use parent's current directory
        &siStartInfo,  // STARTUPINFO pointer
        &piProcInfo);  // receives PROCESS_INFORMATION
    
      if (!success)
      {
        return false;
      }
    
      // Close the thread handle. We don't need it.
      CloseHandle(piProcInfo.hThread);
    
      for (;;)
      {
        // Read the data out of the pipe.
        CHAR buffer[4096] = { 0 };
        DWORD dataSize = 0;
        success = ReadFile(stdoutReadHandle, buffer, sizeof(buffer) - 1, &dataSize, nullptr);
        if (!success)
        {
          return false;
        }
        if (dataSize == 0)
        {
          // We're done.
          break;
        }
        out << buffer;
      }
    
      CloseHandle(stdoutReadHandle);
      CloseHandle(stdoutWriteHandle);
    
      // Wait for the process to exit.
      WaitForSingleObject(piProcInfo.hProcess, INFINITE);
    
      // Get the process.
      DWORD exitCode;
      GetExitCodeProcess(piProcInfo.hProcess, &exitCode);
      CloseHandle(piProcInfo.hThread);
    
      return exitCode == 0;
    }
    

    The problem is that once all of the stdout pipe is read, the next ReadFile() call hangs, or rather blocks forever, expecting further input that never comes.

    From all that I could find so far that's apparently because the write handle to the pipe has not been closed. That is true but I don't know when I'd go about closing it. I tried waiting for the process to exit, close the pipe and then read from it, but that just lead to it hanging on the WaitForSingleObject(). I presume this is because child.exe outputs so much data into stdout that the pipe fills up and you have to read from it so that it can continue writing.



  • Hmm. Two things to try.

    • Check if Win32 is internally duplicating the handle when it launches the child process. If yes, you should be able to CloseHandle() the read handle just after the CreateProcess() in the parent process (the child process will still have a reference to the handle). Maybe that will cause ReadFile() to return the appropriate status once the child process exits.

    • As a workaround you could use non-blocking (=overlapped) IO. Apparently that's not possible with anonymous pipes for some reason, so you'd have to switch to named pipes (CreateNamedPipe() with FILE_FLAG_OVERLAPPED). It's not going to be pretty, and it's pretty easy to end up with something that's essentially polling. (Non sure if you can easily wait for either reads becoming available or a process exiting.)


  • Banned

    @deadfast you can use PeekNamedPipe to check if input is available. Yes, it works with anonymous pipes too. Your loop should first check if the process is alive, then read input if available, and then, if the process is dead, wrap up and return. It's important to check process state before input state, but check input before deciding to return.



  • Have you tried putting CloseHandle(stdoutWriteHandle) right where you close the thread handle (don't know if it matters which you do first)?



  • Thanks guys.

    @cvi said in Reading stdout with WinAPI:

    Check if Win32 is internally duplicating the handle when it launches the child process. If yes, you should be able to CloseHandle() the read handle just after the CreateProcess() in the parent process (the child process will still have a reference to the handle). Maybe that will cause ReadFile() to return the appropriate status once the child process exits.

    @anotherusername said in Reading stdout with WinAPI:

    Have you tried putting CloseHandle(stdoutWriteHandle) right where you close the thread handle (don't know if it matters which you do first)?

    Ok, so testing that now it does seem to read all of the input but then the last ReadFile() errors out with ERROR_BROKEN_PIPE. But I guess that may be fine? I mean it makes sense: after being closed on the parent's end the only thing keeping a handle to that pipe is the child processes so once it ends, the pipe will be closed. It just doesn't feel right.

    @gąska said in Reading stdout with WinAPI:

    @deadfast you can use PeekNamedPipe to check if input is available. Yes, it works with anonymous pipes too. Your loop should first check if the process is alive, then read input if available, and then, if the process is dead, wrap up and return. It's important to check process state before input state, but check input before deciding to return.

    Hm, I actually tried that before and it does work:

    for (;;)
    {
      // Check if there is anything in the pipe.
      DWORD dataSize;
      success = PeekNamedPipe(stdoutReadHandle, nullptr, 0, nullptr, &dataSize, nullptr);
      if (!success)
      {
        return false;
      }
    
      if (dataSize == 0)
      {
        // If there is no data in the pipe check if the process has ended.
        GetExitCodeProcess(piProcInfo.hProcess, &exitCode);
    
        if (exitCode == STILL_ACTIVE)
        {
          // There should still be more output so try again.
          continue;
        }
        else
        {
          // We're done.
          break;
        }
      }
    
      // Read the data out of the pipe.
      CHAR buffer[4096] = { 0 };
      success = ReadFile(stdoutReadHandle, buffer, sizeof(buffer) - 1, &dataSize, NULL);
      if (!success)
      {
        return false;
      }
      out << buffer;
    }
    

    It just feels dodgy as well though.


  • Banned

    @deadfast it's not dodgy, just low-level. Non-blocking IO always looks awful.

    Also, you have potential race condition when the child process writes to pipe just before it exits - if it happens after you checked pipe status but before you checked process status, you lose the final portion of data.



  • @gąska said in Reading stdout with WinAPI:

    Also, you have potential race condition when the child process writes to pipe just before it exits - if it happens after you checked pipe status but before you checked process status, you lose the final portion of data.

    Good point. Any ideas on how to address that?


  • Banned

    @deadfast said in Reading stdout with WinAPI:

    Ok, so testing that now it does seem to read all of the input but then the last ReadFile() errors out with ERROR_BROKEN_PIPE. But I guess that may be fine?

    It is. Broken pipe literally means that it was closed on the other end and there is no more data. This is how you know the other process wrote all its output and ended.

    This might be a better solution than mine. It's just that the documentation is fairly bad at explaining what handle inheritance means exactly - apparently it means the child process gets duplicate handle that can be closed separately, which is a good thing because this is exactly what we'd like. Or it might be something that looks like the handle is duplicate and closing it is actually a bad idea.


  • Banned

    @deadfast said in Reading stdout with WinAPI:

    @gąska said in Reading stdout with WinAPI:

    Also, you have potential race condition when the child process writes to pipe just before it exits - if it happens after you checked pipe status but before you checked process status, you lose the final portion of data.

    Good point. Any ideas on how to address that?

    Do exactly as I told you to do in my first post.



  • @deadfast said in Reading stdout with WinAPI:

    But I guess that may be fine?

    I think so. Not sure if you can get anything better.

    It just feels dodgy as well though.

    It is a bit dodgy IMO. Besides the race that @Gąska points out, you essentially end up in a tight polling loop when the pipe is empty and the process is still alive. Even if you fix the race, I would recommend avoiding this solution due to the polling loop.



  • @gąska said in Reading stdout with WinAPI:

    Do exactly as I told you to do in my first post.

    Heh, I thought I did... Attempt #2:

    DWORD exitCode, dataSize;
    do
    {
      // Check if the process is alive.
      GetExitCodeProcess(piProcInfo.hProcess, &exitCode);
    
      // Check if there is anything in the pipe.
      success = PeekNamedPipe(stdoutReadHandle, nullptr, 0, nullptr, &dataSize, nullptr);
      if (!success)
      {
        return false;
      }
    
      if (dataSize > 0)
      {
        // Read the data out of the pipe.
        CHAR buffer[4096] = { 0 };
        success = ReadFile(stdoutReadHandle, buffer, sizeof(buffer) - 1, &dataSize, nullptr);
        if (!success)
        {
          return false;
        }
        out << buffer;
      }
    } while (exitCode == STILL_ACTIVE || dataSize != 0);
    

    @cvi said in Reading stdout with WinAPI:

    It is a bit dodgy IMO. Besides the race that @Gąska points out, you essentially end up in a tight polling loop when the pipe is empty and the process is still alive. Even if you fix the race, I would recommend avoiding this solution due to the polling loop.

    I don't really see the busy wait as being an issue. The child process normally only runs for a few milliseconds, it just spams a lot of data onto stdout at once that I have to deal with. I guess I could add a yield as well.



  • @deadfast said in Reading stdout with WinAPI:

    I don't really see the busy wait as being an issue. The child process normally only runs for a few milliseconds, it just spams a lot of data onto stdout at once that I have to deal with. I guess I could add a yield as well.

    It's your program, so 🤷♂.

    I consider busy waits/polling loops to be really icky code smell (for code that is running on a multitasking system), and would reject such in a code review with extreme prejudice. Even with a yield thrown into the mix. IMO, it's just almost always bad code. (Even here ... for what? To avoid getting a ERROR_BROKEN_PIPE?)


  • Banned

    @deadfast you shouldn't use returned data size as the amount you query - you should always use buffer size. You can always read the rest later. Also, the unnecessary zeroing of buffer irks me a bit. But otherwise looks good. But I agree with @cvi that busyloop isn't the best solution.

    After some more googling, I finally found the piece of documentation that says yes, you should indeed close the pipe handle in parent process after the child inherits it. So, the best way to go is stick with @anotherusername's solution and wait until you get broken pipe.



  • @gąska said in Reading stdout with WinAPI:

    you shouldn't use returned data size as the amount you query - you should always use buffer size.

    AFAIK he does that (sizeof(buffer)-1). The &dataSize parameter passed to ReadFile is lpNumberOfBytesRead, i.e. ReadFile will store the number of bytes actually read in it.

    You can always read the rest later.

    Yep. The current version may lose data if the pipe contains more than sizeof(buffer)-1 elements when the the child process exited.


  • Banned

    @cvi said in Reading stdout with WinAPI:

    @gąska said in Reading stdout with WinAPI:

    you shouldn't use returned data size as the amount you query - you should always use buffer size.

    AFAIK he does that (sizeof(buffer)-1). The &dataSize parameter passed to ReadFile is lpNumberOfBytesRead, i.e. ReadFile will store the number of bytes actually read in it.

    Oh, right. That's what happens when you have bazillion arguments in a function.

    You can always read the rest later.

    Yep. The current version may lose data if the pipe contains more than sizeof(buffer)-1 elements when the the child process exited.

    No, it will loop around one more time and read remaining input.



  • @gąska said in Reading stdout with WinAPI:

    After some more googling, I finally found the piece of documentation that says yes, you should indeed close the pipe handle in parent process after the child inherits it. So, the best way to go is stick with @anotherusername's solution and wait until you get broken pipe.

    Unless I'm missing something that page doesn't talk about broken pipes. Quite the opposite, it says that you should be getting a zero bytes read:

    The parent process uses the ReadFile function to receive input from the pipe. [..] When all write handles to the pipe are closed, the ReadFile function returns zero.

    This is exactly what I was expecting too but it doesn't seem to be happening.



  • @deadfast Actually, reading the ReadFile() docs again, maybe ERROR_BROKEN_PIPE really is legit. Fucking hell, this is why I normally try to steer clear of WinAPI...


  • Banned

    @deadfast "returns" here means the function return value (the BOOL), not the data read. 0 means FALSE. If you go to the ReadFile documentation, you'll see this:

    If an anonymous pipe is being used and the write handle has been closed, when ReadFile attempts to read using the pipe's corresponding read handle, the function returns FALSE and GetLastError returns ERROR_BROKEN_PIPE.

    In other words: create pipe, create process with inherited write handle as its stdin, close your own write handle. Read for as long as ReadFile() returns TRUE. If it returns FALSE, you check GetLastError(). If the error is ERROR_BROKEN_PIPE, it means you've read all the data successfully and the other process exited. If the error is something else, it means something went wrong.

    Yes, the WinAPI docs are a mess. But it's still light years ahead of most other software.



  • @gąska said in Reading stdout with WinAPI:

    Yes, the WinAPI docs are a mess. But it's still light years ahead of most other software.

    Not compared to .NET which is what I normally try to use when doing stuff like this. Unfortunately this is a legacy application with code so terrible that even a busy wait is an improvement. Honestly, I consider the utter lack of any Unicode support a bigger issue.

    I've ended up switching to checking the return value and:

    1. If error is ERROR_BROKEN_PIPE, we're done.
    2. If error is ERROR_MORE_DATA, keep reading.
    3. If error is anything else, error out.

    I also removed that zero-out. It was just a quick and lazy way of ensuring the string was null terminated for appending to the stream which I don't use in the actual code.

    Thanks everyone!



  • @gąska said in Reading stdout with WinAPI:

    No, it will loop around one more time and read remaining input.

    Sorry, you're right. I missed the || dataSize != 0 in the while condition. :-/



  • @cvi said in Reading stdout with WinAPI:

    @gąska said in Reading stdout with WinAPI:

    No, it will loop around one more time and read remaining input.

    Sorry, you're right. I missed the || dataSize != 0 in the while condition. :-/

    Full disclosure: I edited that in a bit later when I realized that myself. You may have seen the original version.


  • Trolleybus Mechanic

    If you're unsure of the proper way to do it, you could look at the .net process launching code.

    There seems to be an interesting comment on line 1982 about std io handles.


  • Banned

    @deadfast said in Reading stdout with WinAPI:

    @gąska said in Reading stdout with WinAPI:

    Yes, the WinAPI docs are a mess. But it's still light years ahead of most other software.

    Not compared to .NET which is what I normally try to use when doing stuff like this.

    Some more advanced parts of .NET have no documentation whatsoever. WPF API reference has missing documentation in many places, and for a large number of items that are documented, the documentation explains nothing. The overall quality of .NET docs is very similar to WinAPI docs - which is no surprise considering they both come from Microsoft.

    @deadfast said in Reading stdout with WinAPI:

    If error is ERROR_BROKEN_PIPE, we're done.

    Yes. Just don't forget to WAIT for exit status. There's a possibility the pipe closes before the process actually exits.

    If error is ERROR_MORE_DATA, keep reading.

    The docs say it can't happen with anonymous pipes. It might turn out to be dead code.

    If error is anything else, error out.

    That's right.


  • Banned

    @mikehurley said in Reading stdout with WinAPI:

    There seems to be an interesting comment on line 1982 about std io handles.

    Very interesting indeed. The article it refers to, as well as the comment itself, both say that the child can close the handle and bad things will happen for the parent - suggesting that inheriting handles doesn't clone them after all; contradicting the article about pipes I linked upthread. Gosh, this is all so confusing.

    I think I'll fire off an email to Raymond Chen to answer it once and for all.



  • @gąska said in Reading stdout with WinAPI:

    The article it refers to, as well as the comment itself, both say that the child can close the handle and bad things will happen for the parent

    Yeah, I don't understand that part 100% either. So what if the child also closes the parent-end handles? (Then again, not passing them to the child is probably a good idea anyway.)

    But the key point is the following comment in the sample code from the article:

         // Close pipe handles (do not continue to modify the parent).
         // You need to make sure that no handles to the write end of the
         // output pipe are maintained in this process or else the pipe will
         // not close when the child process exits and the ReadFile will hang.
         if (!CloseHandle(hOutputWrite)) DisplayError("CloseHandle");
         if (!CloseHandle(hInputRead )) DisplayError("CloseHandle");
         if (!CloseHandle(hErrorWrite)) DisplayError("CloseHandle");
    

    That was exactly what @Deadfast was experiencing.



  • @deadfast said in Reading stdout with WinAPI:

    @deadfast Actually, reading the ReadFile() docs again, maybe ERROR_BROKEN_PIPE really is legit. Fucking hell, this is why I normally try to steer clear of WinAPI...

    (Just FYI, this is really easy in .NET.)

    (EDIT: oh you knew that but you have to work in a crappy legacy language because it's a crappy legacy program. Condolences.)


  • Banned

    @blakeyrat said in Reading stdout with WinAPI:

    @deadfast said in Reading stdout with WinAPI:

    @deadfast Actually, reading the ReadFile() docs again, maybe ERROR_BROKEN_PIPE really is legit. Fucking hell, this is why I normally try to steer clear of WinAPI...

    (Just FYI, this is really easy in .NET.)

    High-level libraries provide high-level solutions to high-level problems. News at 11.



  • @blakeyrat said in Reading stdout with WinAPI:

    it's a crappy legacy program

    You have no idea. I've barely scratched the surface. My favourite :wtf: so far is a homegrown lousy knock-off of std::ostringstream that doesn't append the null terminator when you append a string to it...

    Thankfully I don't have to look at it much further, I'm just helping out a coworker because, for some reason, it's most broken on my PC.


  • Trolleybus Mechanic

    @deadfast said in Reading stdout with WinAPI:

    @blakeyrat said in Reading stdout with WinAPI:

    it's a crappy legacy program

    You have no idea. I've barely scratched the surface. My favourite :wtf: so far is a homegrown lousy knock-off of std::ostringstream that doesn't append the null terminator when you append a string to it...

    Thankfully I don't have to look at it much further, I'm just helping out a coworker because, for some reason, it's most broken on my PC.

    Could you make this module use C++/CLI? That way for this bit you could use the .NET Process class. Or you could create a new dll if you want to segregate the .NET usage more explicitly.



  • @deadfast I don't know if it is really allowed to have invalid handles in STARTUPINFO if STARTF_USESTDHANDLES is set, but if it is, you should probably set them to INVALID_HANDLE_VALUE instead of nullptr.


Log in to reply