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
andhStdError
fields ofSTARTUPINFO
). - Tell the process to inherit any inheritable handles from its parent so that it will inherit said stdout handle (arg
bInheritHandles
ofCreateProcess()
). - 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 usingCreateFile
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).