0

I want to spawn a child process and capture its stdout (and stderr) using overlapped I/O without using threads. Here's my current knowledge of all the stars that must align in order to achieve that, i.e. here's the recipe:

  • Set an inheritable handle as the stdout (and stderr) of the process when creating the process (set hStdOutput and hStdError fields of STARTUPINFO).
  • Tell the process to inherit any inheritable handles from its parent so that it will inherit said stdout handle (arg bInheritHandles of CreateProcess()).
  • The handle itself must be the writing end of an anonymous pipe. I will then capture the process' stdout by reading from the reading end of that pipe.
  • The pipe must be overlapped-I/O-enabled.
  • Since anonymous pipes don't support overlapped I/O, I must emulate them using a named pipe (which I create with FILE_FLAG_OVERLAPPED). This pipe will serve as the writing end of the anonymous pipe. I then open this pipe using CreateFile to get a handle to the reading end (this is more/less how anonymous pipes are implemented in Windows also).
  • The reading end of the pipe must not be inherited by the child process, so I am careful to not make it inheritable. (does anyone have a good explanation for why that is?)
  • After the process is created and the writing handle is thus inherited, I close said handle in the parent process. This is so that (thanks @o11c in the comments) the writing end of the pipe is left with only one handle open to it (the handle that the child holds), so that when the child exists, the pipe is closed and reading from it fails with a broken pipe error (otherwise we would never know when to stop reading).
  • Now that everything is set up I can start reading from the pipe: I create a completion port, perform an overlapped ReadFile() and then check the completion status.

And here's the problem: GetQueuedCompletionStatus() hangs until timeout and then returns WAIT_TIMEOUT instead of returning immediately with either some data or with ERROR_IO_PENDING so I can check again.

Below is the minimum amount of C code that reproduces the problem. Any help appreciated. Thanks!

#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <windows.h>

PROCESS_INFORMATION pi;
STARTUPINFO si;
SECURITY_ATTRIBUTES sa;
OVERLAPPED o;

#define sz 1024
unsigned char buf[sz];

char* pipe_name = "\\\\.\\pipe\\t1";

int main(int argc, char **argv) {

    sa.nLength = sizeof(sa);
    sa.bInheritHandle = 1;

    HANDLE stdout_r = CreateNamedPipe(pipe_name,
        PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
        0,
        1,
        8192, 8192,
        120 * 1000,
        0
    );
    assert(stdout_r != INVALID_HANDLE_VALUE);

    HANDLE stdout_w = CreateFile(pipe_name,
        GENERIC_WRITE,
        0,
        &sa,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
        0
    );
    assert(stdout_w != INVALID_HANDLE_VALUE);

    si.hStdOutput = stdout_w;
    si.hStdError  = stdout_w;
    si.dwFlags = STARTF_USESTDHANDLES;
    assert(CreateProcess(0, "dir", 0, 0, 1, 0, 0, 0, &si, &pi) != 0);

    assert(CloseHandle(stdout_w) != 0);

    HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
    assert(iocp != INVALID_HANDLE_VALUE);

    if (ReadFile(stdout_r, buf, sz, 0, &o) == 0) {
        if (GetLastError() == ERROR_IO_PENDING) {
            DWORD n;
            ULONG_PTR compkey;
            LPOVERLAPPED po;
            int ret = GetQueuedCompletionStatus(iocp, &n, &compkey, &po, 1000);
            if (!ret) {
                assert(GetLastError() != WAIT_TIMEOUT);
            }
        }
    }

    return 0;
}

NOTE: I built/tested this with mingw but VC should work too (in ANSI mode).

capr
  • 149
  • 1
  • 8
  • 1
    Re your 2 "(does anyone have a good explanation for why that is?)": it's so that the read side correctly detects when the writer disappears, and thew write side correctly det6ects when the reader disappears. If both sides of the pipe are open in the same process, they will always expect more is possible, and keep waiting forever. – o11c Sep 19 '21 at 01:02
  • @o11c thanks for the explanation, I think I got it: so having two writers open (one in the parent and one in the child process) makes it so that the reader never gets a `broken_pipe` result signaling that the child ended, since the writer in the parent keeps the pipe open. – capr Sep 19 '21 at 01:43
  • 2
    I'm not sure why you expect this to work, since you didn't associate the `stdout_r` handle to the IO completion port in `CreateIoCompletionPort`. – ssbssa Sep 19 '21 at 12:49
  • @ssbssa that's exactly what I was forgetting, you're golden, thanks a lot! post an answer if you want, added missing line `assert(CreateIoCompletionPort(stdout_r, iocp, 0, 0) == iocp);` and now it reads perfectly. – capr Sep 19 '21 at 14:40
  • You not need create iocp by self and handle it. Better use BindIoCompletionCallback. And/or use apc completion, if you want handle all in single thread. Also no difference between named and noname (anonymous) pipes. Exist only single type of pipes and it support asynchronous I/O – RbMm Sep 20 '21 at 08:34

0 Answers0