process.stdout/stderr: clear O_NONBLOCK for spawned children, don't drop console output on EAGAIN
What does this PR do?
Same model as Node/libuv for process.stdout / process.stderr on POSIX: a pipe or socket behind fd 1/2 is O_NONBLOCK (libuv's uv_pipe_open does this too) and written asynchronously; TTYs and files are synchronous. O_NONBLOCK lives on the shared open file description, so it leaked in two places that Node covers and Bun did not:
| before | after | |
|---|---|---|
stdio: "inherit" child runs head -c 1000000 after parent used process.stdout | EAGAIN, short output, exit 1 | full output, exit 0 |
| fd 1/2 flags seen by an inherit child | O_NONBLOCK | blocking |
console.log(1 MiB line) after process.stdout.isTTY, piped | 720896 bytes | 1048577 bytes |
process.stdout.write(1 MiB) to a full pipe | false + 'drain' | false + 'drain' (unchanged) |
| same, after an inherit spawn (socket any OS / pipe Linux) | would block like Node | false + 'drain' |
spawnclearsO_NONBLOCKon the fds that become the child's 0–2, as libuv'suv__process_child_initdoes. Done parent-side beforeposix_spawn(same effect, since the description is shared, and macOS file actions cannotfcntl).- The native console writer waits for
POLLOUTonEAGAINinstead of dropping the rest of the line. Node does not need this becauseconsole.*goes through the stdout stream there.
How did you verify your code works?
test/js/node/process/process-stdio.test.ts gains 9 tests (fd-flag probe in an inherit child, head -c through inherited stdout, 1 MiB console.log/console.error, 1 MiB through a real pipe(2) via sh, and the false + 'drain' contract over both a socketpair and a real pipe). 6 fail on 1.4.3 (the post-spawn ones guard this PR's own spawn change), all pass here; also ran process-stdin, spawn, child-process-stdio, console-log locally on macOS.
no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/process/process-stdio.test.ts