// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import CP from "node:child_process"; import { Buffer } from "node:buffer"; import { assert, assertEquals, assertExists, assertNotStrictEquals, assertStrictEquals, assertStringIncludes, } from "@std/assert/mod.ts"; import * as path from "@std/path/mod.ts"; const { spawn, spawnSync, execFile, execFileSync, ChildProcess } = CP; function withTimeout( timeoutInMS = 10_000, ): ReturnType> { const deferred = Promise.withResolvers(); 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(); 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(); 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(); const childProcess = spawn(Deno.execPath(), ["--help"], { env: { NO_COLOR: "true" }, }); 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(); 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(); 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 { const deferred = withTimeout(); 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(); 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[]; function mustBeCalledAfterSpawn() { const deferred = Promise.withResolvers(); 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(); 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(); 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(); 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(); 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((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((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({ name: "[node/child_process] ChildProcess.kill()", async fn() { const script = path.join( path.dirname(path.fromFileUrl(import.meta.url)), "./testdata/infinite_loop.js", ); const childProcess = spawn(Deno.execPath(), ["run", script]); const p = withTimeout(); const pStdout = withTimeout(); const pStderr = withTimeout(); childProcess.on("exit", () => p.resolve()); childProcess.stdout.on("close", () => pStdout.resolve()); childProcess.stderr.on("close", () => pStderr.resolve()); childProcess.kill("SIGKILL"); await p.promise; await pStdout.promise; await pStderr.promise; assert(childProcess.killed); assertEquals(childProcess.signalCode, "SIGKILL"); assertExists(childProcess.exitCode); }, }); 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", "--unstable", script, ]); const deferred = Promise.withResolvers(); 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(); 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("[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(); 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(); 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(); 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 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(); const pStdout = withTimeout(); const pStderr = withTimeout(); 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"); }, }); // Regression test for https://github.com/denoland/deno/issues/20373 Deno.test(async function undefinedValueInEnvVar() { const deferred = withTimeout(); 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(); 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(); 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(); 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(); 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); }, );