I'm trying to implement interoperability between a host process written in C++ and a child process which is a Node.js instance on Windows. I do not have control on the child process's IPC API, I have to conform to what Node.js does.
Node.js when used as a server, creates a Windows named pipe object and passes this to the child process as file descriptor 3:
I could confirm by spawning many processes that this fd (also passed in the environment as NODE_CHANNEL_FD) are always equal to 3 when in the child process.
Given the following Node.js program:
const { spawn } = require('child_process');
const child = spawn('child.exe', [], {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: env
});
child.on('message', (message) => {
let m = JSON.stringify(message);
console.log(`message from child: ${m}`)
});
child.send({foo: 123, bar: "456"});
Then from a C++ child.exe:
int main(int argc, char **argv)
{
HANDLE h = _get_osfhandle(3);
...
WriteFile(h, etc...);
...
ReadFile(h, etc...);
}
gives the expected results, the apps can communicate.
Now, I am trying to do the reverse: from a C++ host, create and pass the right data structures to make sure that the right thing ends up as fd 3 in the child process's fd table. Sadly I am unable to replicate this:
On the parent process, I create my pipe like this:
static constexpr auto PIPENAME = "\\\\?\\pipe\\echo.sock";
OVERLAPPED overlapped = {0};
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof sa;
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
HANDLE serverPipeHandle = CreateNamedPipeA(PIPENAME,
PIPE_ACCESS_DUPLEX
| FILE_FLAG_OVERLAPPED
| WRITE_DAC
| FILE_FLAG_FIRST_PIPE_INSTANCE,
PIPE_TYPE_BYTE
| PIPE_READMODE_BYTE
| PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
65536,
65536,
0,
&sa);
if (serverPipeHandle == INVALID_HANDLE_VALUE) {
printf("CreateNamedPipe failed, GLE=%lu.\n", GetLastError());
return 1;
}
HANDLE client_pipe;
client_pipe = CreateFileA(PIPENAME,
GENERIC_READ | GENERIC_WRITE | WRITE_DAC,
0,
&sa,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL);
if (!ConnectNamedPipe(serverPipeHandle, NULL)) {
if (GetLastError() != ERROR_PIPE_CONNECTED) {
printf("ConnectNamedPipe failed, GLE=%lu.\n", GetLastError());
}
}
I can print the unix-like file handles:
auto h = _open_osfhandle((intptr_t) serverPipeHandle, 0);
// Prints: Handle=276 (or sometimes 260 or close numbers)
fprintf(stderr, "Handle=%llu \n", (std::uintptr_t) serverPipeHandle);
// Always prints: fd=3
fprintf(stderr, "fd=%d \n", h);
Then I create my process:
STARTUPINFOA si = {0};
ZeroMemory(&si, sizeof(STARTUPINFOA));
si.cb = sizeof(si);
PROCESS_INFORMATION pi = {0};
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
CreateProcess(NULL, app, NULL, NULL,
TRUE, // handle inheritance
CREATE_SUSPENDED, NULL, NULL,
&si, &pi);
ResumeThread(pi.hThread);
But now, in the child process, fd 3 is not available. What am I missing to make it available?
I tried:
SetHandleInformation(serverPipeHandle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
SetHandleInformation(client_pipe, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
HANDLE hFileDup;
DuplicateHandle(GetCurrentProcess(),
serverPipeHandle,
pi.hProcess,
&hFileDup,
0,
TRUE,
DUPLICATE_SAME_ACCESS);
Calling the CreateProcessWithExplicitHandles
described in https://devblogs.microsoft.com/oldnewthing/20111216-00/?p=8873 with an array of handles containing both the handle of CreateNamedPipeA and of CreateFileA.
Trying to open every fd in the child process to see if the fd would go somewhere else:
for (int i = -1000; i < 1000; i++) {
auto hFile = (HANDLE) _get_osfhandle(i);
if (hFile != INVALID_HANDLE_VALUE) {
fprintf(stderr, "Trying %d ...", i);
fprintf(stderr, "success! \n");
}
}
This prints:
Trying 0 ...success!
Trying 1 ...success!
Trying 2 ...success!
So only stdin, stdout, stderr but not any other of my handle. (interestingly, these numbers do not seem to match the values of STD_OUTPUT_HANDLE, STD_OUTPUT_HANDLE etc. : https://learn.microsoft.com/en-us/windows/console/getstdhandle).
Thus I am at loss, what are my options to make sure that my serverPipeHandle correctly ends up as fd=3 in my child process?
I managed to do it through the undocumented API that MSVCRT uses to load the file descriptors:
#define FOPEN 0x01
#define FPIPE 0x08
#define FDEV 0x40
STARTUPINFOA si = {0};
...
struct {
int count = 4;
unsigned char flags[4];
HANDLE handles[4];
} fd_buf{.count = 4,
.flags = {FOPEN | FDEV, FOPEN | FDEV, FOPEN | FDEV, FOPEN | FPIPE},
.handles = {si.hStdInput, si.hStdOutput, si.hStdError, serverPipeHandle}};
si.lpReserved2 = (unsigned char *) &fd_buf;
si.cbReserved2 = sizeof(fd_buf);