Skip to content

fix(ext/node): enable test-stdio-closed tests#32237

Merged
fraidev merged 11 commits intodenoland:mainfrom
fraidev:fix/node-compat-stdio-closed
Feb 20, 2026
Merged

fix(ext/node): enable test-stdio-closed tests#32237
fraidev merged 11 commits intodenoland:mainfrom
fraidev:fix/node-compat-stdio-closed

Conversation

@fraidev
Copy link
Copy Markdown
Contributor

@fraidev fraidev commented Feb 19, 2026

  • Fix child_process shell command translation to handle shell redirections (1>&-, < file), piped commands (cmd1 | cmd2), and shell variable prefixes ("$NODE", ${NODE}). Previously shell operators were passed to the CLI arg translator, causing parse failures or incorrect output.
  • Remove duplicate transformDenoShellCommand call from normalizeSpawnArguments — it was already called in buildCommand, and the double transformation corrupted POSIX '\'' quoting.
  • Add wrap_eval parameter to op_node_translate_cli_args so shell command contexts skip eval code wrapping (which introduces metacharacters unsafe for shell embedding).
  • Map BrokenPipe to EPIPE (was incorrectly mapped to EBADF) in stream_wrap, and catch BrokenPipe in process.stdout/ process.stderr writeSync to emit it as an async error event.
  • Add minimal _handle with ref()/unref()/close() to PIPE stdin. Intentionally omits readStart/readStop/reading — adding those resets _readableState.reading, which triggers duplicate _read() calls with orphaned reffed promises that prevent process exit.
  • Ref io.stdin in _read() and unref on pause when no readStop exists, matching Node.js event loop ref-counting behavior.
  • Add platform-specific quoting in transformDenoShellCommand: POSIX uses single quotes with '\'' escaping, Windows uses double quotes with \\ doubling before ".
  • Add Windows cmd.exe detection (/d /s /c pattern) in buildCommand for shell command transformation.
  • Deduplicate --unstable-* flags in fork() between CLI translator and bootstrap args.

Enables 3 new node_compat tests:

  • test-stdio-closed.js
  • test-stdout-close-catch.js
  • test-stdout-close-unref.js

Needs:

@fraidev fraidev force-pushed the fix/node-compat-stdio-closed branch from c2ae4a0 to 9d3b7c8 Compare February 20, 2026 00:04
This reverts commit 215dae1.
When a non-deno process (e.g. Python) is spawned with Deno's execPath in
its args, set DENO_NODE_COMPAT=1 in the child's environment. This env var
is then read by cli/args/flags.rs to enable allow_all, bare_node_builtins,
and detect_cjs for bare-run scripts, allowing the child deno process to use
require() and other Node.js compat features even when invoked directly
without the usual node compat flags.

This fixes test-stdio-closed.js on Windows, where the test spawns Python
which closes fd 0/1/2 and then calls deno directly (bypassing child_process
arg translation), causing require() to be undefined and exit code 1.
@fraidev fraidev force-pushed the fix/node-compat-stdio-closed branch from 284ea4b to 032511e Compare February 20, 2026 13:24
Comment on lines +43 to +51
writer.writeSync(
ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, buf)
? buf
: Buffer.from(buf, enc),
);
} catch (e) {
// BrokenPipe errors should be emitted as async 'error' events,
// not thrown synchronously, matching Node.js behavior.
if (
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Geee, this is so brittle

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is a workaround to replicate the BrokenPipe error, but if we had FD access in net modules like new Socket({ fd: 1 }) we would not need it.

Without the workaround, that exception escapes the stream machinery entirely and becomes an uncaught error, the 'error' event on process.stdout never fires.

I'll add TODO to fix it when/if we have raw fd access. Or should we skip/ignore this test?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a Todo is fine

Comment on lines +262 to +281
// Provide a minimal _handle so code that checks process.stdin._handle
// (e.g. test-stdout-close-unref.js) works. We intentionally omit
// readStart/readStop/reading so the onpause handler takes the simple
// io.stdin UNREF path - adding those methods causes _readableState.reading
// to be reset, which triggers duplicate _read() calls and orphaned
// reffed promises that prevent process exit.
stdin._handle = {
close(cb) {
io.stdin?.close();
if (typeof cb === "function") cb();
},
ref() {
io.stdin?.[io.REF]();
},
unref() {
io.stdin?.[io.UNREF]();
},
getAsyncId() {
return -1;
},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could provide a proper implementation once we have equivalent of libuv stdio handle?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that would help here. Should I also add a todo?

@fraidev fraidev merged commit a29b955 into denoland:main Feb 20, 2026
216 of 218 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants