/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
tests/unit_node/child_process_test.ts
1 708 строк
47 KB
Nathan Whitaker
fix(ext/node): handle pre-quoted shell arguments (#36371)
31 июл 2026, 02:58
Не верифицирован
31 июл 2026, 02:58
5558c9d
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. import CP from "node:child_process"; import net from "node:net"; import { Buffer } from "node:buffer"; import { assert, assertEquals, assertNotStrictEquals, assertStrictEquals, assertStringIncludes, assertThrows, } from "@std/assert"; import * as path from "@std/path"; import { clearTimeout, setTimeout } from "node:timers"; const { spawn, spawnSync, execFile, execFileSync, ChildProcess } = CP; function withTimeout<T>( timeoutInMS = 10_000, ): ReturnType<typeof Promise.withResolvers<T>> { const deferred = Promise.withResolvers<T>(); const timer = setTimeout(() => { deferred.reject("Timeout"); }, timeoutInMS); deferred.promise.then(() => { clearTimeout(timer); }); return deferred; } // TODO(uki00a): Once Node.js's `parallel/test-child-process-spawn-error.js` works, this test case should be removed. Deno.test("[node/child_process spawn] The 'error' event is emitted when no binary is found", async () => { const deferred = withTimeout<void>(); const childProcess = spawn("no-such-cmd"); childProcess.on("error", (_err: Error) => { // TODO(@bartlomieju) Assert an error message. deferred.resolve(); }); await deferred.promise; }); Deno.test("[node/child_process spawn] The 'exit' event is emitted with an exit code after the child process ends", async () => { const deferred = withTimeout<void>(); const childProcess = spawn(Deno.execPath(), ["--help"], { env: { NO_COLOR: "true" }, }); try { let exitCode = null; childProcess.on("exit", (code: number) => { deferred.resolve(); exitCode = code; }); await deferred.promise; assertStrictEquals(exitCode, 0); assertStrictEquals(childProcess.exitCode, exitCode); } finally { childProcess.kill(); childProcess.stdout?.destroy(); childProcess.stderr?.destroy(); } }); Deno.test("[node/child_process disconnect] the method exists", async () => { const deferred = withTimeout<void>(); const childProcess = spawn(Deno.execPath(), ["--help"], { env: { NO_COLOR: "true" }, stdio: ["pipe", "pipe", "pipe", "ipc"], }); try { childProcess.disconnect(); childProcess.on("exit", () => { deferred.resolve(); }); await deferred.promise; } finally { childProcess.kill(); childProcess.stdout?.destroy(); childProcess.stderr?.destroy(); } }); Deno.test({ name: "[node/child_process spawn] Verify that stdin and stdout work", fn: async () => { const deferred = withTimeout<void>(); const childProcess = spawn(Deno.execPath(), ["fmt", "-"], { env: { NO_COLOR: "true" }, stdio: ["pipe", "pipe"], }); try { assert(childProcess.stdin, "stdin should be defined"); assert(childProcess.stdout, "stdout should be defined"); let data = ""; childProcess.stdout.on("data", (chunk) => { data += chunk; }); childProcess.stdin.write(" console.log('hello')", "utf-8"); childProcess.stdin.end(); childProcess.on("close", () => { deferred.resolve(); }); await deferred.promise; assertStrictEquals(data, `console.log("hello");\n`); } finally { childProcess.kill(); } }, }); Deno.test({ name: "[node/child_process spawn] stdin and stdout with binary data", fn: async () => { const deferred = withTimeout<void>(); const p = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "./testdata/binary_stdio.js", ); const childProcess = spawn(Deno.execPath(), ["run", p], { env: { NO_COLOR: "true" }, stdio: ["pipe", "pipe"], }); try { assert(childProcess.stdin, "stdin should be defined"); assert(childProcess.stdout, "stdout should be defined"); let data: Buffer; childProcess.stdout.on("data", (chunk) => { data = chunk; }); const buffer = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); childProcess.stdin.write(buffer); childProcess.stdin.end(); childProcess.on("close", () => { deferred.resolve(); }); await deferred.promise; assertEquals(new Uint8Array(data!), buffer); } finally { childProcess.kill(); } }, }); async function spawnAndGetEnvValue( inputValue: string | number | boolean, ): Promise<string> { const deferred = withTimeout<string>(); const env = spawn( `"${Deno.execPath()}" eval -p "Deno.env.toObject().BAZ"`, { env: { BAZ: String(inputValue), NO_COLOR: "true" }, shell: true, }, ); try { let envOutput = ""; assert(env.stdout); env.on("error", (err: Error) => deferred.reject(err)); env.stdout.on("data", (data) => { envOutput += data; }); env.on("close", () => { deferred.resolve(envOutput.trim()); }); return await deferred.promise; } finally { env.kill(); } } Deno.test({ ignore: Deno.build.os === "windows", name: "[node/child_process spawn] Verify that environment values can be numbers", async fn() { const envOutputValue = await spawnAndGetEnvValue(42); assertStrictEquals(envOutputValue, "42"); }, }); Deno.test({ ignore: Deno.build.os === "windows", name: "[node/child_process spawn] Verify that environment values can be booleans", async fn() { const envOutputValue = await spawnAndGetEnvValue(false); assertStrictEquals(envOutputValue, "false"); }, }); /* Start of ported part */ // Copyright Joyent and Node contributors. All rights reserved. MIT license. // Ported from Node 15.5.1 // TODO(uki00a): Remove this case once Node's `parallel/test-child-process-spawn-event.js` works. Deno.test("[child_process spawn] 'spawn' event", async () => { const timeout = withTimeout<void>(); const subprocess = spawn(Deno.execPath(), ["eval", "console.log('ok')"]); let didSpawn = false; subprocess.on("spawn", function () { didSpawn = true; }); function mustNotBeCalled() { timeout.reject(new Error("function should not have been called")); } const promises = [] as Promise<void>[]; function mustBeCalledAfterSpawn() { const deferred = Promise.withResolvers<void>(); promises.push(deferred.promise); return () => { if (didSpawn) { deferred.resolve(); } else { deferred.reject( new Error("function should be called after the 'spawn' event"), ); } }; } subprocess.on("error", mustNotBeCalled); subprocess.stdout!.on("data", mustBeCalledAfterSpawn()); subprocess.stdout!.on("end", mustBeCalledAfterSpawn()); subprocess.stdout!.on("close", mustBeCalledAfterSpawn()); subprocess.stderr!.on("data", mustNotBeCalled); subprocess.stderr!.on("end", mustBeCalledAfterSpawn()); subprocess.stderr!.on("close", mustBeCalledAfterSpawn()); subprocess.on("exit", mustBeCalledAfterSpawn()); subprocess.on("close", mustBeCalledAfterSpawn()); try { await Promise.race([Promise.all(promises), timeout.promise]); timeout.resolve(); } finally { subprocess.kill(); } }); // TODO(uki00a): Remove this case once Node's `parallel/test-child-process-spawn-shell.js` works. Deno.test("[child_process spawn] Verify that a shell is executed", async () => { const deferred = withTimeout<void>(); const doesNotExist = spawn("does-not-exist", { shell: true }); try { assertNotStrictEquals(doesNotExist.spawnfile, "does-not-exist"); doesNotExist.on("error", () => { deferred.reject("The 'error' event must not be emitted."); }); doesNotExist.on("exit", (code: number, signal: null) => { assertStrictEquals(signal, null); if (Deno.build.os === "windows") { assertStrictEquals(code, 1); // Exit code of cmd.exe } else { assertStrictEquals(code, 127); // Exit code of /bin/sh }); } deferred.resolve(); }); await deferred.promise; } finally { doesNotExist.kill(); doesNotExist.stdout?.destroy(); doesNotExist.stderr?.destroy(); } }); // TODO(uki00a): Remove this case once Node's `parallel/test-child-process-spawn-shell.js` works. Deno.test({ ignore: Deno.build.os === "windows", name: "[node/child_process spawn] Verify that passing arguments works", async fn() { const deferred = withTimeout<void>(); const echo = spawn("echo", ["foo"], { shell: true, }); let echoOutput = ""; try { assertStrictEquals( echo.spawnargs[echo.spawnargs.length - 1].replace(/"/g, ""), "echo foo", ); assert(echo.stdout); echo.stdout.on("data", (data) => { echoOutput += data; }); echo.on("close", () => { assertStrictEquals(echoOutput.trim(), "foo"); deferred.resolve(); }); await deferred.promise; } finally { echo.kill(); } }, }); // TODO(uki00a): Remove this case once Node's `parallel/test-child-process-spawn-shell.js` works. Deno.test({ ignore: Deno.build.os === "windows", name: "[node/child_process spawn] Verity that shell features can be used", async fn() { const deferred = withTimeout<void>(); const cmd = "echo bar | cat"; const command = spawn(cmd, { shell: true, }); try { let commandOutput = ""; assert(command.stdout); command.stdout.on("data", (data) => { commandOutput += data; }); command.on("close", () => { assertStrictEquals(commandOutput.trim(), "bar"); deferred.resolve(); }); await deferred.promise; } finally { command.kill(); } }, }); // TODO(uki00a): Remove this case once Node's `parallel/test-child-process-spawn-shell.js` works. Deno.test({ ignore: Deno.build.os === "windows", name: "[node/child_process spawn] Verity that environment is properly inherited", async fn() { const deferred = withTimeout<void>(); const env = spawn( `"${Deno.execPath()}" eval -p "Deno.env.toObject().BAZ"`, { env: { BAZ: "buzz", NO_COLOR: "true" }, shell: true, }, ); try { let envOutput = ""; assert(env.stdout); env.on("error", (err: Error) => deferred.reject(err)); env.stdout.on("data", (data) => { envOutput += data; }); env.on("close", () => { assertStrictEquals(envOutput.trim(), "buzz"); deferred.resolve(); }); await deferred.promise; } finally { env.kill(); } }, }); /* End of ported part */ Deno.test({ name: "[node/child_process execFile] Get stdout as a string", async fn() { let child: unknown; const script = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "./testdata/exec_file_text_output.js", ); const promise = new Promise<string | null>((resolve, reject) => { child = execFile(Deno.execPath(), ["run", script], (err, stdout) => { if (err) reject(err); else if (stdout) resolve(stdout as string); else resolve(null); }); }); try { const stdout = await promise; assertEquals(stdout, "Hello World!\n"); } finally { if (child instanceof ChildProcess) { child.kill(); } } }, }); Deno.test({ name: "[node/child_process execFile] Get stdout as a buffer", async fn() { let child: unknown; const script = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "./testdata/exec_file_text_output.js", ); const promise = new Promise<Buffer | null>((resolve, reject) => { child = execFile( Deno.execPath(), ["run", script], { encoding: "buffer" }, (err, stdout) => { if (err) reject(err); else if (stdout) resolve(stdout as Buffer); else resolve(null); }, ); }); try { const stdout = await promise; assert(Buffer.isBuffer(stdout)); assertEquals(stdout.toString("utf8"), "Hello World!\n"); } finally { if (child instanceof ChildProcess) { child.kill(); } } }, }); Deno.test({ name: "[node/child_process execFile] Get stderr", async fn() { let child: unknown; const script = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "./testdata/exec_file_text_error.js", ); const promise = new Promise< { err: Error | null; stderr?: string | Buffer } >((resolve) => { child = execFile(Deno.execPath(), ["run", script], (err, _, stderr) => { resolve({ err, stderr }); }); }); try { const { err, stderr } = await promise; if (child instanceof ChildProcess) { assertEquals(child.exitCode, 1); assertEquals(stderr, "yikes!\n"); } else { throw err; } } finally { if (child instanceof ChildProcess) { child.kill(); } } }, }); Deno.test({ name: "[node/child_process execFile] Exceed given maxBuffer limit", async fn() { let child: unknown; const script = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "./testdata/exec_file_text_error.js", ); const promise = new Promise< { err: Error | null; stderr?: string | Buffer } >((resolve) => { child = execFile(Deno.execPath(), ["run", script], { encoding: "buffer", maxBuffer: 3, }, (err, _, stderr) => { resolve({ err, stderr }); }); }); try { const { err, stderr } = await promise; if (child instanceof ChildProcess) { assert(err); assertEquals( // deno-lint-ignore no-explicit-any (err as any).code, "ERR_CHILD_PROCESS_STDIO_MAXBUFFER", ); assertEquals(err.message, "stderr maxBuffer length exceeded"); assertEquals((stderr as Buffer).toString("utf8"), "yik"); } else { throw err; } } finally { if (child instanceof ChildProcess) { child.kill(); } } }, }); Deno.test({ ignore: true, name: "[node/child_process] ChildProcess.unref()", async fn() { const script = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "testdata", "child_process_unref.js", ); const childProcess = spawn(Deno.execPath(), [ "run", "-A", script, ]); const deferred = Promise.withResolvers<void>(); childProcess.on("exit", () => deferred.resolve()); await deferred.promise; }, }); Deno.test({ ignore: true, name: "[node/child_process] child_process.fork", async fn() { const testdataDir = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "testdata", ); const script = path.join( testdataDir, "node_modules", "foo", "index.js", ); const p = Promise.withResolvers<void>(); const cp = CP.fork(script, [], { cwd: testdataDir, stdio: "pipe" }); let output = ""; cp.on("close", () => p.resolve()); cp.stdout?.on("data", (data) => { output += data; }); await p.promise; assertEquals(output, "foo\ntrue\ntrue\ntrue\n"); }, }); Deno.test({ name: "[node/child_process] child_process.fork with URL", async fn() { const testdataDir = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "testdata", ); const script = path.join( testdataDir, "node_modules", "foo", "index.js", ); const scriptUrl = path.toFileUrl(script); const p = Promise.withResolvers<void>(); const cp = CP.fork(scriptUrl, [], { cwd: testdataDir, stdio: "pipe" }); let output = ""; cp.on("close", () => p.resolve()); cp.stdout?.on("data", (data) => { output += data; }); await p.promise; assertEquals(output, "foo\ntrue\ntrue\ntrue\n"); }, }); Deno.test("[node/child_process execFileSync] 'inherit' stdout and stderr", () => { execFileSync(Deno.execPath(), ["--help"], { stdio: "inherit" }); }); Deno.test( "[node/child_process spawn] supports windowsVerbatimArguments option", { ignore: Deno.build.os !== "windows" }, async () => { const cmdFinished = Promise.withResolvers<void>(); let output = ""; const cp = spawn("cmd", ["/d", "/s", "/c", '"deno ^"--version^""'], { stdio: "pipe", windowsVerbatimArguments: true, }); cp.on("close", () => cmdFinished.resolve()); cp.stdout?.on("data", (data) => { output += data; }); await cmdFinished.promise; assertStringIncludes(output, "deno"); assertStringIncludes(output, "v8"); assertStringIncludes(output, "typescript"); }, ); Deno.test( "[node/child_process spawn] supports stdio array option", async () => { const cmdFinished = Promise.withResolvers<void>(); 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.promise; assertStringIncludes(output, "foo"); assertStringIncludes(output, "close"); }, ); Deno.test( "[node/child_process spawn] supports stdio [0, 1, 2] option", async () => { const cmdFinished = Promise.withResolvers<void>(); let output = ""; const script = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "testdata", "child_process_stdio_012.js", ); const cp = spawn(Deno.execPath(), ["run", "-A", script]); cp.stdout?.on("data", (data) => { output += data; }); cp.on("close", () => cmdFinished.resolve()); await cmdFinished.promise; assertStringIncludes(output, "foo"); assertStringIncludes(output, "close"); }, ); Deno.test({ name: "[node/child_process spawn] supports Deno child fd 3 pipe", ignore: Deno.build.os === "windows", async fn() { const cmdFinished = Promise.withResolvers<void>(); let output = ""; let stderr = ""; const script = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "testdata", "child_process_extra_stdio_fd3.js", ); const cp = spawn(Deno.execPath(), ["run", "-A", script], { stdio: ["ignore", "ignore", "pipe", "pipe"], }); const extra = cp.stdio[3]; if (!extra) { throw new Error("missing fd 3 pipe"); } extra.on("data", (data) => { output += data; }); cp.stderr?.on("data", (data) => { stderr += data; }); cp.on("close", (code) => { assertEquals(code, 0, stderr); assertEquals(stderr, ""); assertEquals(output, "hello from fd 3"); cmdFinished.resolve(); }); await cmdFinished.promise; }, }); Deno.test({ name: "[node/child_process spawn] concurrent spawns with extra pipe fds keep handles valid (#35994)", async fn() { // Playwright launches Firefox/WebKit with two extra pipe fds (3 and 4) // for its "pipe" browser-protocol transport. On Windows the child end of // each extra pipe used to be closed twice, which - under concurrent // spawns - raced with handle-value reuse and intermittently failed a // sibling spawn with "The handle is invalid. (os error 6)". Spawn a batch // concurrently to stress handle reuse and assert none of them fail. const isWindows = Deno.build.os === "windows"; const command = isWindows ? "cmd.exe" : "/bin/sh"; const args = isWindows ? ["/c", "exit 0"] : ["-c", "exit 0"]; const spawnOne = () => new Promise<void>((resolve, reject) => { const cp = spawn(command, args, { stdio: ["ignore", "pipe", "pipe", "pipe", "pipe"], }); cp.on("error", reject); // Drain every piped stream so the child's exit is observed promptly // and no pipe keeps the process from closing. for (const stream of cp.stdio) { // deno-lint-ignore no-explicit-any const s = stream as any; if (s && typeof s.resume === "function") { s.resume(); } } cp.on("close", (code) => { if (code !== 0) { reject(new Error(`child exited with code ${code}`)); } else { resolve(); } }); }); const BATCH = 24; await Promise.all(Array.from({ length: BATCH }, () => spawnOne())); }, }); Deno.test({ name: "[node/child_process spawn] Deno child fd 3 stays claimable by net.Socket", ignore: Deno.build.os === "windows", async fn() { const cmdFinished = Promise.withResolvers<void>(); let output = ""; let stderr = ""; const script = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "testdata", "child_process_extra_stdio_fd3_socket.js", ); const cp = spawn(Deno.execPath(), ["run", "-A", script], { stdio: ["ignore", "ignore", "pipe", "pipe"], }); const extra = cp.stdio[3]; if (!extra) { throw new Error("missing fd 3 pipe"); } extra.on("data", (data) => { output += data; }); cp.stderr?.on("data", (data) => { stderr += data; }); cp.on("close", (code) => { assertEquals(code, 0, stderr); assertEquals(stderr, ""); assertEquals(output, "hello from fd 3 and from fd 3 socket"); cmdFinished.resolve(); }); await cmdFinished.promise; }, }); Deno.test({ name: "[node/child_process spawn] Deno child adopts a TCP fd passed as extra stdio", ignore: Deno.build.os === "windows", async fn() { const cmdFinished = Promise.withResolvers<void>(); const received = Promise.withResolvers<string>(); const serverClosed = Promise.withResolvers<void>(); let stderr = ""; const script = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "testdata", "child_process_extra_stdio_fd3_tcp.js", ); const server = net.createServer((conn) => { let data = ""; conn.on("data", (chunk) => { data += chunk; }); conn.on("end", () => received.resolve(data)); }); server.listen(0, "127.0.0.1", () => { const address = server.address() as net.AddressInfo; const client = net.connect(address.port, "127.0.0.1", () => { client.pause(); // Grab a raw duplicate of the connected TCP fd to pass through the // child's stdio slot 3. The dup is intentionally left open here; only // the child writes to the connection, and its shutdown() delivers the // FIN to the server regardless of this process's remaining handles. // deno-lint-ignore no-explicit-any const rawFd = (client as any)._handle.fdForIpc(); assert(rawFd >= 0); const cp = spawn(Deno.execPath(), ["run", "-A", script], { stdio: ["ignore", "ignore", "pipe", rawFd], }); cp.stderr?.on("data", (data) => { stderr += data; }); cp.on("close", (code) => { client.destroy(); server.close(() => serverClosed.resolve()); assertEquals(code, 0, stderr); assertEquals(stderr, ""); cmdFinished.resolve(); }); }); }); await cmdFinished.promise; assertEquals(await received.promise, "hello from fd 3 tcp"); await serverClosed.promise; }, }); Deno.test({ name: "[node/child_process spawn] Deno child adopts a listening TCP fd passed as extra stdio", ignore: Deno.build.os === "windows", async fn() { const cmdFinished = Promise.withResolvers<void>(); const received = Promise.withResolvers<string>(); let stderr = ""; const script = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "testdata", "child_process_extra_stdio_fd3_tcp_listen.js", ); // Create a listening socket, then hand a raw dup of it to the child and // close our copy; the kernel keeps the socket listening through the dup // (which is intentionally left open in this process). const bootstrap = net.createServer(); bootstrap.listen(0, "127.0.0.1", () => { const address = bootstrap.address() as net.AddressInfo; // deno-lint-ignore no-explicit-any const rawFd = (bootstrap as any)._handle.fdForIpc(); assert(rawFd >= 0); bootstrap.close(() => { const cp = spawn(Deno.execPath(), ["run", "-A", script], { stdio: ["ignore", "ignore", "pipe", rawFd], }); cp.stderr?.on("data", (data) => { stderr += data; }); cp.on("close", (code) => { assertEquals(code, 0, stderr); assertEquals(stderr, ""); cmdFinished.resolve(); }); // The kernel queues this connection on the still-listening socket // until the child adopts fd 3 and accepts it. const client = net.connect(address.port, "127.0.0.1"); let data = ""; client.on("data", (chunk) => { data += chunk; }); client.on("end", () => received.resolve(data)); client.on("error", (err) => received.reject(err)); }); }); await cmdFinished.promise; assertEquals(await received.promise, "hello from fd 3 listener"); }, }); Deno.test({ name: "[node/child_process spawn] supports SIGIOT signal", ignore: Deno.build.os === "windows", async fn() { // Note: attempting to kill Deno with SIGABRT causes the process to zombify on certain OSX builds // eg: 22.5.0 Darwin Kernel Version 22.5.0: Mon Apr 24 20:53:19 PDT 2023; root:xnu-8796.121.2~5/RELEASE_ARM64_T6020 arm64 // M2 Pro running Ventura 13.4 // Spawn an infinite cat const cp = spawn("cat", ["-"]); const p = withTimeout<void>(); const pStdout = withTimeout<void>(); const pStderr = withTimeout<void>(); cp.on("exit", () => p.resolve()); cp.stdout.on("close", () => pStdout.resolve()); cp.stderr.on("close", () => pStderr.resolve()); cp.kill("SIGIOT"); await p.promise; await pStdout.promise; await pStderr.promise; assert(cp.killed); assertEquals(cp.signalCode, "SIGIOT"); }, }); Deno.test({ name: "[node/child_process spawn] AbortSignal aborts the child and emits AbortError", async fn() { const ac = new AbortController(); const cp = spawn( Deno.execPath(), ["eval", "setInterval(() => {}, 1000)"], { signal: ac.signal }, ); const error = withTimeout<Error>(); const exit = withTimeout<void>(); cp.on("error", (err) => error.resolve(err)); cp.on("exit", () => exit.resolve()); cp.on("spawn", () => ac.abort()); const err = await error.promise; assertEquals(err.name, "AbortError"); await exit.promise; }, }); Deno.test({ name: "[node/child_process] kill() on an exited process returns false and does not throw", async fn() { // Regression for https://github.com/denoland/deno/issues/28882 — killing // a process that has already exited must not surface a spurious error // (PermissionError on Windows / ESRCH on Unix), it should just return // false like Node.js does. const cp = spawn(Deno.execPath(), ["eval", "Deno.exit(0)"]); const exit = withTimeout<void>(); cp.on("exit", () => exit.resolve()); await exit.promise; assertEquals(cp.kill("SIGTERM"), false); }, }); Deno.test({ name: "[node/child_process spawn] child inherits Deno.env when options.env is not provided", async fn() { const deferred = withTimeout<string>(); Deno.env.set("BAR", "BAR"); const env = spawn( `"${Deno.execPath()}" eval -p "Deno.env.toObject().BAR"`, { shell: true, }, ); try { let envOutput = ""; assert(env.stdout); env.on("error", (err: Error) => deferred.reject(err)); env.stdout.on("data", (data) => { envOutput += data; }); env.on("close", () => { deferred.resolve(envOutput.trim()); }); await deferred.promise; } finally { env.kill(); Deno.env.delete("BAR"); } const value = await deferred.promise; assertEquals(value, "BAR"); }, }); Deno.test({ name: "[node/child_process spawn] child doesn't inherit Deno.env when options.env is provided", async fn() { const deferred = withTimeout<string>(); Deno.env.set("BAZ", "BAZ"); const env = spawn( `"${Deno.execPath()}" eval -p "Deno.env.toObject().BAZ"`, { env: {}, shell: true, }, ); try { let envOutput = ""; assert(env.stdout); env.on("error", (err: Error) => deferred.reject(err)); env.stdout.on("data", (data) => { envOutput += data; }); env.on("close", () => { deferred.resolve(envOutput.trim()); }); await deferred.promise; } finally { env.kill(); Deno.env.delete("BAZ"); } const value = await deferred.promise; assertEquals(value, "undefined"); }, }); // Regression test for https://github.com/denoland/deno/issues/20373 Deno.test(async function undefinedValueInEnvVar() { const deferred = withTimeout<string>(); const env = spawn( `"${Deno.execPath()}" eval -p "Deno.env.toObject().BAZ"`, { env: { BAZ: "BAZ", NO_COLOR: "true", UNDEFINED_ENV: undefined, // deno-lint-ignore no-explicit-any NULL_ENV: null as any, }, shell: true, }, ); try { let envOutput = ""; assert(env.stdout); env.on("error", (err: Error) => deferred.reject(err)); env.stdout.on("data", (data) => { envOutput += data; }); env.on("close", () => { deferred.resolve(envOutput.trim()); }); await deferred.promise; } finally { env.kill(); } const value = await deferred.promise; assertEquals(value, "BAZ"); }); // Regression test for https://github.com/denoland/deno/issues/20373 Deno.test(function spawnSyncUndefinedValueInEnvVar() { const ret = spawnSync( `"${Deno.execPath()}" eval -p "Deno.env.toObject().BAZ"`, { env: { BAZ: "BAZ", NO_COLOR: "true", UNDEFINED_ENV: undefined, // deno-lint-ignore no-explicit-any NULL_ENV: null as any, }, shell: true, }, ); assertEquals(ret.status, 0); assertEquals(ret.stdout.toString("utf-8").trim(), "BAZ"); }); Deno.test(function spawnSyncStdioUndefined() { const ret = spawnSync( `"${Deno.execPath()}" eval "console.log('hello');console.error('world')"`, { stdio: [undefined, undefined, undefined], shell: true, }, ); assertEquals(ret.status, 0); assertEquals(ret.stdout.toString("utf-8").trim(), "hello"); assertEquals(ret.stderr.toString("utf-8").trim(), "world"); }); Deno.test(function spawnSyncExitNonZero() { const ret = spawnSync( `"${Deno.execPath()}" eval "Deno.exit(22)"`, { shell: true }, ); assertEquals(ret.status, 22); }); // https://github.com/denoland/deno/issues/21630 Deno.test(async function forkIpcKillDoesNotHang() { const testdataDir = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "testdata", ); const script = path.join( testdataDir, "node_modules", "foo", "index.js", ); const p = Promise.withResolvers<void>(); const cp = CP.fork(script, [], { cwd: testdataDir, stdio: ["inherit", "inherit", "inherit", "ipc"], }); cp.on("close", () => p.resolve()); cp.kill(); await p.promise; }); Deno.test(async function stripForkEnableSourceMaps() { const testdataDir = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "testdata", ); const script = path.join( testdataDir, "node_modules", "foo", "check_argv.js", ); const p = Promise.withResolvers<void>(); const cp = CP.fork(script, [], { cwd: testdataDir, stdio: "pipe", execArgv: ["--enable-source-maps"], }); let output = ""; cp.on("close", () => p.resolve()); cp.stdout?.on("data", (data) => { output += data; cp.kill(); }); await p.promise; assertEquals(output, "2\n"); }); Deno.test(async function execFileWithUndefinedTimeout() { const { promise, resolve, reject } = Promise.withResolvers<void>(); CP.execFile( "git", ["--version"], { timeout: undefined, encoding: "utf8" }, (err) => { if (err) { reject(err); return; } resolve(); }, ); await promise; }); Deno.test(async function spawnCommandNotFoundErrno() { const { promise, resolve } = Promise.withResolvers<void>(); const cp = CP.spawn("no-such-command"); cp.on("error", (err) => { const errno = Deno.build.os === "windows" ? -4058 : -2; // @ts-ignore: errno missing from typings assertEquals(err.errno, errno); resolve(); }); await promise; }); // https://github.com/denoland/deno/issues/23045 Deno.test(function spawnCommandNullStdioArray() { const ret = spawnSync( `"${Deno.execPath()}" eval "console.log('hello');console.error('world')"`, { stdio: [null, null, null], shell: true, }, ); assertEquals(ret.status, 0); }); Deno.test( function stdinInherit() { const script = ` function timeoutPromise(promise, timeout) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { Deno.exit(69); }, timeout); promise.then((value) => { clearTimeout(timeoutId); resolve(value); }, (reason) => { clearTimeout(timeoutId); reject(reason); }); }); } await timeoutPromise(Deno.stdin.read(new Uint8Array(1)), 100) `; const output = spawnSync(Deno.execPath(), ["eval", script], { stdio: "inherit", }); // We want to timeout to occur because the stdin isn't 'null' assertEquals(output.status, 69); assertEquals(output.stdout, null); assertEquals(output.stderr, null); }, ); Deno.test( async function ipcSerialization() { const timeout = withTimeout<void>(); const script = ` if (typeof process.send !== "function") { console.error("process.send is not a function"); process.exit(1); } class BigIntWrapper { constructor(value) { this.value = value; } toJSON() { return this.value.toString(); } } const makeSab = (arr) => { const sab = new SharedArrayBuffer(arr.length); const buf = new Uint8Array(sab); for (let i = 0; i < arr.length; i++) { buf[i] = arr[i]; } return buf; }; const inputs = [ "foo", { foo: "bar", }, 42, true, null, new Uint8Array([1, 2, 3]), { foo: new Uint8Array([1, 2, 3]), bar: makeSab([4, 5, 6]), }, [1, { foo: 2 }, [3, 4]], new BigIntWrapper(42n), ]; for (const input of inputs) { process.send(input); } `; const file = await Deno.makeTempFile(); await Deno.writeTextFile(file, script); const child = CP.fork(file, [], { stdio: ["inherit", "inherit", "inherit", "ipc"], }); const expect = [ "foo", { foo: "bar", }, 42, true, null, [1, 2, 3], { foo: [1, 2, 3], bar: [4, 5, 6], }, [1, { foo: 2 }, [3, 4]], "42", ]; let i = 0; child.on("message", (message) => { assertEquals(message, expect[i]); i++; }); child.on("close", () => timeout.resolve()); await timeout.promise; assertEquals(i, expect.length); }, ); Deno.test(async function ipcSerializationAdvanced() { const timeout = withTimeout<void>(); const script = ` if (typeof process.send !== "function") { console.error("process.send is not a function"); process.exit(1); } class BigIntWrapper { constructor(value) { this.value = value; } toJSON() { return this.value.toString(); } } const makeSab = (arr) => { const sab = new SharedArrayBuffer(arr.length); const buf = new Uint8Array(sab); for (let i = 0; i < arr.length; i++) { buf[i] = arr[i]; } return buf; }; const inputs = [ "foo", { foo: "bar", }, 42, true, null, new Uint8Array([1, 2, 3]), // Empty typed arrays / buffers used to panic the IPC deserializer // because a zero-length ArrayBuffer has a null backing store. new Uint8Array([]), Buffer.alloc(0), { foo: new Uint8Array([1, 2, 3]), bar: makeSab([4, 5, 6]), baz: new Uint8Array([]), }, [1, { foo: 2 }, [3, 4]], 42n, ]; for (const input of inputs) { process.send(input); } `; const makeSab = (arr: number[]) => { const sab = new SharedArrayBuffer(arr.length); const buf = new Uint8Array(sab); for (let i = 0; i < arr.length; i++) { buf[i] = arr[i]; } return buf; }; const file = await Deno.makeTempFile(); await Deno.writeTextFile(file, script); const child = CP.fork(file, [], { stdio: ["inherit", "inherit", "inherit", "ipc"], serialization: "advanced", }); const expect = [ "foo", { foo: "bar", }, 42, true, null, new Uint8Array([1, 2, 3]), new Uint8Array([]), Buffer.alloc(0), { foo: new Uint8Array([1, 2, 3]), bar: makeSab([4, 5, 6]), baz: new Uint8Array([]), }, [1, { foo: 2 }, [3, 4]], 42n, ]; let i = 0; child.on("message", (message) => { assertEquals(message, expect[i]); i++; }); child.on("close", () => timeout.resolve()); await timeout.promise; assertEquals(i, expect.length); }); Deno.test(async function childProcessExitsGracefully() { const testdataDir = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "testdata", ); const script = path.join( testdataDir, "node_modules", "foo", "index.js", ); const p = Promise.withResolvers<void>(); const cp = CP.fork(script, [], { cwd: testdataDir, stdio: ["inherit", "inherit", "inherit", "ipc"], }); cp.on("close", () => p.resolve()); await p.promise; }); Deno.test(async function killMultipleTimesNoError() { const loop = ` while (true) { await new Promise((resolve) => setTimeout(resolve, 10000)); } `; const timeout = withTimeout<void>(); const file = await Deno.makeTempFile(); await Deno.writeTextFile(file, loop); const child = CP.fork(file, [], { stdio: ["inherit", "inherit", "inherit", "ipc"], }); child.on("close", () => { timeout.resolve(); }); child.kill(); child.kill(); // explicitly calling disconnect after kill should throw assertThrows(() => child.disconnect()); await timeout.promise; }); // Make sure that you receive messages sent before a "message" event listener is set up Deno.test(async function bufferMessagesIfNoListener() { const code = ` process.on("message", (_) => { process.channel.unref(); }); process.send("hello"); process.send("world"); console.error("sent messages"); `; const file = await Deno.makeTempFile(); await Deno.writeTextFile(file, code); const timeout = withTimeout<void>(); const child = CP.fork(file, [], { stdio: ["inherit", "inherit", "pipe", "ipc"], }); let got = 0; child.on("message", (message) => { if (got++ === 0) { assertEquals(message, "hello"); } else { assertEquals(message, "world"); } }); child.on("close", () => { timeout.resolve(); }); let stderr = ""; child.stderr?.on("data", (data) => { stderr += data; if (stderr.includes("sent messages")) { // now that we've set up the listeners, and the child // has sent the messages, we can let it exit child.send("ready"); } }); await timeout.promise; assertEquals(got, 2); }); Deno.test(async function sendAfterClosedThrows() { const code = ``; const file = await Deno.makeTempFile(); await Deno.writeTextFile(file, code); const timeout = withTimeout<void>(); const child = CP.fork(file, [], { stdio: ["inherit", "inherit", "inherit", "ipc"], }); child.on("error", (err) => { assert("code" in err); assertEquals(err.code, "ERR_IPC_CHANNEL_CLOSED"); timeout.resolve(); }); child.on("close", () => { child.send("ready"); }); await timeout.promise; }); Deno.test(async function noWarningsFlag() { const code = ``; const file = await Deno.makeTempFile(); await Deno.writeTextFile(file, code); const timeout = withTimeout<void>(); const child = CP.fork(file, [], { execArgv: ["--no-warnings"], stdio: ["inherit", "inherit", "inherit", "ipc"], }); child.on("close", () => { timeout.resolve(); }); await timeout.promise; }); Deno.test({ name: "[node/child_process] spawnSync supports input option", fn() { const text = " console.log('hello')"; const expected = `console.log("hello");\n`; { const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], { input: text, }); assertEquals(stdout.toString(), expected); } { const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], { input: Buffer.from(text), }); assertEquals(stdout.toString(), expected); } { const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], { input: new TextEncoder().encode(text), }); assertEquals(stdout.toString(), expected); } { const b = Buffer.from(text); const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], { input: new DataView(b.buffer, b.byteOffset, b.byteLength), }); assertEquals(stdout.toString(), expected); } assertThrows( () => { spawnSync(Deno.execPath(), ["fmt", "-"], { // deno-lint-ignore no-explicit-any input: {} as any, }); }, Error, 'The "input" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Object', ); }, }); // Regression test for https://github.com/denoland/deno/issues/25776 Deno.test(async function killSignalZero() { const child = CP.spawn(Deno.execPath(), [ "eval", "setTimeout(() => {}, 60_000)", ]); try { // Signal 0 checks if the process exists without sending a signal const alive = child.kill(0); assertEquals(alive, true); // The child should not be marked as killed assertEquals(child.killed, false); } finally { child.kill(); await new Promise((resolve) => child.on("close", resolve)); } }); Deno.test(async function experimentalFlag() { const code = ``; const file = await Deno.makeTempFile(); await Deno.writeTextFile(file, code); const timeout = withTimeout<void>(); const child = CP.fork(file, [], { execArgv: ["--experimental-vm-modules"], stdio: ["inherit", "inherit", "inherit", "ipc"], }); child.on("close", () => { timeout.resolve(); }); await timeout.promise; }); // Regression test for https://github.com/denoland/deno/issues/26784 // process.stdout partial writes were silently dropped, causing data // truncation when piping >64KB through a subprocess. Deno.test(async function stdoutPipePartialWriteNotTruncated() { // Child script: pipe a file to stdout via createReadStream const pipeScript = await Deno.makeTempFile({ suffix: ".mjs" }); await Deno.writeTextFile( pipeScript, `import fs from "node:fs";fs.createReadStream(process.argv[2]).pipe(process.stdout);`, ); // Create a file >65536 bytes with multiple lines (triggers chunked reads) const dataFile = await Deno.makeTempFile(); const content = "x".repeat(40000) + "\n" + "y".repeat(40000) + "\n"; await Deno.writeTextFile(dataFile, content); try { const output = execFileSync(Deno.execPath(), [ "run", "--allow-read", pipeScript, dataFile, ], { encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 }); assertEquals(output.length, content.length); assertEquals(output, content); } finally { await Deno.remove(pipeScript); await Deno.remove(dataFile); } }); Deno.test(async function stdoutWriteMultipleChunksNotTruncated() { // Child script: write two large chunks to stdout directly const script = await Deno.makeTempFile({ suffix: ".mjs" }); await Deno.writeTextFile( script, `const chunk1 = "A".repeat(50000);const chunk2 = "B".repeat(50000);process.stdout.write(chunk1);process.stdout.write(chunk2);`, ); try { const output = execFileSync(Deno.execPath(), ["run", script], { encoding: "utf-8", maxBuffer: 50 * 1024 * 1024, }); assertEquals(output.length, 100000); assertEquals(output, "A".repeat(50000) + "B".repeat(50000)); } finally { await Deno.remove(script); } }); Deno.test(function spawnSyncShellMetacharactersEscaped() { // Shell metacharacters in args should be escaped, not interpreted. // On Unix, & would launch a background process; on Windows, it // would chain a second command. Either way echo should print // the literal string. const ret = spawnSync( Deno.execPath(), ["eval", "console.log(Deno.args[0])", "--", "a&b|c<d>e"], { shell: true, encoding: "utf-8" }, ); assertEquals(ret.status, 0); assertEquals(ret.stdout.trim(), "a&b|c<d>e"); }); Deno.test({ name: "spawn shell preserves pre-quoted arguments", ignore: Deno.build.os === "windows", async fn() { for (const quote of ["'", '"']) { const deferred = withTimeout<number | null>(); const child = spawn( "printf", [`${quote}<%s>\\n${quote}`, `${quote}hello world${quote}`], { shell: true }, ); let stdout = ""; child.stdout!.setEncoding("utf-8"); child.stdout!.on("data", (chunk) => stdout += chunk); child.on("error", deferred.reject); child.on("close", deferred.resolve); assertEquals(await deferred.promise, 0); assertEquals(stdout, "<hello world>\n"); } }, }); Deno.test({ name: "spawnSync shell preserves pre-quoted arguments", ignore: Deno.build.os === "windows", fn() { for (const quote of ["'", '"']) { const ret = spawnSync( "printf", [`${quote}<%s>\\n${quote}`, `${quote}hello world${quote}`], { shell: true, encoding: "utf-8" }, ); assertEquals(ret.status, 0); assertEquals(ret.stdout, "<hello world>\n"); } const breakout = spawnSync( "printf", ["'%s\\n'", "'safe'; printf injected; 'still-safe'"], { shell: true, encoding: "utf-8" }, ); assertEquals(breakout.status, 0); assertEquals( breakout.stdout, "safe'; printf injected; 'still-safe\n", ); }, }); Deno.test(function spawnSyncReturnsPid() { const ret = spawnSync(Deno.execPath(), ["eval", "console.log('hi')"]); assertEquals(ret.status, 0); assertEquals(typeof ret.pid, "number"); assert(ret.pid > 0); }); Deno.test({ name: "spawnWithNumericFdInStdioArray", ignore: Deno.build.os === "windows", async fn() { const fs = await import("node:fs"); const tmpFile = Deno.makeTempFileSync(); try { const fd = fs.openSync(tmpFile, "w"); const child = spawn("/bin/sh", [ "-c", "echo hello from child >&3", ], { stdio: ["ignore", "pipe", "pipe", fd], }); const { promise, resolve } = Promise.withResolvers<number>(); child.on("close", (code: number) => { resolve(code); }); const code = await promise; fs.closeSync(fd); assertEquals(code, 0); const content = Deno.readTextFileSync(tmpFile); assertEquals(content, "hello from child\n"); } finally { Deno.removeSync(tmpFile); } }, });