// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // This module implements 'child_process' module of Node.JS API. // ref: https://nodejs.org/api/child_process.html // TODO(petamoriken): enable prefer-primordials for node polyfills // deno-lint-ignore-file prefer-primordials import { core, internals } from "ext:core/mod.js"; import { op_node_ipc_read, op_node_ipc_ref, op_node_ipc_unref, op_node_ipc_write, } from "ext:core/ops"; import { ArrayIsArray, ArrayPrototypeFilter, ArrayPrototypeJoin, ArrayPrototypePush, ArrayPrototypeSlice, ArrayPrototypeSort, ArrayPrototypeUnshift, ObjectHasOwn, StringPrototypeStartsWith, StringPrototypeToUpperCase, } from "ext:deno_node/internal/primordials.mjs"; import { assert } from "ext:deno_node/_util/asserts.ts"; import { EventEmitter } from "node:events"; import { os } from "ext:deno_node/internal_binding/constants.ts"; import { notImplemented } from "ext:deno_node/_utils.ts"; import { Readable, Stream, Writable } from "node:stream"; import { isWindows } from "ext:deno_node/_util/os.ts"; import { nextTick } from "ext:deno_node/_next_tick.ts"; import { AbortError, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_IPC_CHANNEL_CLOSED, ERR_UNKNOWN_SIGNAL, } from "ext:deno_node/internal/errors.ts"; import { Buffer } from "node:buffer"; import { errnoException } from "ext:deno_node/internal/errors.ts"; import { ErrnoException } from "ext:deno_node/_global.d.ts"; import { codeMap } from "ext:deno_node/internal_binding/uv.ts"; import { isInt32, validateBoolean, validateObject, validateString, } from "ext:deno_node/internal/validators.mjs"; import { kEmptyObject } from "ext:deno_node/internal/util.mjs"; import { getValidatedPath } from "ext:deno_node/internal/fs/utils.mjs"; import process from "node:process"; import { StringPrototypeSlice } from "ext:deno_node/internal/primordials.mjs"; export function mapValues( record: Readonly>, transformer: (value: T) => O, ): Record { const ret: Record = {}; const entries = Object.entries(record); for (const [key, value] of entries) { if (typeof value === "undefined") { continue; } if (value === null) { continue; } const mappedValue = transformer(value); ret[key] = mappedValue; } return ret; } type NodeStdio = "pipe" | "overlapped" | "ignore" | "inherit" | "ipc"; type DenoStdio = "inherit" | "piped" | "null"; export function stdioStringToArray( stdio: NodeStdio, channel: NodeStdio | number, ) { const options: (NodeStdio | number)[] = []; switch (stdio) { case "ignore": case "overlapped": case "pipe": options.push(stdio, stdio, stdio); break; case "inherit": options.push(stdio, stdio, stdio); break; default: throw new ERR_INVALID_ARG_VALUE("stdio", stdio); } if (channel) options.push(channel); return options; } const kClosesNeeded = Symbol("_closesNeeded"); const kClosesReceived = Symbol("_closesReceived"); // 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: ChildProcess) { child[kClosesReceived]++; if (child[kClosesNeeded] === child[kClosesReceived]) { child.emit("close", child.exitCode, child.signalCode); } } export class ChildProcess extends EventEmitter { /** * The exit code of the child process. This property will be `null` until the child process exits. */ exitCode: number | null = null; /** * This property is set to `true` after `kill()` is called. */ killed = false; /** * The PID of this child process. */ pid!: number; /** * The signal received by this child process. */ signalCode: string | null = null; /** * Command line arguments given to this child process. */ spawnargs: string[]; /** * The executable file name of this child process. */ spawnfile: string; /** * This property represents the child process's stdin. */ stdin: Writable | null = null; /** * This property represents the child process's stdout. */ stdout: Readable | null = null; /** * This property represents the child process's stderr. */ stderr: Readable | null = null; /** * Pipes to this child process. */ stdio: [Writable | null, Readable | null, Readable | null] = [ null, null, null, ]; disconnect?: () => void; #process!: Deno.ChildProcess; #spawned = Promise.withResolvers(); [kClosesNeeded] = 1; [kClosesReceived] = 0; canDisconnect = false; constructor( command: string, args?: string[], options?: ChildProcessOptions, ) { super(); const { env = {}, stdio = ["pipe", "pipe", "pipe"], cwd, shell = false, signal, windowsVerbatimArguments = false, } = options || {}; const normalizedStdio = normalizeStdioOption(stdio); const [ stdin = "pipe", stdout = "pipe", stderr = "pipe", _channel, // TODO(kt3k): handle this correctly ] = normalizedStdio; const [cmd, cmdArgs] = buildCommand( command, args || [], shell, ); this.spawnfile = cmd; this.spawnargs = [cmd, ...cmdArgs]; const ipc = normalizedStdio.indexOf("ipc"); const stringEnv = mapValues(env, (value) => value.toString()); try { this.#process = new Deno.Command(cmd, { args: cmdArgs, cwd, env: stringEnv, stdin: toDenoStdio(stdin), stdout: toDenoStdio(stdout), stderr: toDenoStdio(stderr), windowsRawArguments: windowsVerbatimArguments, ipc, // internal }).spawn(); this.pid = this.#process.pid; if (stdin === "pipe") { assert(this.#process.stdin); this.stdin = Writable.fromWeb(this.#process.stdin); } if (stdin instanceof Stream) { this.stdin = stdin; } if (stdout instanceof Stream) { this.stdout = stdout; } if (stderr instanceof Stream) { this.stderr = stderr; } if (stdout === "pipe") { assert(this.#process.stdout); this[kClosesNeeded]++; this.stdout = Readable.fromWeb(this.#process.stdout); this.stdout.on("close", () => { maybeClose(this); }); } if (stderr === "pipe") { assert(this.#process.stderr); this[kClosesNeeded]++; this.stderr = Readable.fromWeb(this.#process.stderr); this.stderr.on("close", () => { maybeClose(this); }); } // TODO(nathanwhit): once we impl > 3 stdio pipes make sure we also listen for their // close events (like above) this.stdio[0] = this.stdin; this.stdio[1] = this.stdout; this.stdio[2] = this.stderr; nextTick(() => { this.emit("spawn"); this.#spawned.resolve(); }); if (signal) { const onAbortListener = () => { try { if (this.kill("SIGKILL")) { this.emit("error", new AbortError()); } } 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 pipeFd = internals.getPipeFd(this.#process); if (typeof pipeFd == "number") { setupChannel(this, pipeFd); this[kClosesNeeded]++; this.on("disconnect", () => { maybeClose(this); }); } (async () => { const status = await this.#process.status; this.exitCode = status.code; this.#spawned.promise.then(async () => { const exitCode = this.signalCode == null ? this.exitCode : null; const signalCode = this.signalCode == null ? null : this.signalCode; // The 'exit' and 'close' events must be emitted after the 'spawn' event. this.emit("exit", exitCode, signalCode); await this.#_waitForChildStreamsToClose(); this.#closePipes(); maybeClose(this); }); })(); } catch (err) { let e = err; if (e instanceof Deno.errors.NotFound) { e = _createSpawnSyncError("ENOENT", command, args); } this.#_handleError(e); } } /** * @param signal NOTE: this parameter is not yet implemented. */ kill(signal?: number | string): boolean { if (this.killed) { return this.killed; } const denoSignal = signal == null ? "SIGTERM" : toDenoSignal(signal); this.#closePipes(); try { this.#process.kill(denoSignal); } catch (err) { const alreadyClosed = err instanceof TypeError || err instanceof Deno.errors.PermissionDenied; if (!alreadyClosed) { throw err; } } /* Cancel any pending IPC I/O */ if (this.canDisconnect) { this.disconnect?.(); } this.killed = true; this.signalCode = denoSignal; return this.killed; } ref() { this.#process.ref(); } unref() { this.#process.unref(); } async #_waitForChildStreamsToClose() { const promises = [] as Array>; // 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(); promises.push(waitForStreamToClose(this.stdin)); } // Only readable streams need to be closed if ( this.stdout && !this.stdout.destroyed && this.stdout instanceof Readable ) { promises.push(waitForReadableToClose(this.stdout)); } // Only readable streams need to be closed if ( this.stderr && !this.stderr.destroyed && this.stderr instanceof Readable ) { promises.push(waitForReadableToClose(this.stderr)); } await Promise.all(promises); } #_handleError(err: unknown) { nextTick(() => { this.emit("error", err); // TODO(uki00a) Convert `err` into nodejs's `SystemError` class. }); } #closePipes() { if (this.stdin) { assert(this.stdin); this.stdin.destroy(); } /// TODO(nathanwhit): for some reason when the child process exits /// and the child end of the named pipe closes, reads still just return `Pending` /// instead of returning that 0 bytes were read (to signal the pipe died). /// For now, just forcibly disconnect, but in theory I think we could miss messages /// that haven't been read yet. if (Deno.build.os === "windows") { if (this.canDisconnect) { this.disconnect?.(); } } } } const supportedNodeStdioTypes: NodeStdio[] = [ "pipe", "ignore", "inherit", "ipc", ]; function toDenoStdio( pipe: NodeStdio | number | Stream | null | undefined, ): DenoStdio { if (pipe instanceof Stream) { return "inherit"; } if (typeof pipe === "number") { /* Assume it's a rid returned by fs APIs */ return pipe; } if ( !supportedNodeStdioTypes.includes(pipe as NodeStdio) ) { notImplemented(`toDenoStdio pipe=${typeof pipe} (${pipe})`); } switch (pipe) { case "pipe": case undefined: case null: return "piped"; case "ignore": return "null"; case "inherit": return "inherit"; case "ipc": return "ipc_for_internal_use"; default: notImplemented(`toDenoStdio pipe=${typeof pipe} (${pipe})`); } } function toDenoSignal(signal: number | string): Deno.Signal { if (typeof signal === "number") { for (const name of keys(os.signals)) { if (os.signals[name] === signal) { return name as Deno.Signal; } } throw new ERR_UNKNOWN_SIGNAL(String(signal)); } const denoSignal = signal as Deno.Signal; if (denoSignal in os.signals) { return denoSignal; } throw new ERR_UNKNOWN_SIGNAL(signal); } function keys>(object: T): Array { return Object.keys(object); } export interface ChildProcessOptions { /** * Current working directory of the child process. */ cwd?: string | URL; /** * Environment variables passed to the child process. */ env?: Record; /** * This option defines child process's stdio configuration. * @see https://nodejs.org/api/child_process.html#child_process_options_stdio */ stdio?: Array | NodeStdio; /** * NOTE: This option is not yet implemented. */ detached?: boolean; /** * NOTE: This option is not yet implemented. */ uid?: number; /** * NOTE: This option is not yet implemented. */ gid?: number; /** * NOTE: This option is not yet implemented. */ argv0?: string; /** * * If this option is `true`, run the command in the shell. * * If this option is a string, run the command in the specified shell. */ shell?: string | boolean; /** * Allows aborting the child process using an AbortSignal. */ signal?: AbortSignal; /** * NOTE: This option is not yet implemented. */ serialization?: "json" | "advanced"; /** No quoting or escaping of arguments is done on Windows. Ignored on Unix. * Default: false. */ windowsVerbatimArguments?: boolean; /** * NOTE: This option is not yet implemented. */ windowsHide?: boolean; } function copyProcessEnvToEnv( env: Record, name: string, optionEnv?: Record, ) { if ( Deno.env.get(name) && (!optionEnv || !ObjectHasOwn(optionEnv, name)) ) { env[name] = Deno.env.get(name); } } function normalizeStdioOption( stdio: Array | NodeStdio = [ "pipe", "pipe", "pipe", ], ): [ Stream | NodeStdio | number, Stream | NodeStdio | number, Stream | NodeStdio | number, ...Array, ] { if (Array.isArray(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})`); } } } export function normalizeSpawnArguments( file: string, args: string[], options: SpawnOptions & SpawnSyncOptions, ) { validateString(file, "file"); if (file.length === 0) { throw new ERR_INVALID_ARG_VALUE("file", file, "cannot be empty"); } 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 = []; } if (options === undefined) { options = kEmptyObject; } else { validateObject(options, "options"); } let cwd = options.cwd; // Validate the cwd, if present. if (cwd != null) { cwd = getValidatedPath(cwd, "options.cwd") as string; } // Validate detached, if present. if (options.detached != null) { validateBoolean(options.detached, "options.detached"); } // Validate the uid, if present. if (options.uid != null && !isInt32(options.uid)) { throw new ERR_INVALID_ARG_TYPE("options.uid", "int32", options.uid); } // Validate the gid, if present. if (options.gid != null && !isInt32(options.gid)) { throw new ERR_INVALID_ARG_TYPE("options.gid", "int32", 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, ); } // Validate argv0, if present. if (options.argv0 != null) { validateString(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", ); } if (options.shell) { const command = ArrayPrototypeJoin([file, ...args], " "); // Set the shell, switches, and commands. 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 (/^(?:.*\\)?cmd(?:\.exe)?$/i.exec(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: string[][] = []; // 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: string[] = []; // Prototype values are intentionally included. for (const key in env) { if (Object.hasOwn(env, key)) { 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) /** TODO: implement SafeSet and makeSafe */ const sawKey = new Set(); envKeys = ArrayPrototypeFilter( ArrayPrototypeSort(envKeys), (key: string) => { const uppercaseKey = StringPrototypeToUpperCase(key); if (sawKey.has(uppercaseKey)) { return false; } sawKey.add(uppercaseKey); return true; }, ); } for (const key of envKeys) { const value = env[key]; if (value !== undefined) { ArrayPrototypePush(envPairs, `${key}=${value}`); } } return { // Make a shallow copy so we don't clobber the user's options object. ...options, args, cwd, detached: !!options.detached, envPairs, file, windowsHide: !!options.windowsHide, windowsVerbatimArguments: !!windowsVerbatimArguments, }; } function waitForReadableToClose(readable: Readable) { readable.resume(); // Ensure buffered data will be consumed. return waitForStreamToClose(readable as unknown as Stream); } function waitForStreamToClose(stream: Stream) { const deferred = Promise.withResolvers(); const cleanup = () => { stream.removeListener("close", onClose); stream.removeListener("error", onError); }; const onClose = () => { cleanup(); deferred.resolve(); }; const onError = (err: Error) => { cleanup(); deferred.reject(err); }; stream.once("close", onClose); stream.once("error", onError); return deferred.promise; } /** * 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: string, args: string[], shell: string | boolean, ): [string, string[]] { if (file === Deno.execPath()) { // The user is trying to spawn another Deno process as Node.js. args = toDenoArgs(args); } if (shell) { const command = [file, ...args].join(" "); // Set the shell, switches, and commands. if (isWindows) { if (typeof shell === "string") { file = shell; } else { file = Deno.env.get("comspec") || "cmd.exe"; } // '/d /s /c' is used only for cmd.exe. if (/^(?:.*\\)?cmd(?:\.exe)?$/i.test(file)) { args = ["/d", "/s", "/c", `"${command}"`]; } else { args = ["-c", command]; } } else { if (typeof shell === "string") { file = shell; } else { file = "/bin/sh"; } args = ["-c", command]; } } return [file, args]; } function _createSpawnSyncError( status: string, command: string, args: string[] = [], ): ErrnoException { const error = errnoException( codeMap.get(status), "spawnSync " + command, ); error.path = command; error.spawnargs = args; return error; } export interface SpawnOptions extends ChildProcessOptions { /** * NOTE: This option is not yet implemented. */ timeout?: number; /** * NOTE: This option is not yet implemented. */ killSignal?: string; } export interface SpawnSyncOptions extends Pick< ChildProcessOptions, | "cwd" | "env" | "argv0" | "stdio" | "uid" | "gid" | "shell" | "windowsVerbatimArguments" | "windowsHide" > { input?: string | Buffer | DataView; timeout?: number; maxBuffer?: number; encoding?: string; /** * NOTE: This option is not yet implemented. */ killSignal?: string; } export interface SpawnSyncResult { pid?: number; output?: [string | null, string | Buffer | null, string | Buffer | null]; stdout?: Buffer | string | null; stderr?: Buffer | string | null; status?: number | null; signal?: string | null; error?: Error; } function parseSpawnSyncOutputStreams( output: Deno.CommandOutput, name: "stdout" | "stderr", ): string | Buffer | null { // new Deno.Command().outputSync() returns getters for stdout and stderr that throw when set // to 'inherit'. try { return Buffer.from(output[name]) as string | Buffer; } catch { return null; } } export function spawnSync( command: string, args: string[], options: SpawnSyncOptions, ): SpawnSyncResult { const { env = Deno.env.toObject(), stdio = ["pipe", "pipe", "pipe"], shell = false, cwd, encoding, uid, gid, maxBuffer, windowsVerbatimArguments = false, } = options; const [ stdin_ = "pipe", stdout_ = "pipe", stderr_ = "pipe", _channel, // TODO(kt3k): handle this correctly ] = normalizeStdioOption(stdio); [command, args] = buildCommand(command, args ?? [], shell); const result: SpawnSyncResult = {}; try { const output = new Deno.Command(command, { args, cwd, env: mapValues(env, (value) => value.toString()), stdout: toDenoStdio(stdout_), stderr: toDenoStdio(stderr_), stdin: stdin_ == "inherit" ? "inherit" : "null", uid, gid, windowsRawArguments: windowsVerbatimArguments, }).outputSync(); const status = output.signal ? null : output.code; let stdout = parseSpawnSyncOutputStreams(output, "stdout"); let stderr = parseSpawnSyncOutputStreams(output, "stderr"); if ( (stdout && stdout.length > maxBuffer!) || (stderr && stderr.length > maxBuffer!) ) { result.error = _createSpawnSyncError("ENOBUFS", command, args); } if (encoding && encoding !== "buffer") { stdout = stdout && stdout.toString(encoding); stderr = stderr && stderr.toString(encoding); } result.status = status; result.signal = output.signal; result.stdout = stdout; result.stderr = stderr; result.output = [output.signal, stdout, stderr]; } catch (err) { if (err instanceof Deno.errors.NotFound) { result.error = _createSpawnSyncError("ENOENT", command, args); } } return result; } // These are Node.js CLI flags that expect a value. It's necessary to // understand these flags in order to properly replace flags passed to the // child process. For example, -e is a Node flag for eval mode if it is part // of process.execArgv. However, -e could also be an application flag if it is // part of process.execv instead. We only want to process execArgv flags. const kLongArgType = 1; const kShortArgType = 2; const kLongArg = { type: kLongArgType }; const kShortArg = { type: kShortArgType }; const kNodeFlagsMap = new Map([ ["--build-snapshot", kLongArg], ["-c", kShortArg], ["--check", kLongArg], ["-C", kShortArg], ["--conditions", kLongArg], ["--cpu-prof-dir", kLongArg], ["--cpu-prof-interval", kLongArg], ["--cpu-prof-name", kLongArg], ["--diagnostic-dir", kLongArg], ["--disable-proto", kLongArg], ["--dns-result-order", kLongArg], ["-e", kShortArg], ["--eval", kLongArg], ["--experimental-loader", kLongArg], ["--experimental-policy", kLongArg], ["--experimental-specifier-resolution", kLongArg], ["--heapsnapshot-near-heap-limit", kLongArg], ["--heapsnapshot-signal", kLongArg], ["--heap-prof-dir", kLongArg], ["--heap-prof-interval", kLongArg], ["--heap-prof-name", kLongArg], ["--icu-data-dir", kLongArg], ["--input-type", kLongArg], ["--inspect-publish-uid", kLongArg], ["--max-http-header-size", kLongArg], ["--openssl-config", kLongArg], ["-p", kShortArg], ["--print", kLongArg], ["--policy-integrity", kLongArg], ["--prof-process", kLongArg], ["-r", kShortArg], ["--require", kLongArg], ["--redirect-warnings", kLongArg], ["--report-dir", kLongArg], ["--report-directory", kLongArg], ["--report-filename", kLongArg], ["--report-signal", kLongArg], ["--secure-heap", kLongArg], ["--secure-heap-min", kLongArg], ["--snapshot-blob", kLongArg], ["--title", kLongArg], ["--tls-cipher-list", kLongArg], ["--tls-keylog", kLongArg], ["--unhandled-rejections", kLongArg], ["--use-largepages", kLongArg], ["--v8-pool-size", kLongArg], ]); const kDenoSubcommands = new Set([ "add", "bench", "bundle", "cache", "check", "compile", "completions", "coverage", "doc", "eval", "fmt", "help", "info", "init", "install", "lint", "lsp", "publish", "repl", "run", "tasks", "test", "types", "uninstall", "upgrade", "vendor", ]); function toDenoArgs(args: string[]): string[] { if (args.length === 0) { return args; } // Update this logic as more CLI arguments are mapped from Node to Deno. const denoArgs: string[] = []; let useRunArgs = true; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg.charAt(0) !== "-" || arg === "--") { // Not a flag or no more arguments. // If the arg is a Deno subcommand, then the child process is being // spawned as Deno, not Deno in Node compat mode. In this case, bail out // and return the original args. if (kDenoSubcommands.has(arg)) { return args; } // Copy of the rest of the arguments to the output. for (let j = i; j < args.length; j++) { denoArgs.push(args[j]); } break; } // Something that looks like a flag was passed. let flag = arg; let flagInfo = kNodeFlagsMap.get(arg); let isLongWithValue = false; let flagValue; if (flag === "--v8-options") { // If --v8-options is passed, it should be replaced with --v8-flags="--help". denoArgs.push("--v8-flags=--help"); continue; } if (flagInfo === undefined) { // If the flag was not found, it's either not a known flag or it's a long // flag containing an '='. const splitAt = arg.indexOf("="); if (splitAt !== -1) { flag = arg.slice(0, splitAt); flagInfo = kNodeFlagsMap.get(flag); flagValue = arg.slice(splitAt + 1); isLongWithValue = true; } } if (flagInfo === undefined) { // Not a known flag that expects a value. Just copy it to the output. denoArgs.push(arg); continue; } // This is a flag with a value. Get the value if we don't already have it. if (flagValue === undefined) { i++; if (i >= args.length) { // There was user error. There should be another arg for the value, but // there isn't one. Just copy the arg to the output. It's not going // to work anyway. denoArgs.push(arg); continue; } flagValue = args[i]; } // Remap Node's eval flags to Deno. if (flag === "-e" || flag === "--eval") { denoArgs.push("eval", flagValue); useRunArgs = false; } else if (isLongWithValue) { denoArgs.push(arg); } else { denoArgs.push(flag, flagValue); } } if (useRunArgs) { // -A is not ideal, but needed to propagate permissions. denoArgs.unshift("run", "-A"); } return denoArgs; } const kControlDisconnect = Symbol("kControlDisconnect"); const kPendingMessages = Symbol("kPendingMessages"); // controls refcounting for the IPC channel class Control extends EventEmitter { #channel: number; #refs: number = 0; #refExplicitlySet = false; #connected = true; [kPendingMessages] = []; constructor(channel: number) { super(); this.#channel = channel; } #ref() { if (this.#connected) { op_node_ipc_ref(this.#channel); } } #unref() { if (this.#connected) { op_node_ipc_unref(this.#channel); } } [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(); } } type InternalMessage = { cmd: `NODE_${string}`; }; // deno-lint-ignore no-explicit-any function isInternal(msg: any): msg is InternalMessage { if (msg && typeof msg === "object") { const cmd = msg["cmd"]; if (typeof cmd === "string") { return StringPrototypeStartsWith(cmd, "NODE_"); } } return false; } function internalCmdName(msg: InternalMessage): string { return StringPrototypeSlice(msg.cmd, 5); } // deno-lint-ignore no-explicit-any export function setupChannel(target: any, ipc: number) { const control = new Control(ipc); target.channel = control; async function readLoop() { try { while (true) { if (!target.connected || target.killed) { return; } const prom = op_node_ipc_read(ipc); // there will always be a pending read promise, // but it shouldn't keep the event loop from exiting core.unrefOpPromise(prom); const msg = await prom; if (isInternal(msg)) { const cmd = internalCmdName(msg); if (cmd === "CLOSE") { // Channel closed. target.disconnect(); return; } else { // TODO(nathanwhit): once we add support for sending // handles, if we want to support deno-node IPC interop, // we'll need to handle the NODE_HANDLE_* messages here. continue; } } process.nextTick(handleMessage, msg); } } catch (err) { if ( err instanceof Deno.errors.Interrupted || err instanceof Deno.errors.BadResource ) { return; } } } function handleMessage(msg) { if (!target.channel) { return; } if (target.listenerCount("message") !== 0) { target.emit("message", msg); return; } ArrayPrototypePush(target.channel[kPendingMessages], msg); } target.on("newListener", () => { nextTick(() => { if (!target.channel || !target.listenerCount("message")) { return; } for (const msg of target.channel[kPendingMessages]) { target.emit("message", msg); } 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 TypeError("ERR_MISSING_ARGS", "message"); } if (handle !== undefined) { notImplemented("ChildProcess.send with handle"); } if (!target.connected) { const err = new ERR_IPC_CHANNEL_CLOSED(); if (typeof callback === "function") { console.error("ChildProcess.send with callback"); process.nextTick(callback, err); } else { nextTick(() => target.emit("error", err)); } return false; } // 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(); op_node_ipc_write(ipc, message, queueOk) .then(() => { control.unrefCounted(); if (callback) { process.nextTick(callback, null); } }); return queueOk[0]; }; target.connected = true; target.disconnect = function () { if (!target.connected) { target.emit("error", new Error("IPC channel is already disconnected")); return; } target.connected = false; target.canDisconnect = false; control[kControlDisconnect](); process.nextTick(() => { target.channel = null; core.close(ipc); target.emit("disconnect"); }); }; target.canDisconnect = true; // Start reading messages from the channel. readLoop(); return control; } export default { ChildProcess, normalizeSpawnArguments, stdioStringToArray, spawnSync, setupChannel, };