/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/node/polyfills/internal/child_process.ts
2 983 строки
89 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. // This module implements 'child_process' module of Node.JS API. // ref: https://nodejs.org/api/child_process.html (function () { const { core, primordials } = __bootstrap; const { ArrayBufferIsView, ArrayPrototypeFindIndex, ArrayPrototypeIncludes, ArrayPrototypeIndexOf, ArrayPrototypeMap, DataViewPrototypeGetBuffer, DataViewPrototypeGetByteLength, DataViewPrototypeGetByteOffset, Error, ObjectEntries, ObjectKeys, ObjectPrototype, ObjectPrototypeIsPrototypeOf, ObjectSetPrototypeOf, PromisePrototypeThen, PromiseWithResolvers, ReflectHas, RegExpPrototypeExec, RegExpPrototypeTest, SafeArrayIterator, SafeMap, SafeMapIterator, SafePromiseAll, SafeRegExp, SafeSet, SafeWeakMap, String, StringPrototypeEndsWith, StringPrototypeIndexOf, StringPrototypeMatch, StringPrototypeReplace, StringPrototypeSubstring, StringPrototypeTrimStart, Symbol, SymbolDispose, TypeErrorPrototype, TypedArrayPrototypeGetBuffer, TypedArrayPrototypeGetByteLength, TypedArrayPrototypeGetByteOffset, } = primordials; const { isTypedArray } = core; const { op_node_in_npm_package, op_node_ipc_buffer_constructor, op_node_ipc_read_advanced, op_node_ipc_read_json, op_node_ipc_ref, op_node_ipc_unref, op_node_ipc_write_advanced, op_node_ipc_write_json, op_node_parse_shell_args, op_node_translate_cli_args, } = core.ops; const { ArrayIsArray, ArrayPrototypeFilter, ArrayPrototypeJoin, ArrayPrototypePush, ArrayPrototypeSlice, ArrayPrototypeSort, ArrayPrototypeUnshift, ObjectHasOwn, StringPrototypeIncludes, StringPrototypeStartsWith, StringPrototypeToUpperCase, } = core.loadExtScript("ext:deno_node/internal/primordials.mjs"); const { default: assert } = core.loadExtScript("ext:deno_node/assert.ts"); const { EventEmitter } = core.loadExtScript("ext:deno_node/_events.mjs"); const { os } = core.loadExtScript( "ext:deno_node/internal_binding/constants.ts", ); const { notImplemented } = core.loadExtScript("ext:deno_node/_utils.ts"); const lazyStream = core.createLazyLoader("node:stream"); const { isWindows } = core.loadExtScript("ext:deno_node/_util/os.ts"); const { nextTick } = core.loadExtScript("ext:deno_node/_next_tick.ts"); const { AbortError, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_INVALID_HANDLE_TYPE, ERR_INVALID_SYNC_FORK_INPUT, ERR_IPC_CHANNEL_CLOSED, ERR_IPC_ONE_PIPE, ERR_IPC_SYNC_FORK, ERR_MISSING_ARGS, ERR_UNKNOWN_SIGNAL, } = core.loadExtScript("ext:deno_node/internal/errors.ts"); const { Buffer } = core.loadExtScript("ext:deno_node/internal/buffer.mjs"); const { FastBuffer } = core.loadExtScript( "ext:deno_node/internal/buffer.mjs", ); const { ERR_IPC_DISCONNECTED, errnoException, } = core.loadExtScript("ext:deno_node/internal/errors.ts"); const { codeMap } = core.loadExtScript( "ext:deno_node/internal_binding/uv.ts", ); const { validateBoolean, validateInt32, validateObject, validateOneOf, validateString, } = core.loadExtScript("ext:deno_node/internal/validators.mjs"); const { kEmptyObject } = core.loadExtScript( "ext:deno_node/internal/util.mjs", ); const lazyFsUtils = core.createLazyLoader( "ext:deno_node/internal/fs/utils.mjs", ); const lazyProcess = core.createLazyLoader("node:process"); const { StringPrototypeSlice } = core.loadExtScript( "ext:deno_node/internal/primordials.mjs", ); const { Pipe, socketType } = core.loadExtScript( "ext:deno_node/internal_binding/pipe_wrap.ts", ); const { socketType: tcpSocketType, TCP, } = core.loadExtScript("ext:deno_node/internal_binding/tcp_wrap.ts"); const lazyNet = core.createLazyLoader("node:net"); const { channel: createDiagnosticsChannel, tracingChannel } = core .loadExtScript( "ext:deno_node/diagnostics_channel.js", ); const childProcessChannel = createDiagnosticsChannel("child_process"); const childProcessSpawnChannel = tracingChannel("child_process.spawn"); const lazyDgram = core.createLazyLoader("node:dgram"); const lazyDgramInternal = () => core.loadExtScript("ext:deno_node/internal/dgram.ts"); const lazyUdpWrap = () => core.loadExtScript("ext:deno_node/internal_binding/udp_wrap.ts"); const { kNeedsNpmProcessState, nodeSpawnChild, nodeSpawnSyncChild, } = core.loadExtScript("ext:deno_process/40_process.js"); // Precompiled regular expressions (captured as SafeRegExp so the prototype // can't be tampered with at runtime). const cmdExeRe = new SafeRegExp("^(?:.*\\\\)?cmd(?:\\.exe)?$", "i"); const winSpecialCharsRe = new SafeRegExp('[\\s"\\\\&|<>^!()]'); const winEscapeQuotesRe = new SafeRegExp('(\\\\*)"', "g"); const winTrailingBackslashRe = new SafeRegExp("(\\\\+)$"); const posixSpecialCharsRe = new SafeRegExp("[^a-zA-Z0-9_./-]"); const singleQuoteRe = new SafeRegExp("'", "g"); const winQuoteCharsRe = new SafeRegExp('[\\s"&|<>^]'); const shellVarRefRe = new SafeRegExp("\\$\\{[^}]+\\}|\\$[A-Za-z_]"); const unsafeInDoubleQuotesRe = new SafeRegExp("`|\\$\\(|\\\\"); const posixMetacharsRe = new SafeRegExp("[();&|<>`!\\n\\r\\s\"'\\\\$]"); const doubleQuoteRe = new SafeRegExp('"', "g"); const pipePrefixRe = new SafeRegExp("^\\s*\\|\\s*"); const shellVarMatchRe = new SafeRegExp( '^(?:"\\$\\{([^}]+)\\}"|"\\$([A-Za-z_][A-Za-z0-9_]*)"|\\$\\{([^}]+)\\}|\\$([A-Za-z_][A-Za-z0-9_]*))', ); const shellCompoundOperatorRe = new SafeRegExp("^(.*?)\\s*(&&|\\|\\||;)\\s*"); function mapValues( record, transformer, ) { const ret = {}; const entries = ObjectEntries(record); for (const entry of new SafeArrayIterator(entries)) { const key = entry[0]; const value = entry[1]; if (typeof value === "undefined") { continue; } if (value === null) { continue; } const mappedValue = transformer(value); ret[key] = mappedValue; } return ret; } function stdioStringToArray( stdio, channel, ) { const options = []; switch (stdio) { case "ignore": case "overlapped": case "pipe": ArrayPrototypePush(options, stdio, stdio, stdio); break; case "inherit": ArrayPrototypePush(options, stdio, stdio, stdio); break; default: throw new ERR_INVALID_ARG_VALUE("stdio", stdio); } if (channel) ArrayPrototypePush(options, channel); return options; } const kClosesNeeded = Symbol("_closesNeeded"); const kClosesReceived = Symbol("_closesReceived"); const kCanDisconnect = Symbol("_canDisconnect"); const kChildStdioUsedAsInput = Symbol("childStdioUsedAsInput"); const childStdioStreamsByFd = new SafeMap(); let emittedShellDeprecation = false; // We only want to emit a close event for the child process when all of // the writable streams have closed. The value of `child[kClosesNeeded]` should be 1 + // the number of opened writable streams (note this excludes `stdin`). function maybeClose(child) { child[kClosesReceived]++; if (child[kClosesNeeded] === child[kClosesReceived]) { child.emit("close", child.exitCode, child.signalCode); } } function flushStdio(subprocess) { const stdio = subprocess.stdio; if (stdio == null) return; for (let i = 0; i < stdio.length; i++) { const stream = stdio[i]; if (!stream || !stream.readable) { continue; } if (stream[kChildStdioUsedAsInput]) { continue; } stream.resume(); } } class ChildProcess extends EventEmitter { /** * The exit code of the child process. This property will be `null` until the child process exits. */ exitCode = null; /** * This property is set to `true` after `kill()` is called. */ killed = false; /** * The PID of this child process. */ pid; /** * The signal received by this child process. */ signalCode = null; /** * Command line arguments given to this child process. */ spawnargs = []; /** * The executable file name of this child process. */ spawnfile = ""; /** * This property represents the child process's stdin. */ stdin = null; /** * This property represents the child process's stdout. */ stdout = null; /** * This property represents the child process's stderr. */ stderr = null; /** * Pipes to this child process. */ stdio = [ null, null, null, ]; disconnect; #process; #spawned = PromiseWithResolvers(); [kClosesNeeded] = 1; [kClosesReceived] = 0; [kCanDisconnect] = false; constructor() { super(); // 'child_process' channel fires once per ChildProcess construction, before // spawn(). cluster.fork() / cp.fork() / cp.spawn() all flow through here, // so a single publish site covers every entry point. if (childProcessChannel.hasSubscribers) { childProcessChannel.publish({ process: this }); } } /** * Internal spawn method used by Node.js internals. * This is called after creating a ChildProcess instance. */ spawn(options) { // Validate options if (options == null || typeof options !== "object") { throw new ERR_INVALID_ARG_TYPE("options", "object", options); } // Validate envPairs before file (Node.js validation order) const { envPairs } = options; if (envPairs !== undefined && !ArrayIsArray(envPairs)) { throw new ERR_INVALID_ARG_TYPE("options.envPairs", "Array", envPairs); } // Validate args const { args } = options; if (args !== undefined && !ArrayIsArray(args)) { throw new ERR_INVALID_ARG_TYPE("options.args", "Array", args); } // Validate file const { file } = options; if (file == null || typeof file !== "string") { throw new ERR_INVALID_ARG_TYPE("options.file", "string", file); } // 'child_process.spawn' tracingChannel: publish start before invoking the // platform spawn. #spawnInternal publishes end on success or error on // synchronous spawn failures (ENOENT/EACCES/EPERM) itself, because it // catches and handles the failure rather than re-throwing. if (childProcessSpawnChannel.hasSubscribers) { childProcessSpawnChannel.start.publish({ process: this, options }); } this.#spawnInternal(file, args || [], options); } /** * Internal method that performs the actual spawning. */ #spawnInternal( command, args, options, ) { const { stdio = ["pipe", "pipe", "pipe"], cwd, signal, windowsVerbatimArguments = false, windowsHide = true, detached, envPairs, uid, gid, } = options; // Convert envPairs array to env object const env = {}; if (envPairs) { for (const pair of new SafeArrayIterator(envPairs)) { const idx = StringPrototypeIndexOf(pair, "="); if (idx !== -1) { env[StringPrototypeSubstring(pair, 0, idx)] = StringPrototypeSubstring( pair, idx + 1, ); } } } const serialization = options.serialization || "json"; const normalizedStdio = normalizeStdioOption(stdio); const stdin = normalizedStdio[0] === undefined ? "pipe" : normalizedStdio[0]; const stdout = normalizedStdio[1] === undefined ? "pipe" : normalizedStdio[1]; const stderr = normalizedStdio[2] === undefined ? "pipe" : normalizedStdio[2]; const extraStdio = ArrayPrototypeSlice(normalizedStdio, 3); const { Stream } = lazyStream(); const { Socket } = lazyNet(); // buildCommand handles Node.js to Deno CLI arg translation when spawning Deno // args[0] is argv0 (prepended by normalizeSpawnArguments). Capture it // before slicing so we can pass it via kArgv0 for OS-level argv[0]. const argv0 = args.length > 0 ? args[0] : command; const builtCommand = buildCommand( command, ArrayPrototypeSlice(args, 1), env, ); const cmd = builtCommand[0]; const cmdArgs = builtCommand[1]; const includeNpmProcessState = builtCommand[2]; this.spawnfile = cmd; this.spawnargs = [cmd, ...new SafeArrayIterator(cmdArgs)]; const ipc = ArrayPrototypeIndexOf(normalizedStdio, "ipc"); if ( ipc !== -1 && ArrayPrototypeIndexOf(normalizedStdio, "ipc", ipc + 1) !== -1 ) { throw new ERR_IPC_ONE_PIPE(); } const extraStdioOffset = 3; // stdin, stdout, stderr const extraStdioNormalized = []; for (let i = 0; i < extraStdio.length; i++) { const fd = i + extraStdioOffset; if (fd === ipc) { // IPC fd is handled separately in Rust via the kIpc option. // Push a placeholder so the array indices stay aligned with // fd numbers, but don't double-push. ArrayPrototypePush(extraStdioNormalized, "null"); continue; } ArrayPrototypePush(extraStdioNormalized, toDenoStdio(extraStdio[i])); } // Windows does not support uid/gid options - throw ENOTSUP synchronously. if (isWindows && (uid != null || gid != null)) { throw _createSpawnError( "ENOTSUP", command, ArrayPrototypeSlice(args, 1), ); } try { this.#process = nodeSpawnChild(cmd, { args: cmdArgs, clearEnv: true, cwd, env, stdin: toDenoStdio(stdin), stdout: toDenoStdio(stdout), stderr: toDenoStdio(stderr), windowsRawArguments: windowsVerbatimArguments, windowsHide, uid, gid, detached, serialization, ipc, extraStdio: extraStdioNormalized, needsNpmProcessState: options[kNeedsNpmProcessState] || includeNpmProcessState, argv0: argv0 !== cmd ? argv0 : undefined, }); this.pid = this.#process.pid; if (stdin === "pipe" && this.#process.stdinFd != null) { const pipe = new Pipe(socketType.SOCKET); pipe.open(this.#process.stdinFd); this.stdin = new Socket({ handle: pipe, writable: true, readable: false, }); } if (ObjectPrototypeIsPrototypeOf(Stream.prototype, stdin)) { if (streamHandleFd(stdin) >= 0) { // The fd was passed directly to the child via dup(). The child // reads/writes the shared pipe at the OS level. The parent's // Stream is not assigned as this.stdin so the caller retains // independent control of the original stream. } else if (this.#process.stdinFd != null) { // Stream without a usable fd - we created a pipe. Wire up // JS-level piping from the user's readable stream into the // child's stdin pipe. const p = new Pipe(socketType.SOCKET); p.open(this.#process.stdinFd); const stdinSocket = new Socket({ handle: p, writable: true, readable: false, }); if (typeof stdin.pipe === "function") { stdin.pipe(stdinSocket); } this.stdin = stdinSocket; } } if (ObjectPrototypeIsPrototypeOf(Stream.prototype, stdout)) { if (streamHandleFd(stdout) >= 0) { // fd passed directly - see stdin comment above. } else if (this.#process.stdoutFd != null) { this[kClosesNeeded]++; const p = new Pipe(socketType.SOCKET); p.open(this.#process.stdoutFd); const stdoutSocket = new Socket({ handle: p, writable: false, readable: true, }); if (typeof stdout.write === "function") { // Don't end the target stream when this child exits -- other // children may also be piping into the same destination (e.g. // multiple children sharing p3.stdin as their stdout). stdoutSocket.pipe(stdout, { end: false }); } this.stdout = stdoutSocket; stdoutSocket.on("close", () => { maybeClose(this); }); } } if (ObjectPrototypeIsPrototypeOf(Stream.prototype, stderr)) { if (streamHandleFd(stderr) >= 0) { // fd passed directly - see stdin comment above. } else if (this.#process.stderrFd != null) { this[kClosesNeeded]++; const p = new Pipe(socketType.SOCKET); p.open(this.#process.stderrFd); const stderrSocket = new Socket({ handle: p, writable: false, readable: true, }); if (typeof stderr.write === "function") { stderrSocket.pipe(stderr, { end: false }); } this.stderr = stderrSocket; stderrSocket.on("close", () => { maybeClose(this); }); } } if (stdout === "pipe" && this.#process.stdoutFd != null) { this[kClosesNeeded]++; const pipe = new Pipe(socketType.SOCKET); pipe.open(this.#process.stdoutFd); this.stdout = new Socket({ handle: pipe, writable: false, readable: true, }); registerChildStdioStream(this.stdout); this.stdout.on("close", () => { maybeClose(this); }); } if (stderr === "pipe" && this.#process.stderrFd != null) { this[kClosesNeeded]++; const pipe = new Pipe(socketType.SOCKET); pipe.open(this.#process.stderrFd); this.stderr = new Socket({ handle: pipe, writable: false, readable: true, }); registerChildStdioStream(this.stderr); this.stderr.on("close", () => { maybeClose(this); }); } this.stdio[0] = this.stdin; this.stdio[1] = this.stdout; this.stdio[2] = this.stderr; if (ipc >= 0) { this.stdio[ipc] = null; } const extraPipeFds = this.#process.extraPipeFds; for (let i = 0; i < extraPipeFds.length; i++) { const extraFd = extraPipeFds[i]; const fd = i + extraStdioOffset; if (extraFd !== null && extraFd !== undefined) { this[kClosesNeeded]++; const pipe = new Pipe(socketType.SOCKET); pipe.open(extraFd); this.stdio[fd] = new Socket( { handle: pipe, }, ); registerChildStdioStream(this.stdio[fd]); this.stdio[fd]?.on("close", () => { maybeClose(this); }); } } nextTick(() => { this.emit("spawn"); this.#spawned.resolve(); }); if (signal) { const killSignal = options.killSignal ?? "SIGTERM"; const onAbortListener = () => { try { if (this.kill(killSignal)) { this.emit( "error", new AbortError( undefined, { cause: signal.reason }, ), ); } } catch (err) { this.emit("error", err); } }; if (signal.aborted) { nextTick(onAbortListener); } else { signal.addEventListener("abort", onAbortListener, { once: true }); this.addListener( "exit", () => signal.removeEventListener("abort", onAbortListener), ); } } const pipeRid = this.#process.ipcPipeRid; if (typeof pipeRid == "number") { setupChannel(this, pipeRid, serialization); this[kClosesNeeded]++; this.on("disconnect", () => { maybeClose(this); }); } (async () => { const status = await this.#process.status; this.signalCode = this.signalCode || status.signal || null; if (this.signalCode) { this.exitCode = null; } else { this.exitCode = status.code; } PromisePrototypeThen(this.#spawned.promise, async () => { // The 'exit' and 'close' events must be emitted after the 'spawn' event. this.emit("exit", this.exitCode, this.signalCode); await this.#_waitForChildStreamsToClose(); this.#closePipes(); maybeClose(this); nextTick(flushStdio, this); }); })(); // Synchronous spawn succeeded. Publish 'child_process.spawn'.end so the // tracing channel mirrors Node's "end XOR error" semantics. if (childProcessSpawnChannel.hasSubscribers) { childProcessSpawnChannel.end.publish({ process: this }); } } catch (err) { const { Readable, Writable } = lazyStream(); let e = err; if (ObjectPrototypeIsPrototypeOf(Deno.errors.NotFound.prototype, e)) { // args.slice(1) to exclude argv0 (prepended by normalizeSpawnArguments) e = _createSpawnError( "ENOENT", command, ArrayPrototypeSlice(args, 1), ); } else if ( ObjectPrototypeIsPrototypeOf( Deno.errors.PermissionDenied.prototype, e, ) ) { // Node distinguishes two failure modes that Deno collapses into // PermissionDenied: // * setuid/setgid refused by the kernel -> EPERM, thrown sync, // matching `process_handle_init` in libuv. // * execve() refused because the target isn't executable -> EACCES, // emitted asynchronously on the child's 'error' event (see Node's // test-diagnostics-channel-child-process EACCES branch). // Tell them apart by whether uid/gid were requested. if (uid != null || gid != null) { if (childProcessSpawnChannel.hasSubscribers) { const epermErr = _createSpawnError( "EPERM", command, ArrayPrototypeSlice(args, 1), ); childProcessSpawnChannel.error.publish({ process: this, error: epermErr, }); throw epermErr; } throw _createSpawnError( "EPERM", command, ArrayPrototypeSlice(args, 1), ); } e = _createSpawnError( "EACCES", command, ArrayPrototypeSlice(args, 1), ); } // Publish 'child_process.spawn'.error for the non-rethrow cases (ENOENT // and other Deno spawn failures that turn into an async 'error' event // on the child). Subscribers see the synthesized Node-style error. if (childProcessSpawnChannel.hasSubscribers) { childProcessSpawnChannel.error.publish({ process: this, error: e }); } // When spawn fails due to EMFILE/ENFILE, the OS couldn't create pipes // so stdio must remain undefined (matching Node.js behavior). const isResourceError = e && (e.code === "EMFILE" || e.code === "ENFILE"); if (isResourceError) { // deno-lint-ignore no-explicit-any (this as any).stdin = undefined; // deno-lint-ignore no-explicit-any (this as any).stdout = undefined; // deno-lint-ignore no-explicit-any (this as any).stderr = undefined; // deno-lint-ignore no-explicit-any (this as any).stdio = undefined; } else { // Set up stdio streams even when spawn fails (Node.js creates pipes // before the OS spawn call, so they exist regardless of spawn outcome). if (stdin === "pipe") { this.stdin = new Writable({ write(_chunk, _enc, cb) { cb(new Error("spawn failed")); }, }); } if (stdout === "pipe") { this.stdout = new Readable({ read() {} }); this[kClosesNeeded]++; this.stdout.on("close", () => { maybeClose(this); }); } if (stderr === "pipe") { this.stderr = new Readable({ read() {} }); this[kClosesNeeded]++; this.stderr.on("close", () => { maybeClose(this); }); } this.stdio[0] = this.stdin; this.stdio[1] = this.stdout; this.stdio[2] = this.stderr; } this.#_handleError(e); if (!isResourceError) { // Destroy stdio streams and emit close (matching Node.js behavior // where failed spawns still trigger 'close' but not 'exit'). nextTick(() => { if (this.stdout) { this.stdout.destroy(); } if (this.stderr) { this.stderr.destroy(); } if (this.stdin) { this.stdin.destroy(); } maybeClose(this); }); } } } /** * @param signal NOTE: this parameter is not yet implemented. */ kill(signal) { const process = lazyProcess().default; // Signal 0 is a special case: it checks if the process exists // without sending a signal (POSIX kill(pid, 0)). This must run // before the `killed` check because kill(0) is an existence probe // that should work even after a prior successful kill(). if (signal === 0 || signal === "0") { try { process.kill(this.pid, 0); return true; } catch { return false; } } if (this.killed) { return false; } let signalName = signal == null ? "SIGTERM" : toDenoSignal(signal); this.#closePipes(); try { this.#process.kill(signalName); } catch (err) { if (isWindows) { // On Windows, unsupported signals fall back to SIGKILL // (matching Node's TerminateProcess behavior). try { this.#process.kill("SIGKILL"); signalName = "SIGKILL"; } catch (err2) { const alreadyClosed = ObjectPrototypeIsPrototypeOf(TypeErrorPrototype, err2) || ObjectPrototypeIsPrototypeOf( Deno.errors.NotFound.prototype, err2, ) || ObjectPrototypeIsPrototypeOf( Deno.errors.PermissionDenied.prototype, err2, ); if (!alreadyClosed) { throw err2; } return false; } } else { const alreadyClosed = ObjectPrototypeIsPrototypeOf(TypeErrorPrototype, err) || ObjectPrototypeIsPrototypeOf( Deno.errors.NotFound.prototype, err, ) || ObjectPrototypeIsPrototypeOf( Deno.errors.PermissionDenied.prototype, err, ); if (!alreadyClosed) { throw err; } // Process already exited, signal was not delivered. return false; } } /* Cancel any pending IPC I/O */ if (this[kCanDisconnect]) { this.disconnect?.(); } this.killed = true; this.signalCode = signalName; return true; } [SymbolDispose]() { if (!this.killed) { this.kill(); } } ref() { this.#process.ref(); } unref() { this.#process.unref(); } async #_waitForChildStreamsToClose() { const { Readable } = lazyStream(); const process = lazyProcess().default; const promises = []; // Don't close parent process stdin if that's passed through if (this.stdin && !this.stdin.destroyed && this.stdin !== process.stdin) { assert(this.stdin); this.stdin.destroy(); ArrayPrototypePush(promises, waitForStreamToClose(this.stdin)); } // Only readable streams need to be closed if ( this.stdout && !this.stdout.destroyed && ObjectPrototypeIsPrototypeOf(Readable.prototype, this.stdout) ) { ArrayPrototypePush(promises, waitForReadableToClose(this.stdout)); } // Only readable streams need to be closed if ( this.stderr && !this.stderr.destroyed && ObjectPrototypeIsPrototypeOf(Readable.prototype, this.stderr) ) { ArrayPrototypePush(promises, waitForReadableToClose(this.stderr)); } await SafePromiseAll(promises); } #_handleError(err) { nextTick(() => { this.emit("error", err); // TODO(uki00a) Convert `err` into nodejs's `SystemError` class. }); } #closePipes() { if (this.stdin) { assert(this.stdin); this.stdin.destroy(); } } } function streamHandleFd(stream) { const handle = stream._handle; if (handle && typeof handle.fd === "number" && handle.fd >= 0) { return handle.fd; } return -1; } function registerChildStdioStream(stream) { const fd = streamHandleFd(stream); if (fd < 0) { return; } childStdioStreamsByFd.set(fd, stream); stream.on("close", () => { if (childStdioStreamsByFd.get(fd) === stream) { childStdioStreamsByFd.delete(fd); } }); } function markChildStdioUsedAsInput(fd) { const stream = childStdioStreamsByFd.get(fd); if (stream) { stream[kChildStdioUsedAsInput] = true; stream.pause(); stream._handle?.readStop?.(); } } function toDenoStdio( pipe, ) { const { Stream } = lazyStream(); if (ObjectPrototypeIsPrototypeOf(Stream.prototype, pipe)) { // If the stream has an underlying handle with a valid fd (e.g. a Socket // backed by a PipeWrap), pass that fd directly to the child process. // The Rust side will dup() it so both parent and child share the pipe. // This matches Node.js behavior where passing a child's stdout as // another child's stdin shares the underlying OS pipe. const fd = streamHandleFd(pipe); if (fd >= 0) { pipe[kChildStdioUsedAsInput] = true; pipe.pause(); pipe._handle?.readStop?.(); return fd; } // For streams without a usable fd, create a pipe and set up JS-level // piping after spawn. return "piped"; } if (typeof pipe === "number") { markChildStdioUsedAsInput(pipe); return pipe; } switch (pipe) { case "pipe": case "overlapped": case undefined: case null: return "piped"; case "ignore": return "null"; case "inherit": return "inherit"; case "ipc": return "ipc_for_internal_use"; default: throw new ERR_INVALID_ARG_VALUE("stdio", pipe); } } function toDenoSignal(signal) { const nodeSignals = os.signals; if (typeof signal === "number") { for (const name of new SafeArrayIterator(keys(nodeSignals))) { if (nodeSignals[name] === signal) { return name; } } throw new ERR_UNKNOWN_SIGNAL(String(signal)); } if (ReflectHas(nodeSignals, signal)) { return signal; } // On Windows, os.signals only lists native signals. Accept any // POSIX signal name so the caller can remap it to SIGTERM. if (isWindows && StringPrototypeStartsWith(signal, "SIG")) { return signal; } throw new ERR_UNKNOWN_SIGNAL(signal); } function keys(object) { return ObjectKeys(object); } function copyProcessEnvToEnv( env, name, optionEnv, ) { if ( Deno.env.get(name) && (!optionEnv || !ObjectHasOwn(optionEnv, name)) ) { env[name] = Deno.env.get(name); } } function normalizeStdioOption( stdio = [ "pipe", "pipe", "pipe", ], ) { if (ArrayIsArray(stdio)) { // `[0, 1, 2]` is equivalent to `"inherit"` if ( stdio.length === 3 && (stdio[0] === 0 && stdio[1] === 1 && stdio[2] === 2) ) { return ["inherit", "inherit", "inherit"]; } // `[null, null, null]` is equivalent to `"pipe" if ( stdio.length === 3 && (stdio[0] === null && stdio[1] === null && stdio[2] === null) ) { return ["pipe", "pipe", "pipe"]; } // At least 3 stdio must be created to match node while (stdio.length < 3) { ArrayPrototypePush(stdio, undefined); } return stdio; } else { switch (stdio) { case "overlapped": if (isWindows) { notImplemented("normalizeStdioOption overlapped (on windows)"); } // 'overlapped' is same as 'piped' on non Windows system. return ["pipe", "pipe", "pipe"]; case "pipe": return ["pipe", "pipe", "pipe"]; case "inherit": return ["inherit", "inherit", "inherit"]; case "ignore": return ["ignore", "ignore", "ignore"]; default: notImplemented( `normalizeStdioOption stdio=${typeof stdio} (${stdio})`, ); } } } // Valid stdio string values const validStdioStrings = ["ignore", "pipe", "inherit", "overlapped"]; /** * Validates and processes stdio configuration. * This is an internal function used by Node.js's child_process module. */ function getValidStdio( stdio, sync, ) { const { Stream } = lazyStream(); let ipc; let ipcFd; // If stdio is a string, validate it if (typeof stdio === "string") { if (!ArrayPrototypeIncludes(validStdioStrings, stdio)) { throw new ERR_INVALID_ARG_VALUE("stdio", stdio); } // Convert string to array stdio = [stdio, stdio, stdio]; } else if (!ArrayIsArray(stdio)) { throw new ERR_INVALID_ARG_VALUE("stdio", stdio); } // Expand stdio array to at least 3 elements (mutates the input array) while (stdio.length < 3) { ArrayPrototypePush(stdio, undefined); } // Process each stdio element const result = []; for (let i = 0; i < stdio.length; i++) { const value = stdio[i]; if (value === "ipc") { if (sync) { throw new ERR_IPC_SYNC_FORK(); } ipc = i; ipcFd = i; ArrayPrototypePush(result, { type: "ipc" }); } else if (value === "ignore" || value === null) { ArrayPrototypePush(result, { type: "ignore" }); } else if (value === "pipe" || value === undefined) { ArrayPrototypePush(result, { type: "pipe" }); } else if (value === "inherit") { ArrayPrototypePush(result, { type: "inherit" }); } else if (value === "overlapped") { ArrayPrototypePush(result, { type: "overlapped" }); } else if (typeof value === "number") { ArrayPrototypePush(result, { type: "fd", fd: value }); } else if (typeof value === "string") { // Invalid string value throw new ERR_INVALID_SYNC_FORK_INPUT(value); } else if (typeof value === "object" && value !== null) { // Check if it's a Stream with fd property (like process.stdin/stdout/stderr) if ( value.fd !== undefined && typeof value.fd === "number" ) { ArrayPrototypePush(result, { type: "fd", fd: value.fd }); } else if (ObjectPrototypeIsPrototypeOf(Stream.prototype, value)) { // Valid Stream object but without fd ArrayPrototypePush(result, { type: "pipe" }); } else { // Invalid object throw new ERR_INVALID_ARG_VALUE("stdio", value); } } else { throw new ERR_INVALID_ARG_VALUE("stdio", value); } } return { stdio: result, ipc, ipcFd, }; } // Check for null bytes in a string and throw ERR_INVALID_ARG_VALUE if found function validateNullByteNotInArg(value, name) { if (StringPrototypeIncludes(value, "\0")) { throw new ERR_INVALID_ARG_VALUE( name, value, "must be a string without null bytes", ); } } function normalizeSpawnArguments( file, args, options, ) { const process = lazyProcess().default; validateString(file, "file"); if (file.length === 0) { throw new ERR_INVALID_ARG_VALUE("file", file, "cannot be empty"); } // Check for null bytes in file validateNullByteNotInArg(file, "file"); if (ArrayIsArray(args)) { args = ArrayPrototypeSlice(args); } else if (args == null) { args = []; } else if (typeof args !== "object") { throw new ERR_INVALID_ARG_TYPE("args", "object", args); } else { options = args; args = []; } // Check for null bytes in args for (let i = 0; i < args.length; i++) { const arg = args[i]; if (typeof arg === "string") { validateNullByteNotInArg(arg, `args[${i}]`); } } if (options === undefined) { options = kEmptyObject; } else { validateObject(options, "options"); } options = { __proto__: null, ...options }; let cwd = options.cwd; // Validate the cwd, if present. if (cwd != null) { cwd = lazyFsUtils().getValidatedPath(cwd, "options.cwd"); validateNullByteNotInArg(cwd, "options.cwd"); } // Validate detached, if present. // deno-lint-ignore deno-internal/prefer-primordials if (options.detached != null) { // deno-lint-ignore deno-internal/prefer-primordials validateBoolean(options.detached, "options.detached"); } // Validate the uid, if present. if (options.uid != null) { validateInt32(options.uid, "options.uid"); } // Validate the gid, if present. if (options.gid != null) { validateInt32(options.gid, "options.gid"); } // Validate the shell, if present. if ( options.shell != null && typeof options.shell !== "boolean" && typeof options.shell !== "string" ) { throw new ERR_INVALID_ARG_TYPE( "options.shell", ["boolean", "string"], options.shell, ); } if (typeof options.shell === "string") { validateNullByteNotInArg(options.shell, "options.shell"); } // Validate argv0, if present. if (options.argv0 != null) { validateString(options.argv0, "options.argv0"); validateNullByteNotInArg(options.argv0, "options.argv0"); } // Validate windowsHide, if present. if (options.windowsHide != null) { validateBoolean(options.windowsHide, "options.windowsHide"); } // Validate windowsVerbatimArguments, if present. let { windowsVerbatimArguments } = options; if (windowsVerbatimArguments != null) { validateBoolean( windowsVerbatimArguments, "options.windowsVerbatimArguments", ); } validateOneOf(options.serialization, "options.serialization", [ undefined, "json", "advanced", ]); if (options.shell) { // When args are provided, escape them to prevent shell injection. // When no args are provided (just a string command), the user intends // for shell interpretation, so don't escape. let command; if (args.length > 0) { if (!emittedShellDeprecation) { process.emitWarning( "Passing args to a child process with shell option true can lead to security " + "vulnerabilities, as the arguments are not escaped, only concatenated.", "DeprecationWarning", "DEP0190", ); emittedShellDeprecation = true; } const escapedParts = [ escapeShellArg(stripShellArgQuotes(file)), ...new SafeArrayIterator(ArrayPrototypeMap( args, (arg) => escapeShellArg(stripShellArgQuotes(arg)), )), ]; command = ArrayPrototypeJoin(escapedParts, " "); } else { command = file; } // Set the shell, switches, and commands. // Note: transformDenoShellCommand is NOT called here because buildCommand() // already handles it for both `-c` (POSIX) and `/d /s /c` (cmd.exe) cases. // Calling it here would cause double transformation. if (process.platform === "win32") { if (typeof options.shell === "string") { file = options.shell; } else { file = Deno.env.get("comspec") || "cmd.exe"; } // '/d /s /c' is used only for cmd.exe. if (RegExpPrototypeExec(cmdExeRe, file) !== null) { args = ["/d", "/s", "/c", `"${command}"`]; windowsVerbatimArguments = true; } else { args = ["-c", command]; } } else { /** TODO: add Android condition */ if (typeof options.shell === "string") { file = options.shell; } else { file = "/bin/sh"; } args = ["-c", command]; } } if (typeof options.argv0 === "string") { ArrayPrototypeUnshift(args, options.argv0); } else { ArrayPrototypeUnshift(args, file); } const env = options.env || Deno.env.toObject(); const envPairs = []; // process.env.NODE_V8_COVERAGE always propagates, making it possible to // collect coverage for programs that spawn with white-listed environment. copyProcessEnvToEnv(env, "NODE_V8_COVERAGE", options.env); /** TODO: add `isZOS` condition */ let envKeys = []; // Prototype values are intentionally included. // deno-lint-ignore guard-for-in for (const key in env) { ArrayPrototypePush(envKeys, key); } if (process.platform === "win32") { // On Windows env keys are case insensitive. Filter out duplicates, // keeping only the first one (in lexicographic order) const sawKey = new SafeSet(); envKeys = ArrayPrototypeFilter( ArrayPrototypeSort(envKeys), (key) => { const uppercaseKey = StringPrototypeToUpperCase(key); if (sawKey.has(uppercaseKey)) { return false; } sawKey.add(uppercaseKey); return true; }, ); } for (const key of new SafeArrayIterator(envKeys)) { const value = env[key]; if (value !== undefined) { // Check for null bytes in env keys and values validateNullByteNotInArg(key, `options.env['${key}']`); validateNullByteNotInArg(String(value), `options.env['${key}']`); ArrayPrototypePush(envPairs, `${key}=${value}`); } } return { // Make a shallow copy so we don't clobber the user's options object. __proto__: null, ...options, args, cwd, // deno-lint-ignore deno-internal/prefer-primordials detached: !!options.detached, env, envPairs, file, windowsHide: options.windowsHide !== false, windowsVerbatimArguments: !!windowsVerbatimArguments, serialization: options.serialization || "json", }; } function waitForReadableToClose(readable) { if (readable[kChildStdioUsedAsInput]) { const closePromise = waitForStreamToClose(readable); readable.destroy(); return closePromise; } readable.resume(); // Ensure buffered data will be consumed. return waitForStreamToClose(readable); } function waitForStreamToClose(stream) { const deferred = PromiseWithResolvers(); const cleanup = () => { stream.removeListener("close", onClose); stream.removeListener("error", onError); }; const onClose = () => { cleanup(); deferred.resolve(); }; const onError = (err) => { cleanup(); deferred.reject(err); }; stream.once("close", onClose); stream.once("error", onError); return deferred.promise; } /** * Removes one layer of quotes that callers added around a complete shell * argument. The argument is escaped again before being passed to the shell. */ function stripShellArgQuotes(arg) { if (arg.length < 2) { return arg; } const doubleQuoted = StringPrototypeStartsWith(arg, '"') && StringPrototypeEndsWith(arg, '"'); // cmd.exe treats single quotes as ordinary characters, while POSIX shells // recognize both single and double quotes. const singleQuoted = lazyProcess().default.platform !== "win32" && StringPrototypeStartsWith(arg, "'") && StringPrototypeEndsWith(arg, "'"); if (doubleQuoted || singleQuoted) { return StringPrototypeSlice(arg, 1, -1); } return arg; } /** * Escapes a string for safe use as a shell argument. * On Unix, wraps in single quotes and escapes embedded single quotes. * On Windows, wraps in double quotes and escapes embedded double quotes and backslashes. */ function escapeShellArg(arg) { const process = lazyProcess().default; if (process.platform === "win32") { // Windows: use double quotes, escape double quotes and backslashes // Empty string needs to be quoted if (arg === "") { return '""'; } // If no special characters, return as-is // Must include cmd.exe metacharacters: &|<>^!() // Note: % is not included because cmd.exe expands %VAR% even inside // double quotes and there is no reliable escape for it outside batch files. if (!RegExpPrototypeTest(winSpecialCharsRe, arg)) { return arg; } // Escape backslashes before quotes, then escape quotes let escaped = StringPrototypeReplace(arg, winEscapeQuotesRe, '$1$1\\"'); // Escape trailing backslashes escaped = StringPrototypeReplace(escaped, winTrailingBackslashRe, "$1$1"); return `"${escaped}"`; } else { // Unix: use single quotes, escape embedded single quotes // Empty string needs to be quoted if (arg === "") { return "''"; } // If no special characters, return as-is if (!RegExpPrototypeTest(posixSpecialCharsRe, arg)) { return arg; } // Wrap in single quotes and escape any embedded single quotes // Single quotes are escaped by ending the string, adding an escaped quote, and starting a new string return "'" + StringPrototypeReplace(arg, singleQuoteRe, "'\\''") + "'"; } } /** * Transforms a shell command that invokes Deno with Node.js flags into Deno-compatible flags. * Uses the Rust CLI parser (op_node_translate_cli_args) to handle argument translation, * including subcommand detection, -c/--check flag handling, and adding "run -A". */ function transformDenoShellCommand( command, env, isCmdExe = false, ) { const denoPath = Deno.execPath(); // Check if the command starts with the Deno executable (possibly quoted) const quotedDenoPath = `"${denoPath}"`; const singleQuotedDenoPath = `'${denoPath}'`; let startsWithDeno = false; let denoPathLength = 0; let shellVarPrefix = ""; if (StringPrototypeStartsWith(command, quotedDenoPath)) { startsWithDeno = true; denoPathLength = quotedDenoPath.length; } else if (StringPrototypeStartsWith(command, singleQuotedDenoPath)) { startsWithDeno = true; denoPathLength = singleQuotedDenoPath.length; } else if (StringPrototypeStartsWith(command, denoPath)) { startsWithDeno = true; denoPathLength = denoPath.length; } else if (env) { // Check for shell variable that references the Deno path // Pattern: "${VARNAME}", "$VARNAME", ${VARNAME}, or $VARNAME at start of command const shellVarMatch = StringPrototypeMatch(command, shellVarMatchRe); if (shellVarMatch) { const varName = shellVarMatch[1] || shellVarMatch[2] || shellVarMatch[3] || shellVarMatch[4]; const varValue = env[varName]; if (varValue !== undefined && String(varValue) === denoPath) { startsWithDeno = true; shellVarPrefix = shellVarMatch[0]; denoPathLength = shellVarMatch[0].length; } } } if (!startsWithDeno) { // The command doesn't start with deno, but it may contain a deno // invocation after a shell compound operator (&&, ||, ;). // Split on the first operator and try to transform the remainder. // NOTE: This regex doesn't handle quoted strings, so an operator // inside quotes (e.g. `echo "foo && bar"`) would be matched. // This is safe because if no real `deno` invocation follows, the // `transformedRest !== rest` guard returns the original command. const operatorMatch = StringPrototypeMatch( command, shellCompoundOperatorRe, ); if (operatorMatch) { const prefix = operatorMatch[1]; const operator = operatorMatch[2]; const rest = StringPrototypeSlice(command, operatorMatch[0].length); const transformedRest = transformDenoShellCommand(rest, env, isCmdExe); if (transformedRest !== rest) { return prefix + " " + operator + " " + transformedRest; } } return command; } // Extract the rest of the command after the Deno path const rest = StringPrototypeTrimStart( StringPrototypeSlice(command, denoPathLength), ); if (rest.length === 0) { return command; } // Parse the command using the shell parser to separate arguments from // shell operators (redirections, pipes, etc.). const { args, shell_suffix: shellSuffix } = op_node_parse_shell_args( rest, isCmdExe, ); try { const result = op_node_translate_cli_args(args, false, false); // Shell-quote translated args that contain metacharacters so they are // safe to embed in a shell command string. const quotedArgs = isWindows ? ArrayPrototypeMap(result.denoArgs, (a) => { // Windows cmd.exe: use double quotes for args with spaces or // special chars. Backslash is a path separator, not an escape. if (RegExpPrototypeTest(winQuoteCharsRe, a)) { let escaped = StringPrototypeReplace( a, winEscapeQuotesRe, '$1$1\\"', ); escaped = StringPrototypeReplace( escaped, winTrailingBackslashRe, "$1$1", ); return `"${escaped}"`; } return a; }) : ArrayPrototypeMap(result.denoArgs, (a) => { // POSIX shell quoting for translated args. const hasShellVarRef = RegExpPrototypeTest(shellVarRefRe, a); const unsafeInDoubleQuotes = RegExpPrototypeTest( unsafeInDoubleQuotesRe, a, ); const hasShellMetachars = RegExpPrototypeTest(posixMetacharsRe, a); if (hasShellMetachars) { if (hasShellVarRef && !unsafeInDoubleQuotes) { return '"' + StringPrototypeReplace(a, doubleQuoteRe, '\\"') + '"'; } return "'" + StringPrototypeReplace(a, singleQuoteRe, "'\\''") + "'"; } return a; }); const prefix = shellVarPrefix || StringPrototypeSlice(command, 0, denoPathLength); let transformed = prefix + " " + ArrayPrototypeJoin(quotedArgs, " "); if (shellSuffix) { transformed += " " + shellSuffix; } // If the shell suffix starts with a pipe, the command after the pipe // may also be a Deno invocation that needs transformation. if (env) { const pipeMatch = StringPrototypeMatch(shellSuffix, pipePrefixRe); if (pipeMatch) { const afterPipe = StringPrototypeSlice( shellSuffix, pipeMatch[0].length, ); const transformedAfter = transformDenoShellCommand( afterPipe, env, isCmdExe, ); if (transformedAfter !== afterPipe) { transformed = prefix + " " + ArrayPrototypeJoin(quotedArgs, " ") + " | " + transformedAfter; } } } return transformed; } catch { // If the Rust parser fails (unknown flags), return the original command return command; } } /** * Find the first non-flag argument in a list of command line arguments. * This is used to determine if the user is spawning a Deno subcommand * or a script, and to check if the script is in an npm package. */ function findFirstNonFlagArg(args) { for (let i = 0; i < args.length; i++) { const arg = args[i]; // Stop at '--' - everything after is positional if (arg === "--") { return i + 1 < args.length ? args[i + 1] : null; } // If it doesn't start with '-', it's a positional argument if (!StringPrototypeStartsWith(arg, "-")) { return arg; } // Skip known flags that take a value if ( arg === "-e" || arg === "--eval" || arg === "-p" || arg === "--print" || arg === "-r" || arg === "--require" || arg === "-C" || arg === "--conditions" || arg === "-c" || arg === "--check" || arg === "--import" || arg === "--loader" || arg === "--experimental-loader" ) { i++; // Skip the next arg (the value) } } return null; } /** * This function is based on https://github.com/nodejs/node/blob/fc6426ccc4b4cb73076356fb6dbf46a28953af01/lib/child_process.js#L504-L528. * Copyright Joyent, Inc. and other Node contributors. All rights reserved. MIT license. */ function buildCommand( file, args, env, ) { let includeNpmProcessState = false; if (file === Deno.execPath() && !Deno.build.standalone) { // Ensure all args are strings (Node allows numbers in args array) args = ArrayPrototypeMap(args, (arg) => String(arg)); // Find script path to check if it's in an npm package const firstNonFlagArg = findFirstNonFlagArg(args); const scriptInNpmPackage = firstNonFlagArg !== null ? op_node_in_npm_package(firstNonFlagArg) : false; // Use the Rust parser to translate Node.js args to Deno args // The parser handles Deno-style args (e.g., "run -A script.js") by passing them through unchanged if (ArrayPrototypeIncludes(args, "--use-env-proxy")) { env.NODE_USE_ENV_PROXY = "1"; } else if (ArrayPrototypeIncludes(args, "--no-use-env-proxy")) { env.NODE_USE_ENV_PROXY = "0"; } const result = op_node_translate_cli_args(args, scriptInNpmPackage, true); args = result.denoArgs; includeNpmProcessState = result.needsNpmProcessState; if (result.caStores?.length) { env.DENO_TLS_CA_STORE = ArrayPrototypeJoin(result.caStores, ","); } if (result.useOpensslCa) { env.DENO_NODE_USE_OPENSSL_CA = "1"; } else { delete env.DENO_NODE_USE_OPENSSL_CA; } if (result.traceEventCategories) { env.DENO_NODE_TRACE_EVENT_CATEGORIES = result.traceEventCategories; } // Update NODE_OPTIONS if needed if (result.nodeOptions.length > 0) { const options = ArrayPrototypeJoin(result.nodeOptions, " "); if (env.NODE_OPTIONS) { env.NODE_OPTIONS += " " + options; } else { env.NODE_OPTIONS = options; } } } // When spawning a shell with `-c` (e.g. spawn('/bin/sh', ['-c', cmd])), // transform any Deno commands inside the shell command string so that // Node.js flags are translated and `-A` is added. if ( file !== Deno.execPath() && args.length >= 2 && args[0] === "-c" ) { const transformed = transformDenoShellCommand(args[1], env, false); if (transformed !== args[1]) { args = ArrayPrototypeSlice(args); args[1] = transformed; } } // Windows cmd.exe: args are ["/d", "/s", "/c", '"command"'] if ( file !== Deno.execPath() && args.length >= 4 && args[0] === "/d" && args[1] === "/s" && args[2] === "/c" ) { let cmdStr = args[3]; // Remove wrapping quotes added by normalizeSpawnArguments const hasWrappingQuotes = StringPrototypeStartsWith(cmdStr, '"') && StringPrototypeEndsWith(cmdStr, '"'); if (hasWrappingQuotes) { cmdStr = StringPrototypeSlice(cmdStr, 1, -1); } const transformed = transformDenoShellCommand(cmdStr, env, true); if (transformed !== cmdStr) { args = ArrayPrototypeSlice(args); args[3] = hasWrappingQuotes ? `"${transformed}"` : transformed; } } // When spawning a non-deno process that has Deno.execPath() in its args // (e.g. Python given deno's path to re-invoke it), pre-translate the args // that follow so the child deno process gets the correct Node.js compat // flags (-A, --unstable-bare-node-builtins, etc.). // This covers cases like test-stdio-closed.js on Windows, where a Python // intermediary closes stdio then calls deno directly via subprocess.call. if (file !== Deno.execPath()) { const denoPath = Deno.execPath(); const denoArgIndex = ArrayPrototypeFindIndex( args, (arg) => arg === denoPath, ); if (denoArgIndex !== -1) { const argsForDeno = ArrayPrototypeSlice(args, denoArgIndex + 1); if (argsForDeno.length > 0) { try { const result = op_node_translate_cli_args(argsForDeno, false, true); args = [ ...new SafeArrayIterator( ArrayPrototypeSlice(args, 0, denoArgIndex + 1), ), ...new SafeArrayIterator(result.denoArgs), ]; } catch { // If translation fails (unknown flags), leave args unchanged } } } } return [file, args, includeNpmProcessState]; } function restorePrototype(obj) { if (obj === null || typeof obj !== "object") return; if (ArrayIsArray(obj)) { for (let i = 0; i < obj.length; i++) { restorePrototype(obj[i]); } return; } ObjectSetPrototypeOf(obj, ObjectPrototype); for (const key of new SafeArrayIterator(ObjectKeys(obj))) { restorePrototype(obj[key]); } } /** Convert a killSignal (string name or number) to its string name. */ function _resolveKillSignalName( killSignal, ) { if (typeof killSignal === "string") return killSignal; if (typeof killSignal === "number") { for (const entry of new SafeArrayIterator(ObjectEntries(os.signals))) { const name = entry[0]; const num = entry[1]; if (num === killSignal) return name; } return String(killSignal); } return "SIGTERM"; } function _createSpawnError( status, command, args = [], sync = false, ) { const syscall = sync ? "spawnSync " : "spawn "; const error = errnoException( codeMap.get(status), syscall + command, ); error.path = command; error.spawnargs = args; return error; } function normalizeInput(input) { if (input == null) { return null; } if (typeof input === "string") { return Buffer.from(input); } if (ArrayBufferIsView(input)) { if (isTypedArray(input)) { return Buffer.from( TypedArrayPrototypeGetBuffer(input), TypedArrayPrototypeGetByteOffset(input), TypedArrayPrototypeGetByteLength(input), ); } // ArrayBuffer.isView() is true only for TypedArrays and DataViews, so the // DataView branch is the only remaining possibility here. return Buffer.from( DataViewPrototypeGetBuffer(input), DataViewPrototypeGetByteOffset(input), DataViewPrototypeGetByteLength(input), ); } throw new ERR_INVALID_ARG_TYPE("input", [ "string", "Buffer", "TypedArray", "DataView", ], input); } function spawnSync( options, ) { const { env = Deno.env.toObject(), input, stdio = ["pipe", "pipe", "pipe"], cwd, encoding, uid, gid, maxBuffer, timeout, killSignal, windowsVerbatimArguments = false, windowsHide = true, } = options; let command = options.file || ""; let args = options.args || []; const normalizedStdio = normalizeStdioOption(stdio); const stdin_ = normalizedStdio[0] === undefined ? "pipe" : normalizedStdio[0]; const stdout_ = normalizedStdio[1] === undefined ? "pipe" : normalizedStdio[1]; const stderr_ = normalizedStdio[2] === undefined ? "pipe" : normalizedStdio[2]; const extraStdio_ = ArrayPrototypeSlice(normalizedStdio, 3); const extraStdioNormalized = []; for (let i = 0; i < extraStdio_.length; i++) { const val = extraStdio_[i]; const fd = i + 3; // extra stdio starts at FD 3 // null/undefined means "don't pass this fd" if (val == null) { ArrayPrototypePush(extraStdioNormalized, "null"); } else if (val === "inherit") { // "inherit" for extra FDs means pass the parent's FD at this index ArrayPrototypePush(extraStdioNormalized, fd); } else { ArrayPrototypePush(extraStdioNormalized, toDenoStdio(val)); } } let includeNpmProcessState = false; // args[0] is argv0 (prepended by normalizeSpawnArguments). Capture it // before slicing so we can pass it via kArgv0 for OS-level argv[0]. const argv0 = args && args.length > 0 ? args[0] : command; const argsToProcess = args && args.length > 0 ? ArrayPrototypeSlice(args, 1) : []; const builtCommand = buildCommand( command, argsToProcess, env, ); command = builtCommand[0]; args = builtCommand[1]; includeNpmProcessState = builtCommand[2]; const input_ = normalizeInput(input); const result = {}; try { const output = nodeSpawnSyncChild({ args: [command, ...new SafeArrayIterator(args)], cwd, // deno-lint-ignore deno-internal/prefer-primordials env: mapValues(env, (value) => value.toString()), argv0: argv0 !== command ? argv0 : undefined, stdout: toDenoStdio(stdout_), stderr: toDenoStdio(stderr_), stdin: stdin_ == "inherit" ? "inherit" : "null", uid, gid, clearEnv: false, extraStdio: extraStdioNormalized, windowsRawArguments: windowsVerbatimArguments, windowsHide, needsNpmProcessState: options[kNeedsNpmProcessState] || includeNpmProcessState, input: input_, timeout, killSignal, maxBuffer, }); const status = output.signal ? null : output.code; let stdout = output.stdout ? Buffer.from(output.stdout) : null; let stderr = output.stderr ? Buffer.from(output.stderr) : null; // Defensive: if Rust didn't kill on overflow (e.g. maxBuffer was // unlimited but the JS layer somehow still has a longer buffer), fall // back to the post-hoc length check. if ( output.killedByMaxBuffer || (stdout && stdout.length > maxBuffer) || (stderr && stderr.length > maxBuffer) ) { result.error = _createSpawnError("ENOBUFS", command, args, true); } if (output.killedByTimeout) { result.error = _createSpawnError("ETIMEDOUT", command, args, true); } if (encoding && encoding !== "buffer") { // deno-lint-ignore deno-internal/prefer-primordials stdout = stdout && stdout.toString(encoding); // deno-lint-ignore deno-internal/prefer-primordials stderr = stderr && stderr.toString(encoding); } result.pid = output.pid; // When killed by timeout or maxBuffer, report the killSignal (matching // Node.js behavior). On Windows there are no real Unix signals, but // Node still reports the configured killSignal so callers can detect // why the child was terminated. const killedByDeno = output.killedByTimeout || output.killedByMaxBuffer; result.status = killedByDeno ? null : status; result.signal = killedByDeno ? _resolveKillSignalName(killSignal) : output.signal; result.stdout = stdout; result.stderr = stderr; result.output = [output.signal, stdout, stderr]; } catch (err) { if (ObjectPrototypeIsPrototypeOf(Deno.errors.NotFound.prototype, err)) { result.error = _createSpawnError("ENOENT", command, args, true); } } return result; } const kControlDisconnect = Symbol("kControlDisconnect"); const kPendingMessages = Symbol("kPendingMessages"); // controls refcounting for the IPC channel class Control extends EventEmitter { #channel; #refs = 0; #refExplicitlySet = false; #connected = true; [kPendingMessages] = []; #serialization; constructor(channel, serialization) { super(); this.#channel = channel; this.#serialization = serialization; } #ref() { if (this.#connected) { op_node_ipc_ref(this.#channel, this.#serialization === "json"); } } #unref() { if (this.#connected) { op_node_ipc_unref(this.#channel, this.#serialization === "json"); } } [kControlDisconnect]() { this.#unref(); this.#connected = false; } refCounted() { if (++this.#refs === 1 && !this.#refExplicitlySet) { this.#ref(); } } unrefCounted() { if (--this.#refs === 0 && !this.#refExplicitlySet) { this.#unref(); this.emit("unref"); } } ref() { this.#refExplicitlySet = true; this.#ref(); } unref() { this.#refExplicitlySet = false; this.#unref(); } } function isInternal(msg) { if (msg && typeof msg === "object") { const cmd = msg["cmd"]; if (typeof cmd === "string") { return StringPrototypeStartsWith(cmd, "NODE_"); } } return false; } function internalCmdName(msg) { return StringPrototypeSlice(msg.cmd, 5); } let hasSetBufferConstructor = false; const IPC_HANDLE_NET_SOCKET = "net.Socket"; const IPC_HANDLE_NET_SERVER = "net.Server"; // node:cluster sends raw TCP/Pipe wraps (without a Socket/Server wrapper) for // connection handoffs (RoundRobinHandle) and shared listening sockets // (SharedHandle). Mirrors Node's `handleConversion["net.Native"]`. const IPC_HANDLE_NET_NATIVE = "net.Native"; const IPC_HANDLE_DGRAM_SOCKET = "dgram.Socket"; const NODE_SOCKET_GET_COUNT = "NODE_SOCKET_GET_COUNT"; const NODE_SOCKET_COUNT = "NODE_SOCKET_COUNT"; const NODE_SOCKET_NOTIFY_CLOSE = "NODE_SOCKET_NOTIFY_CLOSE"; const NODE_SOCKET_CLOSE_ACK = "NODE_SOCKET_CLOSE_ACK"; let nextSocketListKey = 0; const socketListsByChild = new SafeWeakMap(); class SocketListSend extends EventEmitter { constructor(child, server) { super(); this.child = child; this.key = `socket-list:${++nextSocketListKey}`; this.callbacks = new SafeMap(); this.closeCallbacks = new SafeMap(); this.seq = 0; this.closed = false; this.onInternalMessage = (message) => { if (!message || message.key !== this.key) { return; } if (message.cmd === NODE_SOCKET_COUNT) { const cb = this.callbacks.get(message.id); if (cb) { this.callbacks.delete(message.id); cb(null, message.count); } } else if (message.cmd === NODE_SOCKET_CLOSE_ACK) { const cb = this.closeCallbacks.get(message.id); if (cb) { this.closeCallbacks.delete(message.id); cb(); } } }; child.on("internalMessage", this.onInternalMessage); // Flush on both `exit` and `disconnect`: the IPC channel can close while // the child process keeps running, which leaves in-flight requests with no // peer to answer. Either event means no further replies will arrive, so we // resolve pending getConnections()/close() callers rather than hang. this.onClose = () => this._flush(); child.once("exit", this.onClose); child.once("disconnect", this.onClose); server._setupWorker(this); } _flush() { if (this.closed) { return; } this.closed = true; this.child.removeListener("internalMessage", this.onInternalMessage); this.child.removeListener("exit", this.onClose); this.child.removeListener("disconnect", this.onClose); for (const { 1: cb } of new SafeMapIterator(this.callbacks)) { cb(null, 0); } this.callbacks.clear(); for (const { 1: cb } of new SafeMapIterator(this.closeCallbacks)) { cb(); } this.closeCallbacks.clear(); this.emit("exit", this); } getConnections(cb) { if (this.closed || !this.child.connected) { nextTick(cb, null, 0); return; } const id = ++this.seq; this.callbacks.set(id, cb); this.child.send( { cmd: NODE_SOCKET_GET_COUNT, key: this.key, id }, (err) => { if (err && this.callbacks.delete(id)) { cb(err); } }, ); } close(cb) { if (this.closed || !this.child.connected) { nextTick(cb); return; } const id = ++this.seq; this.closeCallbacks.set(id, cb); this.child.send( { cmd: NODE_SOCKET_NOTIFY_CLOSE, key: this.key, id }, (err) => { if (err && this.closeCallbacks.delete(id)) { cb(); } }, ); } } class SocketListReceive { constructor() { this.sockets = new SafeSet(); this.closeCallbacks = []; } add(socket) { this.sockets.add(socket); socket.once("close", () => { this.sockets.delete(socket); this._maybeClose(); }); } getConnections() { return this.sockets.size; } close(cb) { ArrayPrototypePush(this.closeCallbacks, cb); this._maybeClose(); } _maybeClose() { if (this.sockets.size !== 0) { return; } const callbacks = this.closeCallbacks; this.closeCallbacks = []; for (const cb of new SafeArrayIterator(callbacks)) { nextTick(cb); } } } function getSocketListSend(child, server) { let socketLists = socketListsByChild.get(child); if (!socketLists) { socketLists = new SafeWeakMap(); socketListsByChild.set(child, socketLists); } let socketList = socketLists.get(server); if (!socketList) { socketList = new SocketListSend(child, server); socketLists.set(server, socketList); } return socketList; } function rawFdFromTcpHandle(tcpHandle) { if (typeof tcpHandle.fdForIpc !== "function") { notImplemented("ChildProcess.send with handle on this platform"); } const rawFd = tcpHandle.fdForIpc(); if (rawFd < 0) { throw new ERR_INVALID_HANDLE_TYPE(); } return rawFd; } function getIpcHandleInfo(handle, options, target) { const { Socket } = lazyNet(); const { Server: NetServer } = lazyNet(); const { Socket: DgramSocket } = lazyDgram(); if (ObjectPrototypeIsPrototypeOf(Socket.prototype, handle)) { const inner = handle._handle; // Match Node's handleConversion["net.Socket"].send, which returns the // socket's native handle. A socket without an underlying handle (e.g. // already destroyed) yields null; Node then strips the handle and sends // the message alone instead of throwing. if (!inner) { return null; } const isTcp = ObjectPrototypeIsPrototypeOf(TCP.prototype, inner); const isPipe = ObjectPrototypeIsPrototypeOf(Pipe.prototype, inner); if (!isTcp && !isPipe) { notImplemented("ChildProcess.send with non-TCP net.Socket handle"); } const closeAfterSend = options.keepOpen !== true; const message = { cmd: "NODE_HANDLE", type: IPC_HANDLE_NET_SOCKET, // Distinguishes the wrap type the receiver should reconstruct. nativeKind: isTcp ? "tcp" : "pipe", msg: undefined, }; if ( closeAfterSend && handle.server && typeof handle.server._setupWorker === "function" ) { message.socketListKey = getSocketListSend(target, handle.server).key; } return { rawFd: rawFdFromTcpHandle(inner), message, closeAfterSend, close() { handle.parser = null; handle._httpMessage = null; handle.destroy(); }, }; } if (ObjectPrototypeIsPrototypeOf(NetServer.prototype, handle)) { const inner = handle._handle; // Match Node's handleConversion["net.Server"].send, which returns // server._handle. A server that hasn't started listening (or was // already closed) has a null handle; Node then strips the handle and // sends the message alone instead of throwing. if (!inner) { return null; } const isTcp = ObjectPrototypeIsPrototypeOf(TCP.prototype, inner); const isPipe = ObjectPrototypeIsPrototypeOf(Pipe.prototype, inner); if (!isTcp && !isPipe) { notImplemented("ChildProcess.send with non-TCP net.Server handle"); } return { rawFd: rawFdFromTcpHandle(inner), message: { cmd: "NODE_HANDLE", type: IPC_HANDLE_NET_SERVER, // Distinguishes the wrap type the receiver should reconstruct. nativeKind: isTcp ? "tcp" : "pipe", msg: undefined, }, // Match Node's handleConversion["net.Server"].postSend, which calls // server.close() once the receiver acknowledges the transfer. closeAfterSend: options.keepOpen !== true, close() { handle.close(); }, }; } if ( ObjectPrototypeIsPrototypeOf(TCP.prototype, handle) || ObjectPrototypeIsPrototypeOf(Pipe.prototype, handle) ) { return { rawFd: rawFdFromTcpHandle(handle), message: { cmd: "NODE_HANDLE", type: IPC_HANDLE_NET_NATIVE, // 0 = SOCKET, 1 = SERVER. Same encoding for TCP and Pipe. socketType: handle.socketTypeForIpc(), // Distinguishes the wrap type the receiver should reconstruct. nativeKind: ObjectPrototypeIsPrototypeOf(TCP.prototype, handle) ? "tcp" : "pipe", msg: undefined, }, // Match Node's handleConversion["net.Native"]: it has no postSend hook, // so the IPC layer doesn't auto-close. Cluster's RoundRobinHandle and // SharedHandle each manage their own lifecycle (the former closes the // client wrap after the worker ACKs; the latter shares the listening // wrap across workers). closeAfterSend: false, close() { handle.close(); }, }; } if (ObjectPrototypeIsPrototypeOf(DgramSocket.prototype, handle)) { const udpHandle = handle[lazyDgramInternal().kStateSymbol]?.handle; if (!udpHandle || typeof udpHandle.fdForIpc !== "function") { throw new ERR_INVALID_HANDLE_TYPE(); } const rawFd = udpHandle.fdForIpc(); if (rawFd < 0) { throw new ERR_INVALID_HANDLE_TYPE(); } return { rawFd, message: { cmd: "NODE_HANDLE", type: IPC_HANDLE_DGRAM_SOCKET, dgramType: handle.type, msg: undefined, }, // Node's handleConversion["dgram.Socket"].postSend is undefined, so // the IPC layer doesn't auto-close. The socket remains usable in the // sender (both parent and child share the underlying UDP socket). closeAfterSend: false, close() { handle.close(); }, }; } throw new ERR_INVALID_HANDLE_TYPE(); } function createIpcHandle(message, rawFd) { const { Socket } = lazyNet(); const { Server: NetServer } = lazyNet(); const { Socket: DgramSocket } = lazyDgram(); if (message.type === IPC_HANDLE_NET_SOCKET) { const inner = message.nativeKind === "pipe" ? new Pipe(socketType.SOCKET) : new TCP(tcpSocketType.SOCKET); const err = inner.open(rawFd); if (err !== 0) { throw errnoException(codeMap.get(err), "open"); } try { return new Socket({ handle: inner, readable: true, writable: true, }); } catch (err) { inner.close(); throw err; } } if (message.type === IPC_HANDLE_NET_SERVER) { const inner = message.nativeKind === "pipe" ? new Pipe(socketType.SERVER) : new TCP(tcpSocketType.SERVER); const err = inner.open(rawFd); if (err !== 0) { throw errnoException(codeMap.get(err), "open"); } // Match Node's handleConversion["net.Server"].got: hand the wrapped // handle to a fresh net.Server via listen(handle), which registers the // connection callback and emits 'listening' on next tick. Our uv_listen // detects an already-listening fd and skips the bind/listen syscalls. const server = new NetServer(); try { server.listen(inner); return server; } catch (err) { inner.close(); throw err; } } if (message.type === IPC_HANDLE_NET_NATIVE) { if (message.nativeKind === "pipe") { const st = message.socketType === 1 ? socketType.SERVER : socketType.SOCKET; const pipe = new Pipe(st); const err = pipe.open(rawFd); if (err !== 0) { throw errnoException(codeMap.get(err), "open"); } return pipe; } const st = message.socketType === 1 ? tcpSocketType.SERVER : tcpSocketType.SOCKET; const tcp = new TCP(st); const err = tcp.open(rawFd); if (err !== 0) { throw errnoException(codeMap.get(err), "open"); } // Match Node's handleConversion["net.Native"].got: just hand the raw // wrap to the listener. Cluster's worker-side onconnection() / shared() // takes ownership. return tcp; } if (message.type === IPC_HANDLE_DGRAM_SOCKET) { const udp = new (lazyUdpWrap().UDP)(); const err = udp.open(rawFd); if (err !== 0) { throw errnoException(codeMap.get(err), "open"); } // Reconstruct a dgram.Socket from the transferred handle, mirroring // Node's handleConversion["dgram.Socket"].got which calls // socket.bind(handle). The `bind(udpHandle)` path in dgram.ts calls // replaceHandle + startListening, making the socket immediately usable. const socket = new DgramSocket(message.dgramType); // deno-lint-ignore deno-internal/prefer-primordials socket.bind(udp); return socket; } return undefined; } function setupChannel( target, ipc, serialization, ) { const { Socket } = lazyNet(); const { Server: NetServer } = lazyNet(); const { Socket: DgramSocket } = lazyDgram(); const control = new Control(ipc, serialization); target.channel = control; if (!hasSetBufferConstructor) { op_node_ipc_buffer_constructor(Buffer, FastBuffer.prototype); hasSetBufferConstructor = true; } const writeFn = serialization === "json" ? op_node_ipc_write_json : op_node_ipc_write_advanced; const readFn = serialization === "json" ? op_node_ipc_read_json : op_node_ipc_read_advanced; // Sentinel passed to the unified write op when no handle accompanies the // message. Mirrors the Rust-side `NO_RAW_FD` constant. const NO_RAW_FD = -1; // Handle-passing ACK state. Matches Node's // NODE_HANDLE / NODE_HANDLE_ACK protocol in lib/internal/child_process.js: // while a handle send is in flight, the sender keeps its copy of the fd // open. Closing it earlier would drop the OFD refcount to 0 before the // receiver has materialized its dup, and the kernel would send FIN/RST // to the peer. // // `pendingHandleInfo` is the handle waiting for an ACK to close. // `handleQueue` is non-null while we're waiting on an ACK; further // sends (handle or plain message) are queued on it to preserve ordering. let pendingHandleInfo = null; let handleQueue = null; const receivedSocketLists = new SafeMap(); function sendHandleAck() { const queueOk = [true]; control.refCounted(); PromisePrototypeThen( writeFn(ipc, { cmd: "NODE_HANDLE_ACK" }, NO_RAW_FD, queueOk), () => control.unrefCounted(), () => control.unrefCounted(), ); } function onHandleAck() { const info = pendingHandleInfo; pendingHandleInfo = null; if (info && info.closeAfterSend) { info.close(); } const queue = handleQueue; handleQueue = null; if (queue) { for (const item of new SafeArrayIterator(queue)) { enqueueOrDispatch(item.message, item.handleInfo, item.callback); } } } function sendInternalMessage(message) { const queueOk = [true]; control.refCounted(); PromisePrototypeThen( writeFn(ipc, message, NO_RAW_FD, queueOk), () => control.unrefCounted(), () => control.unrefCounted(), ); } function getReceivedSocketList(key) { let socketList = receivedSocketLists.get(key); if (!socketList) { socketList = new SocketListReceive(); receivedSocketLists.set(key, socketList); } return socketList; } function handleSocketListMessage(message) { if (message.cmd === NODE_SOCKET_GET_COUNT) { const socketList = receivedSocketLists.get(message.key); sendInternalMessage({ cmd: NODE_SOCKET_COUNT, key: message.key, id: message.id, count: socketList ? socketList.getConnections() : 0, }); return true; } if (message.cmd === NODE_SOCKET_NOTIFY_CLOSE) { const socketList = receivedSocketLists.get(message.key); const sendAck = () => sendInternalMessage({ cmd: NODE_SOCKET_CLOSE_ACK, key: message.key, id: message.id, }); if (socketList) { // The parent only sends NOTIFY_CLOSE when its server is closing, so no // further sockets will be registered under this key. Drop the entry // once it drains, otherwise a long-lived child accumulates one // SocketListReceive per server it has ever received sockets from. socketList.close(() => { receivedSocketLists.delete(message.key); sendAck(); }); } else { sendAck(); } return true; } return false; } // Release any handles we're still holding open when the channel goes away. // A handle send that already wrote successfully keeps its local copy open // until NODE_HANDLE_ACK arrives (see dispatch); queued sends haven't been // written yet. Once the channel is torn down that ACK will never come, so // those handles would leak -- and an `closeAfterSend` handle (e.g. a // received net.Socket being forwarded back) is a live resource that keeps // the event loop alive, so the process would hang instead of exiting. This // is what made `test-cluster-send-deadlock` time out: the worker forwards // its sockets back, then disconnects before the ACKs land. function cleanupPendingHandles() { const info = pendingHandleInfo; pendingHandleInfo = null; if (info && info.closeAfterSend) { info.close(); } const queue = handleQueue; handleQueue = null; if (queue) { for (const item of new SafeArrayIterator(queue)) { if (item.handleInfo && item.handleInfo.closeAfterSend) { item.handleInfo.close(); } } } } // Either queue the send (if a handle is already in flight awaiting its // ACK) or write it now. `handleInfo` is the already-derived IPC handle // info (see target.send) or null for a plain message. function enqueueOrDispatch(message, handleInfo, callback) { // If a previous handle send is still waiting for its ACK, queue this // one to preserve ordering. Plain messages are queued too so they // don't overtake the pending handle. if (handleQueue !== null) { ArrayPrototypePush(handleQueue, { message, handleInfo, callback, }); return handleQueue.length === 1; } return dispatch(message, handleInfo, callback); } function dispatch(message, handleInfo, callback) { if (handleInfo) { // Start queueing subsequent sends until the ACK arrives. handleQueue = []; } // signals whether the queue is within the limit. // if false, the sender should slow down. // this acts as a backpressure mechanism. const queueOk = [true]; control.refCounted(); const writePromise = handleInfo ? writeFn(ipc, handleInfo.message, handleInfo.rawFd, queueOk) : writeFn(ipc, message, NO_RAW_FD, queueOk); PromisePrototypeThen( writePromise, () => { control.unrefCounted(); if (handleInfo) { // Hold the handle until NODE_HANDLE_ACK arrives; closing now // would drop the OFD refcount to 0 before the receiver has // materialized its dup. pendingHandleInfo = handleInfo; } if (callback) { nextTick(callback, null); } }, (err) => { control.unrefCounted(); if (handleInfo) { // Write failed: the receiver won't ACK, so close the handle now // and drain the queue to unblock any follow-up sends. if (handleInfo.closeAfterSend) { handleInfo.close(); } const queue = handleQueue; handleQueue = null; if (queue) { for (const item of new SafeArrayIterator(queue)) { enqueueOrDispatch( item.message, item.handleInfo, item.callback, ); } } } if ( ObjectPrototypeIsPrototypeOf(Deno.errors.Interrupted.prototype, err) ) { // Channel closed on us mid-write. } else { // Match Node: errors raised from a failed IPC send carry // `syscall: "write"`. Tests like `test-cluster-concurrent-disconnect` // assert on this when racing send() against worker disconnect. const errAny = err; if (errAny && typeof errAny === "object" && !errAny.syscall) { errAny.syscall = "write"; } if (typeof callback === "function") { nextTick(callback, err); } else { nextTick(() => target.emit("error", err)); } } }, ); return queueOk[0]; } async function readLoop() { try { while (true) { if (!target.connected || target.killed) { return; } // TODO(nathanwhit): maybe allow returning multiple messages in a single read? needs benchmarking. const prom = readFn(ipc); // there will always be a pending read promise, // but it shouldn't keep the event loop from exiting core.unrefOpPromise(prom); const read = await prom; const msg = read[0]; const rawFd = read[1]; if (isInternal(msg)) { const cmd = internalCmdName(msg); if (cmd === "CLOSE") { // Channel closed. target.disconnect(); return; } else if (cmd === "HANDLE" && typeof rawFd === "number") { if (serialization === "json") { restorePrototype(msg); } // Acknowledge receipt so the sender can close its local copy. sendHandleAck(); const handle = createIpcHandle(msg, rawFd); if ( msg.socketListKey && ObjectPrototypeIsPrototypeOf(Socket.prototype, handle) ) { getReceivedSocketList(msg.socketListKey).add(handle); } nextTick(handleMessage, msg.msg, handle); continue; } else if (cmd === "HANDLE_ACK") { onHandleAck(); continue; } else if (handleSocketListMessage(msg)) { continue; } else { // TODO(nathanwhit): if we want to support deno-node IPC interop, // handle any future NODE_HANDLE_* control messages here. // Emit as internalMessage for internal consumers. if (serialization === "json") { restorePrototype(msg); } nextTick(() => target.emit("internalMessage", msg)); continue; } } nextTick(handleMessage, msg, undefined); } } catch (err) { // All of these mean the IPC channel went away while we were reading it. // ConnectionReset in particular shows up on Linux when the peer exits // with data still buffered: instead of a clean EOF the kernel delivers a // RST, surfacing here as ECONNRESET. Node treats an IPC channel teardown // as a disconnect, never as a process `error`, so we follow suit and tear // down cleanly instead of emitting an uncaught error. if ( ObjectPrototypeIsPrototypeOf(Deno.errors.Interrupted.prototype, err) || ObjectPrototypeIsPrototypeOf(Deno.errors.BadResource.prototype, err) || ObjectPrototypeIsPrototypeOf(Deno.errors.ConnectionReset.prototype, err) ) { // Channel torn down from under us; release any handles awaiting an // ACK that will now never arrive so they don't keep us alive. cleanupPendingHandles(); return; } nextTick(() => target.emit("error", err)); } } function handleMessage(msg, handle) { if (!target.channel) { return; } // serde_v8 deserializes objects with null prototype, but Node.js IPC // messages should have Object.prototype (as if from JSON.parse). if (serialization === "json") { restorePrototype(msg); } // Match Node: when a handle was attached to an internal command (e.g. // NODE_CLUSTER newconn from the round-robin handle), the unwrapped // payload is itself a NODE_* internal message. Route it as // internalMessage so cluster's listener receives it. if (isInternal(msg)) { target.emit("internalMessage", msg, handle); return; } if (target.listenerCount("message") !== 0) { target.emit("message", msg, handle); return; } ArrayPrototypePush(target.channel[kPendingMessages], [msg, handle]); } target.on("newListener", () => { nextTick(() => { if (!target.channel || !target.listenerCount("message")) { return; } for ( const pending of new SafeArrayIterator( target.channel[kPendingMessages], ) ) { target.emit("message", pending[0], pending[1]); } target.channel[kPendingMessages] = []; }); }); target.send = function (message, handle, options, callback) { if (typeof handle === "function") { callback = handle; handle = undefined; options = undefined; } else if (typeof options === "function") { callback = options; options = undefined; } else if (options !== undefined) { validateObject(options, "options"); } options = { swallowErrors: false, ...options }; if (message === undefined) { throw new ERR_MISSING_ARGS("message"); } if ( typeof message !== "string" && typeof message !== "object" && typeof message !== "number" && typeof message !== "boolean" && typeof message !== "bigint" ) { throw new ERR_INVALID_ARG_TYPE( "message", ["string", "object", "number", "boolean"], message, ); } // Match Node: a falsy `handle` (undefined, null) means "no handle". // Reject only non-falsy values that aren't a recognized handle type. if (handle) { if ( !ObjectPrototypeIsPrototypeOf(Socket.prototype, handle) && !ObjectPrototypeIsPrototypeOf(NetServer.prototype, handle) && !ObjectPrototypeIsPrototypeOf(DgramSocket.prototype, handle) && !ObjectPrototypeIsPrototypeOf(TCP.prototype, handle) && !ObjectPrototypeIsPrototypeOf(Pipe.prototype, handle) ) { throw new ERR_INVALID_HANDLE_TYPE(); } } else { handle = undefined; } if (!target.connected) { const err = new ERR_IPC_CHANNEL_CLOSED(); if (typeof callback === "function") { nextTick(callback, err); } else { nextTick(() => target.emit("error", err)); } return false; } // Derive the handle's IPC info eagerly, while the socket is still // alive, *before* deciding whether this send must be queued. Deferring // derivation until drain time (when a previous handle's ACK arrives) // races against the socket being torn down -- e.g. cluster handing two // connections to a worker that immediately closes them. By then the // socket's `_handle` is null and getIpcHandleInfo would throw // `notImplemented("ChildProcess.send with non-TCP net.Socket handle")`. // fdForIpc() dups the fd, so the captured copy survives the original // socket's destruction. let handleInfo = null; if (handle !== undefined) { handleInfo = getIpcHandleInfo(handle, options, target); // `getIpcHandleInfo` returns null when the handle has no underlying // native handle (e.g. a server that never started listening). Match // Node, which strips the handle and sends the plain message instead. if (handleInfo !== null) { handleInfo.message.msg = message; } } return enqueueOrDispatch(message, handleInfo, callback); }; target.connected = true; target.disconnect = function () { if (!target.connected) { throw new ERR_IPC_DISCONNECTED(); } target.connected = false; target[kCanDisconnect] = false; cleanupPendingHandles(); control[kControlDisconnect](); nextTick(() => { target.channel = null; core.close(ipc); target.emit("disconnect"); }); }; target[kCanDisconnect] = true; // Start reading messages from the channel. readLoop(); return control; } return { ChildProcess, mapValues, stdioStringToArray, getValidStdio, validateNullByteNotInArg, normalizeSpawnArguments, spawnSync, setupChannel, default: { ChildProcess, getValidStdio, normalizeSpawnArguments, stdioStringToArray, spawnSync, setupChannel, }, }; })();