summaryrefslogtreecommitdiff
path: root/ext/node/polyfills/internal/child_process.ts
diff options
context:
space:
mode:
authorBartek IwaƄczuk <biwanczuk@gmail.com>2023-02-14 17:38:45 +0100
committerGitHub <noreply@github.com>2023-02-14 17:38:45 +0100
commitd47147fb6ad229b1c039aff9d0959b6e281f4df5 (patch)
tree6e9e790f2b9bc71b5f0c9c7e64b95cae31579d58 /ext/node/polyfills/internal/child_process.ts
parent1d00bbe47e2ca14e2d2151518e02b2324461a065 (diff)
feat(ext/node): embed std/node into the snapshot (#17724)
This commit moves "deno_std/node" in "ext/node" crate. The code is transpiled and snapshotted during the build process. During the first pass a minimal amount of work was done to create the snapshot, a lot of code in "ext/node" depends on presence of "Deno" global. This code will be gradually fixed in the follow up PRs to migrate it to import relevant APIs from "internal:" modules. Currently the code from snapshot is not used in any way, and all Node/npm compatibility still uses code from "https://deno.land/std/node" (or from the location specified by "DENO_NODE_COMPAT_URL"). This will also be handled in a follow up PRs. --------- Co-authored-by: crowlkats <crowlkats@toaxl.com> Co-authored-by: Divy Srivastava <dj.srivastava23@gmail.com> Co-authored-by: Yoshiya Hinosawa <stibium121@gmail.com>
Diffstat (limited to 'ext/node/polyfills/internal/child_process.ts')
-rw-r--r--ext/node/polyfills/internal/child_process.ts1026
1 files changed, 1026 insertions, 0 deletions
diff --git a/ext/node/polyfills/internal/child_process.ts b/ext/node/polyfills/internal/child_process.ts
new file mode 100644
index 000000000..92aa8d4fa
--- /dev/null
+++ b/ext/node/polyfills/internal/child_process.ts
@@ -0,0 +1,1026 @@
+// Copyright 2018-2023 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
+import { assert } from "internal:deno_node/polyfills/_util/asserts.ts";
+import { EventEmitter } from "internal:deno_node/polyfills/events.ts";
+import { os } from "internal:deno_node/polyfills/internal_binding/constants.ts";
+import {
+ notImplemented,
+ warnNotImplemented,
+} from "internal:deno_node/polyfills/_utils.ts";
+import {
+ Readable,
+ Stream,
+ Writable,
+} from "internal:deno_node/polyfills/stream.ts";
+import { deferred } from "internal:deno_node/polyfills/_util/async.ts";
+import { isWindows } from "internal:deno_node/polyfills/_util/os.ts";
+import { nextTick } from "internal:deno_node/polyfills/_next_tick.ts";
+import {
+ AbortError,
+ ERR_INVALID_ARG_TYPE,
+ ERR_INVALID_ARG_VALUE,
+ ERR_UNKNOWN_SIGNAL,
+} from "internal:deno_node/polyfills/internal/errors.ts";
+import { Buffer } from "internal:deno_node/polyfills/buffer.ts";
+import { errnoException } from "internal:deno_node/polyfills/internal/errors.ts";
+import { ErrnoException } from "internal:deno_node/polyfills/_global.d.ts";
+import { codeMap } from "internal:deno_node/polyfills/internal_binding/uv.ts";
+import {
+ isInt32,
+ validateBoolean,
+ validateObject,
+ validateString,
+} from "internal:deno_node/polyfills/internal/validators.mjs";
+import {
+ ArrayIsArray,
+ ArrayPrototypeFilter,
+ ArrayPrototypeJoin,
+ ArrayPrototypePush,
+ ArrayPrototypeSlice,
+ ArrayPrototypeSort,
+ ArrayPrototypeUnshift,
+ ObjectPrototypeHasOwnProperty,
+ StringPrototypeToUpperCase,
+} from "internal:deno_node/polyfills/internal/primordials.mjs";
+import { kEmptyObject } from "internal:deno_node/polyfills/internal/util.mjs";
+import { getValidatedPath } from "internal:deno_node/polyfills/internal/fs/utils.mjs";
+import process from "internal:deno_node/polyfills/process.ts";
+
+export function mapValues<T, O>(
+ record: Readonly<Record<string, T>>,
+ transformer: (value: T) => O,
+): Record<string, O> {
+ const ret: Record<string, O> = {};
+ const entries = Object.entries(record);
+
+ for (const [key, value] of entries) {
+ const mappedValue = transformer(value);
+
+ ret[key] = mappedValue;
+ }
+
+ return ret;
+}
+
+type NodeStdio = "pipe" | "overlapped" | "ignore" | "inherit" | "ipc";
+type DenoStdio = "inherit" | "piped" | "null";
+
+// @ts-ignore Deno[Deno.internal] is used on purpose here
+const DenoCommand = Deno[Deno.internal]?.nodeUnstable?.Command ||
+ Deno.Command;
+
+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;
+}
+
+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,
+ ];
+
+ #process!: Deno.ChildProcess;
+ #spawned = deferred<void>();
+
+ constructor(
+ command: string,
+ args?: string[],
+ options?: ChildProcessOptions,
+ ) {
+ super();
+
+ const {
+ env = {},
+ stdio = ["pipe", "pipe", "pipe"],
+ cwd,
+ shell = false,
+ signal,
+ windowsVerbatimArguments = false,
+ } = options || {};
+ const [
+ stdin = "pipe",
+ stdout = "pipe",
+ stderr = "pipe",
+ _channel, // TODO(kt3k): handle this correctly
+ ] = normalizeStdioOption(stdio);
+ const [cmd, cmdArgs] = buildCommand(
+ command,
+ args || [],
+ shell,
+ );
+ this.spawnfile = cmd;
+ this.spawnargs = [cmd, ...cmdArgs];
+
+ const stringEnv = mapValues(env, (value) => value.toString());
+
+ try {
+ this.#process = new DenoCommand(cmd, {
+ args: cmdArgs,
+ cwd,
+ env: stringEnv,
+ stdin: toDenoStdio(stdin as NodeStdio | number),
+ stdout: toDenoStdio(stdout as NodeStdio | number),
+ stderr: toDenoStdio(stderr as NodeStdio | number),
+ windowsRawArguments: windowsVerbatimArguments,
+ }).spawn();
+ this.pid = this.#process.pid;
+
+ if (stdin === "pipe") {
+ assert(this.#process.stdin);
+ this.stdin = Writable.fromWeb(this.#process.stdin);
+ }
+
+ if (stdout === "pipe") {
+ assert(this.#process.stdout);
+ this.stdout = Readable.fromWeb(this.#process.stdout);
+ }
+
+ if (stderr === "pipe") {
+ assert(this.#process.stderr);
+ this.stderr = Readable.fromWeb(this.#process.stderr);
+ }
+
+ 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),
+ );
+ }
+ }
+
+ (async () => {
+ const status = await this.#process.status;
+ this.exitCode = status.code;
+ this.#spawned.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();
+ this.emit("close", exitCode, signalCode);
+ });
+ })();
+ } catch (err) {
+ this.#_handleError(err);
+ }
+ }
+
+ /**
+ * @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;
+ }
+ }
+ this.killed = true;
+ this.signalCode = denoSignal;
+ return this.killed;
+ }
+
+ ref() {
+ this.#process.ref();
+ }
+
+ unref() {
+ this.#process.unref();
+ }
+
+ disconnect() {
+ warnNotImplemented("ChildProcess.prototype.disconnect");
+ }
+
+ async #_waitForChildStreamsToClose() {
+ const promises = [] as Array<Promise<void>>;
+ if (this.stdin && !this.stdin.destroyed) {
+ assert(this.stdin);
+ this.stdin.destroy();
+ promises.push(waitForStreamToClose(this.stdin));
+ }
+ if (this.stdout && !this.stdout.destroyed) {
+ promises.push(waitForReadableToClose(this.stdout));
+ }
+ if (this.stderr && !this.stderr.destroyed) {
+ 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();
+ }
+ }
+}
+
+const supportedNodeStdioTypes: NodeStdio[] = ["pipe", "ignore", "inherit"];
+function toDenoStdio(
+ pipe: NodeStdio | number | Stream | null | undefined,
+): DenoStdio {
+ if (
+ !supportedNodeStdioTypes.includes(pipe as NodeStdio) ||
+ typeof pipe === "number" || pipe instanceof Stream
+ ) {
+ notImplemented(`toDenoStdio pipe=${typeof pipe} (${pipe})`);
+ }
+ switch (pipe) {
+ case "pipe":
+ case undefined:
+ case null:
+ return "piped";
+ case "ignore":
+ return "null";
+ case "inherit":
+ return "inherit";
+ 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<T extends Record<string, unknown>>(object: T): Array<keyof T> {
+ 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<string, string | number | boolean>;
+
+ /**
+ * This option defines child process's stdio configuration.
+ * @see https://nodejs.org/api/child_process.html#child_process_options_stdio
+ */
+ stdio?: Array<NodeStdio | number | Stream | null | undefined> | 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<string, string | number | boolean | undefined>,
+ name: string,
+ optionEnv?: Record<string, string | number | boolean>,
+) {
+ if (
+ Deno.env.get(name) &&
+ (!optionEnv ||
+ !ObjectPrototypeHasOwnProperty(optionEnv, name))
+ ) {
+ env[name] = Deno.env.get(name);
+ }
+}
+
+function normalizeStdioOption(
+ stdio: Array<NodeStdio | number | null | undefined | Stream> | NodeStdio = [
+ "pipe",
+ "pipe",
+ "pipe",
+ ],
+) {
+ if (Array.isArray(stdio)) {
+ 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 promise = deferred<void>();
+ const cleanup = () => {
+ stream.removeListener("close", onClose);
+ stream.removeListener("error", onError);
+ };
+ const onClose = () => {
+ cleanup();
+ promise.resolve();
+ };
+ const onError = (err: Error) => {
+ cleanup();
+ promise.reject(err);
+ };
+ stream.once("close", onClose);
+ stream.once("error", onError);
+ return 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 normalizedStdio = normalizeStdioOption(stdio);
+ [command, args] = buildCommand(command, args ?? [], shell);
+
+ const result: SpawnSyncResult = {};
+ try {
+ const output = new DenoCommand(command, {
+ args,
+ cwd,
+ env,
+ stdout: toDenoStdio(normalizedStdio[1] as NodeStdio | number),
+ stderr: toDenoStdio(normalizedStdio[2] as NodeStdio | number),
+ uid,
+ gid,
+ windowsRawArguments: windowsVerbatimArguments,
+ }).outputSync();
+
+ const status = output.signal ? null : 0;
+ 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([
+ "bench",
+ "bundle",
+ "cache",
+ "check",
+ "compile",
+ "completions",
+ "coverage",
+ "doc",
+ "eval",
+ "fmt",
+ "help",
+ "info",
+ "init",
+ "install",
+ "lint",
+ "lsp",
+ "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 (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.
+ // --unstable is needed for Node compat.
+ denoArgs.unshift("run", "-A", "--unstable");
+ }
+
+ return denoArgs;
+}
+
+export default {
+ ChildProcess,
+ normalizeSpawnArguments,
+ stdioStringToArray,
+ spawnSync,
+};