diff --git a/cli/tests/unit_node/child_process_test.ts b/cli/tests/unit_node/child_process_test.ts index b40cfd9ff46435..e89fd7d7928fb3 100644 --- a/cli/tests/unit_node/child_process_test.ts +++ b/cli/tests/unit_node/child_process_test.ts @@ -577,3 +577,25 @@ Deno.test( assertStringIncludes(output, "typescript"); }, ); + +Deno.test( + "[node/child_process spawn] supports stdio array option", + async () => { + const cmdFinished = deferred(); + let output = ""; + const script = path.join( + path.dirname(path.fromFileUrl(import.meta.url)), + "testdata", + "child_process_stdio.js", + ); + const cp = spawn(Deno.execPath(), ["run", "-A", script]); + cp.stdout?.on("data", (data) => { + output += data; + }); + cp.on("close", () => cmdFinished.resolve()); + await cmdFinished; + + assertStringIncludes(output, "foo"); + assertStringIncludes(output, "close"); + }, +); diff --git a/cli/tests/unit_node/testdata/child_process_stdio.js b/cli/tests/unit_node/testdata/child_process_stdio.js new file mode 100644 index 00000000000000..399b890ed11f7f --- /dev/null +++ b/cli/tests/unit_node/testdata/child_process_stdio.js @@ -0,0 +1,15 @@ +import childProcess from "node:child_process"; +import process from "node:process"; +import * as path from "node:path"; + +const script = path.join( + path.dirname(path.fromFileUrl(import.meta.url)), + "node_modules", + "foo", + "index.js", +); + +const child = childProcess.spawn(process.execPath, [script], { + stdio: [process.stdin, process.stdout, process.stderr], +}); +child.on("close", () => console.log("close")); diff --git a/ext/node/polyfills/internal/child_process.ts b/ext/node/polyfills/internal/child_process.ts index edc4caa5e5ea66..365af4add69b91 100644 --- a/ext/node/polyfills/internal/child_process.ts +++ b/ext/node/polyfills/internal/child_process.ts @@ -177,9 +177,9 @@ export class ChildProcess extends EventEmitter { args: cmdArgs, cwd, env: stringEnv, - stdin: toDenoStdio(stdin as NodeStdio | number), - stdout: toDenoStdio(stdout as NodeStdio | number), - stderr: toDenoStdio(stderr as NodeStdio | number), + stdin: toDenoStdio(stdin), + stdout: toDenoStdio(stdout), + stderr: toDenoStdio(stderr), windowsRawArguments: windowsVerbatimArguments, }).spawn(); this.pid = this.#process.pid; @@ -189,6 +189,16 @@ export class ChildProcess extends EventEmitter { this.stdin = Writable.fromWeb(this.#process.stdin); } + if (stdin instanceof Stream) { + this.stdin = stdin; + } + if (stdout instanceof Stream) { + this.stdout = stdout; + } + if (stderr instanceof Stream) { + this.stderr = stderr; + } + if (stdout === "pipe") { assert(this.#process.stdout); this.stdout = Readable.fromWeb(this.#process.stdout); @@ -285,15 +295,22 @@ export class ChildProcess extends EventEmitter { async #_waitForChildStreamsToClose() { const promises = [] as Array>; - if (this.stdin && !this.stdin.destroyed) { + // Don't close parent process stdin if that's passed through + if (this.stdin && !this.stdin.destroyed && this.stdin !== process.stdin) { assert(this.stdin); this.stdin.destroy(); promises.push(waitForStreamToClose(this.stdin)); } - if (this.stdout && !this.stdout.destroyed) { + // Only readable streams need to be closed + if ( + this.stdout && !this.stdout.destroyed && this.stdout instanceof Readable + ) { promises.push(waitForReadableToClose(this.stdout)); } - if (this.stderr && !this.stderr.destroyed) { + // Only readable streams need to be closed + if ( + this.stderr && !this.stderr.destroyed && this.stderr instanceof Readable + ) { promises.push(waitForReadableToClose(this.stderr)); } await Promise.all(promises); @@ -317,9 +334,13 @@ const supportedNodeStdioTypes: NodeStdio[] = ["pipe", "ignore", "inherit"]; function toDenoStdio( pipe: NodeStdio | number | Stream | null | undefined, ): DenoStdio { + if (pipe instanceof Stream) { + return "inherit"; + } + if ( !supportedNodeStdioTypes.includes(pipe as NodeStdio) || - typeof pipe === "number" || pipe instanceof Stream + typeof pipe === "number" ) { notImplemented(`toDenoStdio pipe=${typeof pipe} (${pipe})`); } @@ -441,8 +462,17 @@ function normalizeStdioOption( "pipe", "pipe", ], -) { +): [ + Stream | NodeStdio | number, + Stream | NodeStdio | number, + Stream | NodeStdio | number, + ...Array, +] { if (Array.isArray(stdio)) { + // At least 3 stdio must be created to match node + while (stdio.length < 3) { + ArrayPrototypePush(stdio, undefined); + } return stdio; } else { switch (stdio) { @@ -796,8 +826,8 @@ export function spawnSync( args, cwd, env, - stdout: toDenoStdio(normalizedStdio[1] as NodeStdio | number), - stderr: toDenoStdio(normalizedStdio[2] as NodeStdio | number), + stdout: toDenoStdio(normalizedStdio[1]), + stderr: toDenoStdio(normalizedStdio[2]), uid, gid, windowsRawArguments: windowsVerbatimArguments,