1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-19 04:16:00 -05:00
denoland-deno/tests/unit_node/child_process_test.ts
TateKennington 5a39f2f096
fix(node): Prevent node:child_process from always inheriting the parent environment (#27343) (#27340)
Fixes #27343

Currently the node:child_process polyfill is always passing the full
parent environment to all spawned subprocesses. In the case where
`options.env` is provided those keys are overridden but the rest of the
parent environment is still passed through.

On Node the behaviour is for child processes to only inherit the parent
environment when `options.env` isn't specified. When `options.env` is
specified the child process inherits only those keys.

This PR updates the internal node child_process polyfill so that the
`clearEnv` argument is set to true when spawning the subprocess to
prevent the parent environment always being inherited by default. It
also fixes an issue where `normalizeSpawnArguments` wasn't returning the
`env` option if `options.env` was unset.
2025-01-13 13:46:56 -08:00

1130 lines
30 KiB
TypeScript

// Copyright 2018-2025 the Deno authors. MIT license.
import CP from "node:child_process";
import { Buffer } from "node:buffer";
import {
assert,
assertEquals,
assertExists,
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({
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<void>();
const pStdout = withTimeout<void>();
const pStderr = withTimeout<void>();
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",
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("[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 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] 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 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;
});