mirror of
https://github.com/denoland/deno.git
synced 2024-11-23 15:16:54 -05:00
55fac9f5ea
This PR implements the child_process IPC pipe between parent and child. The implementation uses Windows named pipes created by parent and passes the inheritable file handle to the child. I've also replace parts of the initial implementation which passed the raw parent fd to JS with resource ids instead. This way no file handle is exposed to the JS land (both parent and child). `IpcJsonStreamResource` can stream upto 800MB/s of JSON data on Win 11 AMD Ryzen 7 16GB (without `memchr` vectorization)
1156 lines
29 KiB
TypeScript
1156 lines
29 KiB
TypeScript
// 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
|
|
|
|
// TODO(petamoriken): enable prefer-primordials for node polyfills
|
|
// deno-lint-ignore-file prefer-primordials
|
|
|
|
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, warnNotImplemented } 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_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 {
|
|
ArrayIsArray,
|
|
ArrayPrototypeFilter,
|
|
ArrayPrototypeJoin,
|
|
ArrayPrototypePush,
|
|
ArrayPrototypeSlice,
|
|
ArrayPrototypeSort,
|
|
ArrayPrototypeUnshift,
|
|
ObjectHasOwn,
|
|
StringPrototypeToUpperCase,
|
|
} from "ext:deno_node/internal/primordials.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";
|
|
|
|
const core = globalThis.__bootstrap.core;
|
|
|
|
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) {
|
|
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;
|
|
}
|
|
|
|
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 = Promise.withResolvers<void>();
|
|
|
|
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.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),
|
|
);
|
|
}
|
|
}
|
|
|
|
if (typeof this.#process._pipeFd == "number") {
|
|
setupChannel(this, this.#process._pipeFd);
|
|
}
|
|
|
|
(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();
|
|
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>>;
|
|
// 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();
|
|
}
|
|
}
|
|
}
|
|
|
|
const supportedNodeStdioTypes: NodeStdio[] = ["pipe", "ignore", "inherit"];
|
|
function toDenoStdio(
|
|
pipe: NodeStdio | number | Stream | null | undefined,
|
|
): DenoStdio {
|
|
if (pipe instanceof Stream) {
|
|
return "inherit";
|
|
}
|
|
|
|
if (
|
|
!supportedNodeStdioTypes.includes(pipe as NodeStdio) ||
|
|
typeof pipe === "number"
|
|
) {
|
|
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 ||
|
|
!ObjectHasOwn(optionEnv, name))
|
|
) {
|
|
env[name] = Deno.env.get(name);
|
|
}
|
|
}
|
|
|
|
function normalizeStdioOption(
|
|
stdio: Array<NodeStdio | number | null | undefined | Stream> | NodeStdio = [
|
|
"pipe",
|
|
"pipe",
|
|
"pipe",
|
|
],
|
|
): [
|
|
Stream | NodeStdio | number,
|
|
Stream | NodeStdio | number,
|
|
Stream | NodeStdio | number,
|
|
...Array<Stream | NodeStdio | number>,
|
|
] {
|
|
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"];
|
|
}
|
|
|
|
// 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<void>();
|
|
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", // TODO(bartlomieju): use this?
|
|
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_),
|
|
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([
|
|
"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 function setupChannel(target, ipc) {
|
|
async function readLoop() {
|
|
try {
|
|
while (true) {
|
|
if (!target.connected || target.killed) {
|
|
return;
|
|
}
|
|
const msg = await core.opAsync("op_node_ipc_read", ipc);
|
|
if (msg == null) {
|
|
// Channel closed.
|
|
target.disconnect();
|
|
return;
|
|
}
|
|
|
|
process.nextTick(handleMessage, msg);
|
|
}
|
|
} catch (err) {
|
|
if (
|
|
err instanceof Deno.errors.Interrupted ||
|
|
err instanceof Deno.errors.BadResource
|
|
) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleMessage(msg) {
|
|
target.emit("message", msg);
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
core.opAsync("op_node_ipc_write", ipc, message)
|
|
.then(() => {
|
|
if (callback) {
|
|
process.nextTick(callback, null);
|
|
}
|
|
});
|
|
};
|
|
|
|
target.connected = true;
|
|
|
|
target.disconnect = function () {
|
|
if (!this.connected) {
|
|
this.emit("error", new Error("IPC channel is already disconnected"));
|
|
return;
|
|
}
|
|
|
|
this.connected = false;
|
|
process.nextTick(() => {
|
|
core.close(ipc);
|
|
target.emit("disconnect");
|
|
});
|
|
};
|
|
|
|
// Start reading messages from the channel.
|
|
readLoop();
|
|
}
|
|
|
|
export default {
|
|
ChildProcess,
|
|
normalizeSpawnArguments,
|
|
stdioStringToArray,
|
|
spawnSync,
|
|
setupChannel,
|
|
};
|