Even more child process tests passing (#18052)

This commit is contained in:
pfg
2025-03-11 22:52:12 -07:00
committed by GitHub
parent 5c7df736bf
commit fdd181d68d
52 changed files with 2650 additions and 281 deletions

View File

@@ -178,9 +178,6 @@ function spawn(file, args, options) {
abortChildProcess(child, killSignal, signal.reason);
}
}
process.nextTick(() => {
child.emit("spawn");
});
return child;
}
@@ -295,11 +292,11 @@ function execFile(file, args, options, callback) {
if (args?.length) cmd += ` ${ArrayPrototypeJoin.$call(args, " ")}`;
if (!ex) {
const { getSystemErrorName } = require("node:util");
let message = `Command failed: ${cmd}`;
if (stderr) message += `\n${stderr}`;
ex = genericNodeError(message, {
// code: code < 0 ? getSystemErrorName(code) : code, // TODO: Add getSystemErrorName
code: code,
code: code < 0 ? getSystemErrorName(code) : code,
killed: child.killed || killed,
signal: signal,
});
@@ -565,13 +562,21 @@ function spawnSync(file, args, options) {
success,
exitCode,
signalCode,
exitedDueToTimeout,
pid,
} = Bun.spawnSync({
cmd: options.args,
// normalizeSpawnargs has already prepended argv0 to the spawnargs array
// Bun.spawn() expects cmd[0] to be the command to run, and argv0 to replace the first arg when running the command,
// so we have to set argv0 to spawnargs[0] and cmd[0] to file
cmd: [options.file, ...Array.prototype.slice.$call(options.args, 1)],
env: options.env || undefined,
cwd: options.cwd || undefined,
stdio: bunStdio,
windowsVerbatimArguments: options.windowsVerbatimArguments,
windowsHide: options.windowsHide,
argv0: options.args[0],
timeout: options.timeout,
killSignal: options.killSignal,
});
} catch (err) {
error = err;
@@ -584,6 +589,7 @@ function spawnSync(file, args, options) {
status: exitCode,
// TODO: Need to expose extra pipes from Bun.spawnSync to child_process
output: [null, stdout, stderr],
pid,
};
if (error) {
@@ -601,16 +607,24 @@ function spawnSync(file, args, options) {
result.stdout = result.output[1];
result.stderr = result.output[2];
if (!success && error == null) {
result.error = new SystemError(result.output[2], options.file, "spawnSync", -1, result.status);
if (exitedDueToTimeout && error == null) {
result.error = new SystemError(
"spawnSync " + options.file + " ETIMEDOUT",
options.file,
"spawnSync " + options.file,
etimedoutErrorCode(),
"ETIMEDOUT",
);
}
if (result.error) {
result.error.syscall = "spawnSync " + options.file;
result.error.spawnargs = ArrayPrototypeSlice.$call(options.args, 1);
}
return result;
}
const etimedoutErrorCode = $newZigFunction("node_util_binding.zig", "etimedoutErrorCode", 0);
/**
* Spawns a file as a shell synchronously.
@@ -770,7 +784,7 @@ function fork(modulePath, args = [], options) {
// and stderr from the parent if silent isn't set.
options.stdio = stdioStringToArray(options.silent ? "pipe" : "inherit", "ipc");
} else if (!ArrayPrototypeIncludes.$call(options.stdio, "ipc")) {
throw ERR_CHILD_PROCESS_IPC_REQUIRED("options.stdio");
throw $ERR_CHILD_PROCESS_IPC_REQUIRED("options.stdio");
}
return spawn(options.execPath, args, options);
@@ -870,6 +884,7 @@ function normalizeExecArgs(command, options, callback) {
};
}
const kBunEnv = Symbol("bunEnv");
function normalizeSpawnArguments(file, args, options) {
validateString(file, "file");
validateArgumentNullCheck(file, "file");
@@ -966,7 +981,7 @@ function normalizeSpawnArguments(file, args, options) {
}
const env = options.env || process.env;
const envPairs = {};
const bunEnv = {};
// // process.env.NODE_V8_COVERAGE always propagates, making it possible to
// // collect coverage for programs that spawn with white-listed environment.
@@ -995,7 +1010,7 @@ function normalizeSpawnArguments(file, args, options) {
if (value !== undefined) {
validateArgumentNullCheck(key, `options.env['${key}']`);
validateArgumentNullCheck(value, `options.env['${key}']`);
envPairs[key] = value;
bunEnv[key] = value;
}
}
@@ -1005,11 +1020,13 @@ function normalizeSpawnArguments(file, args, options) {
...options,
args,
cwd,
detached: !!options.detached,
envPairs,
[kBunEnv]: bunEnv,
file,
windowsHide: !!options.windowsHide,
windowsVerbatimArguments: !!windowsVerbatimArguments,
argv0: options.argv0,
};
}
@@ -1026,6 +1043,15 @@ function checkExecSyncError(ret, args, cmd?) {
}
return err;
}
function parseEnvPairs(envPairs: string[] | undefined): Record<string, string> | undefined {
if (!envPairs) return undefined;
const resEnv = {};
for (const line of envPairs) {
const [key, ...value] = line.split("=", 2);
resEnv[key] = value.join("=");
}
return resEnv;
}
//------------------------------------------------------------------------------
// Section 3. ChildProcess class
@@ -1241,28 +1267,19 @@ class ChildProcess extends EventEmitter {
validateOneOf(options.serialization, "options.serialization", [undefined, "json", "advanced"]);
const serialization = options.serialization || "json";
validateString(options.file, "options.file");
// NOTE: This is confusing... So node allows you to pass a file name
// But also allows you to pass a command in the args and it should execute
// To add another layer of confusion, they also give the option to pass an explicit "argv0"
// which overrides the actual command of the spawned process...
var file;
file = this.spawnfile = options.file;
var spawnargs;
if (options.args == null) {
spawnargs = this.spawnargs = [];
} else {
validateArray(options.args, "options.args");
spawnargs = this.spawnargs = options.args;
}
const stdio = options.stdio || ["pipe", "pipe", "pipe"];
const bunStdio = getBunStdioFromOptions(stdio);
const argv0 = file || options.argv0;
const has_ipc = $isJSArray(stdio) && stdio[3] === "ipc";
var env = options.envPairs || process.env;
const has_ipc = $isJSArray(stdio) && stdio.includes("ipc");
// validate options.envPairs but only if has_ipc. for some reason.
if (has_ipc) {
if (options.envPairs !== undefined) {
validateArray(options.envPairs, "options.envPairs");
}
}
var env = options[kBunEnv] || parseEnvPairs(options.envPairs) || process.env;
const detachedOption = options.detached;
this.#encoding = options.encoding || undefined;
@@ -1270,55 +1287,84 @@ class ChildProcess extends EventEmitter {
const stdioCount = stdio.length;
const hasSocketsToEagerlyLoad = stdioCount >= 3;
this.#handle = Bun.spawn({
cmd: spawnargs,
stdio: bunStdio,
cwd: options.cwd || undefined,
env: env,
detached: typeof detachedOption !== "undefined" ? !!detachedOption : false,
onExit: (handle, exitCode, signalCode, err) => {
this.#handle = handle;
this.pid = this.#handle.pid;
$debug("ChildProcess: onExit", exitCode, signalCode, err, this.pid);
validateString(options.file, "options.file");
var file;
file = this.spawnfile = options.file;
if (hasSocketsToEagerlyLoad) {
process.nextTick(() => {
this.stdio;
$debug("ChildProcess: onExit", exitCode, signalCode, err, this.pid);
});
}
process.nextTick(
(exitCode, signalCode, err) => this.#handleOnExit(exitCode, signalCode, err),
exitCode,
signalCode,
err,
);
},
lazy: true,
ipc: has_ipc ? this.#emitIpcMessage.bind(this) : undefined,
onDisconnect: has_ipc ? ok => this.#disconnect(ok) : undefined,
serialization,
argv0,
windowsHide: !!options.windowsHide,
windowsVerbatimArguments: !!options.windowsVerbatimArguments,
});
this.pid = this.#handle.pid;
$debug("ChildProcess: spawn", this.pid, spawnargs);
onSpawnNT(this);
if (has_ipc) {
this.send = this.#send;
this.disconnect = this.#disconnect;
if (options[kFromNode]) this.#closesNeeded += 1;
var spawnargs;
if (options.args === undefined) {
spawnargs = this.spawnargs = [];
// how is this allowed?
} else {
validateArray(options.args, "options.args");
spawnargs = this.spawnargs = options.args;
}
// normalizeSpawnargs has already prepended argv0 to the spawnargs array
// Bun.spawn() expects cmd[0] to be the command to run, and argv0 to replace the first arg when running the command,
// so we have to set argv0 to spawnargs[0] and cmd[0] to file
if (hasSocketsToEagerlyLoad) {
for (let item of this.stdio) {
item?.ref?.();
try {
this.#handle = Bun.spawn({
cmd: [file, ...Array.prototype.slice.$call(spawnargs, 1)],
stdio: bunStdio,
cwd: options.cwd || undefined,
env: env,
detached: typeof detachedOption !== "undefined" ? !!detachedOption : false,
onExit: (handle, exitCode, signalCode, err) => {
this.#handle = handle;
this.pid = this.#handle.pid;
$debug("ChildProcess: onExit", exitCode, signalCode, err, this.pid);
if (hasSocketsToEagerlyLoad) {
process.nextTick(() => {
this.stdio;
$debug("ChildProcess: onExit", exitCode, signalCode, err, this.pid);
});
}
process.nextTick(
(exitCode, signalCode, err) => this.#handleOnExit(exitCode, signalCode, err),
exitCode,
signalCode,
err,
);
},
lazy: true,
ipc: has_ipc ? this.#emitIpcMessage.bind(this) : undefined,
onDisconnect: has_ipc ? ok => this.#onDisconnect(ok) : undefined,
serialization,
argv0: spawnargs[0],
windowsHide: !!options.windowsHide,
windowsVerbatimArguments: !!options.windowsVerbatimArguments,
});
this.pid = this.#handle.pid;
$debug("ChildProcess: spawn", this.pid, spawnargs);
process.nextTick(() => {
this.emit("spawn");
});
if (has_ipc) {
this.send = this.#send;
this.disconnect = this.#disconnect;
if (options[kFromNode]) this.#closesNeeded += 1;
}
if (hasSocketsToEagerlyLoad) {
for (let item of this.stdio) {
item?.ref?.();
}
}
} catch (ex) {
if (ex == null || typeof ex !== "object" || !Object.hasOwn(ex, "errno")) throw ex;
this.#handle = null;
ex.syscall = "spawn " + this.spawnfile;
ex.spawnargs = Array.prototype.slice.$call(this.spawnargs, 1);
process.nextTick(() => {
this.emit("error", ex);
this.emit("close", (ex as SystemError).errno ?? -1);
});
}
}
@@ -1352,7 +1398,7 @@ class ChildProcess extends EventEmitter {
// Bun does not handle handles yet
try {
this.#handle.send(message);
if (callback) process.nextTick(callback);
if (callback) process.nextTick(callback, null);
return true;
} catch (error) {
if (callback) {
@@ -1364,18 +1410,21 @@ class ChildProcess extends EventEmitter {
}
}
#disconnect(ok) {
if (ok == null) {
$assert(this.connected);
this.#handle.disconnect();
} else if (!ok) {
#onDisconnect(firstTime: boolean) {
if (!firstTime) {
// strange
return;
}
$assert(!this.connected);
this.#maybeClose();
process.nextTick(() => this.emit("disconnect"));
}
#disconnect() {
if (!this.connected) {
this.emit("error", $ERR_IPC_DISCONNECTED());
return;
}
this.#handle.disconnect();
$assert(!this.connected);
process.nextTick(() => this.emit("disconnect"));
this.#maybeClose();
}
kill(sig?) {
@@ -1552,10 +1601,6 @@ function normalizeStdio(stdio): string[] {
}
}
function onSpawnNT(self) {
self.emit("spawn");
}
function abortChildProcess(child, killSignal, reason) {
if (!child) return;
try {
@@ -1599,6 +1644,10 @@ class ShimmedStdioOutStream extends EventEmitter {
destroy() {
return this;
}
setEncoding() {
return this;
}
}
//------------------------------------------------------------------------------
@@ -1695,11 +1744,10 @@ var Error = globalThis.Error;
var TypeError = globalThis.TypeError;
var RangeError = globalThis.RangeError;
function genericNodeError(message, options) {
function genericNodeError(message, errorProperties) {
// eslint-disable-next-line no-restricted-syntax
const err = new Error(message);
err.code = options.code;
err.killed = options.killed;
err.signal = options.signal;
ObjectAssign(err, errorProperties);
return err;
}
@@ -1852,12 +1900,6 @@ function ERR_INVALID_OPT_VALUE(name, value) {
return err;
}
function ERR_CHILD_PROCESS_IPC_REQUIRED(name) {
const err = new TypeError(`Forked processes must have an IPC channel, missing value 'ipc' in ${name}`);
err.code = "ERR_CHILD_PROCESS_IPC_REQUIRED";
return err;
}
class SystemError extends Error {
path;
syscall;

View File

@@ -12,8 +12,6 @@ function initializeClusterIPC() {
cluster._setupWorker();
// Make sure it's not accidentally inherited by child processes.
delete process.env.NODE_UNIQUE_ID;
process.channel.unref();
}
}