mirror of
https://github.com/denoland/deno.git
synced 2025-10-02 23:24:37 +00:00

This PR updates `op_node_call_is_from_dependency` op, which now detects node_modules path on windows correctly. This PR also updates the handling of `child_process.spawn(process.execPath, ["-p", souceCode])`. It runs the given string as source code, and prints the last evaluated value. This PR adds wrapper for simulating that behavior.
1535 lines
39 KiB
TypeScript
1535 lines
39 KiB
TypeScript
// Copyright 2018-2025 the Deno authors. 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";
|
|
import { StreamBase } from "ext:deno_node/internal_binding/stream_wrap.ts";
|
|
import { Pipe, socketType } from "ext:deno_node/internal_binding/pipe_wrap.ts";
|
|
import { Socket } from "node:net";
|
|
import {
|
|
kDetached,
|
|
kExtraStdio,
|
|
kInputOption,
|
|
kIpc,
|
|
kNeedsNpmProcessState,
|
|
} from "ext:deno_process/40_process.js";
|
|
|
|
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;
|
|
}
|
|
|
|
const kClosesNeeded = Symbol("_closesNeeded");
|
|
const kClosesReceived = Symbol("_closesReceived");
|
|
const kCanDisconnect = Symbol("_canDisconnect");
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
function flushStdio(subprocess: ChildProcess) {
|
|
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;
|
|
}
|
|
stream.resume();
|
|
}
|
|
}
|
|
|
|
// Wraps a resource in a class that implements
|
|
// StreamBase, so it can be used with node streams
|
|
class StreamResource implements StreamBase {
|
|
#rid: number;
|
|
constructor(rid: number) {
|
|
this.#rid = rid;
|
|
}
|
|
close(): void {
|
|
core.close(this.#rid);
|
|
}
|
|
async read(p: Uint8Array): Promise<number | null> {
|
|
const readPromise = core.read(this.#rid, p);
|
|
core.unrefOpPromise(readPromise);
|
|
const nread = await readPromise;
|
|
return nread > 0 ? nread : null;
|
|
}
|
|
ref(): void {
|
|
return;
|
|
}
|
|
unref(): void {
|
|
return;
|
|
}
|
|
write(p: Uint8Array): Promise<number> {
|
|
return core.write(this.#rid, p);
|
|
}
|
|
}
|
|
|
|
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<void>();
|
|
[kClosesNeeded] = 1;
|
|
[kClosesReceived] = 0;
|
|
[kCanDisconnect] = false;
|
|
|
|
constructor(
|
|
command: string,
|
|
args?: string[],
|
|
options?: ChildProcessOptions,
|
|
) {
|
|
super();
|
|
|
|
const {
|
|
env = {},
|
|
stdio = ["pipe", "pipe", "pipe"],
|
|
cwd,
|
|
shell = false,
|
|
signal,
|
|
windowsVerbatimArguments = false,
|
|
detached,
|
|
} = options || {};
|
|
const normalizedStdio = normalizeStdioOption(stdio);
|
|
const [
|
|
stdin = "pipe",
|
|
stdout = "pipe",
|
|
stderr = "pipe",
|
|
...extraStdio
|
|
] = normalizedStdio;
|
|
const [cmd, cmdArgs] = buildCommand(
|
|
command,
|
|
args || [],
|
|
shell,
|
|
env,
|
|
);
|
|
this.spawnfile = cmd;
|
|
this.spawnargs = [cmd, ...cmdArgs];
|
|
|
|
const ipc = normalizedStdio.indexOf("ipc");
|
|
|
|
const extraStdioOffset = 3; // stdin, stdout, stderr
|
|
|
|
const extraStdioNormalized: DenoStdio[] = [];
|
|
for (let i = 0; i < extraStdio.length; i++) {
|
|
const fd = i + extraStdioOffset;
|
|
if (fd === ipc) extraStdioNormalized.push("null");
|
|
extraStdioNormalized.push(toDenoStdio(extraStdio[i]));
|
|
}
|
|
|
|
const stringEnv = mapValues(env, (value) => value.toString());
|
|
try {
|
|
this.#process = new Deno.Command(cmd, {
|
|
args: cmdArgs,
|
|
clearEnv: true,
|
|
cwd,
|
|
env: stringEnv,
|
|
stdin: toDenoStdio(stdin),
|
|
stdout: toDenoStdio(stdout),
|
|
stderr: toDenoStdio(stderr),
|
|
windowsRawArguments: windowsVerbatimArguments,
|
|
[kIpc]: ipc, // internal
|
|
[kExtraStdio]: extraStdioNormalized,
|
|
[kDetached]: detached,
|
|
// deno-lint-ignore no-explicit-any
|
|
[kNeedsNpmProcessState]: (options ?? {} as any)[kNeedsNpmProcessState],
|
|
}).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);
|
|
});
|
|
}
|
|
|
|
this.stdio[0] = this.stdin;
|
|
this.stdio[1] = this.stdout;
|
|
this.stdio[2] = this.stderr;
|
|
|
|
if (ipc >= 0) {
|
|
this.stdio[ipc] = null;
|
|
}
|
|
|
|
const pipeRids = internals.getExtraPipeRids(this.#process);
|
|
for (let i = 0; i < pipeRids.length; i++) {
|
|
const rid: number | null = pipeRids[i];
|
|
const fd = i + extraStdioOffset;
|
|
if (rid) {
|
|
this[kClosesNeeded]++;
|
|
this.stdio[fd] = new Socket(
|
|
{
|
|
handle: new Pipe(
|
|
socketType.IPC,
|
|
new StreamResource(rid),
|
|
),
|
|
// deno-lint-ignore no-explicit-any
|
|
} as any,
|
|
);
|
|
this.stdio[fd]?.on("close", () => {
|
|
maybeClose(this);
|
|
});
|
|
}
|
|
}
|
|
|
|
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 pipeRid = internals.getIpcPipeRid(this.#process);
|
|
if (typeof pipeRid == "number") {
|
|
setupChannel(this, pipeRid);
|
|
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);
|
|
nextTick(flushStdio, 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[kCanDisconnect]) {
|
|
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<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",
|
|
"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<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;
|
|
|
|
/**
|
|
* Whether to spawn the process in a detached state.
|
|
*/
|
|
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"];
|
|
}
|
|
|
|
// `[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,
|
|
env,
|
|
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,
|
|
env: Record<string, string | number | boolean>,
|
|
): [string, string[]] {
|
|
if (file === Deno.execPath()) {
|
|
let nodeOptions: string[];
|
|
// The user is trying to spawn another Deno process as Node.js.
|
|
[args, nodeOptions] = toDenoArgs(args);
|
|
|
|
// Update NODE_OPTIONS if it exists
|
|
if (nodeOptions.length > 0) {
|
|
const options = nodeOptions.join(" ");
|
|
if (env.NODE_OPTIONS) {
|
|
env.NODE_OPTIONS += " " + options;
|
|
} else {
|
|
env.NODE_OPTIONS = options;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
function normalizeInput(input: unknown) {
|
|
if (input == null) {
|
|
return null;
|
|
}
|
|
if (typeof input === "string") {
|
|
return Buffer.from(input);
|
|
}
|
|
if (input instanceof Uint8Array) {
|
|
return input;
|
|
}
|
|
if (input instanceof DataView) {
|
|
return Buffer.from(input.buffer, input.byteOffset, input.byteLength);
|
|
}
|
|
throw new ERR_INVALID_ARG_TYPE("input", [
|
|
"string",
|
|
"Buffer",
|
|
"TypedArray",
|
|
"DataView",
|
|
], input);
|
|
}
|
|
|
|
export function spawnSync(
|
|
command: string,
|
|
args: string[],
|
|
options: SpawnSyncOptions,
|
|
): SpawnSyncResult {
|
|
const {
|
|
env = Deno.env.toObject(),
|
|
input,
|
|
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, env);
|
|
const input_ = normalizeInput(input);
|
|
|
|
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,
|
|
[kInputOption]: input_,
|
|
}).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",
|
|
"cache",
|
|
"check",
|
|
"compile",
|
|
"completions",
|
|
"coverage",
|
|
"doc",
|
|
"eval",
|
|
"fmt",
|
|
"help",
|
|
"info",
|
|
"init",
|
|
"install",
|
|
"lint",
|
|
"lsp",
|
|
"publish",
|
|
"repl",
|
|
"run",
|
|
"tasks",
|
|
"test",
|
|
"types",
|
|
"uninstall",
|
|
"upgrade",
|
|
"vendor",
|
|
]);
|
|
|
|
/** Wraps the script for (Node.js) --eval / --print argument
|
|
* Note: Builtin modules are available as global variables */
|
|
function wrapScriptForEval(sourceCode: string): string {
|
|
// Note: We need vm.runInThisContext call here to get the last evaluated
|
|
// value of the source with multiple statements. `deno eval -p` surrounds
|
|
// the source code like `console.log(${source})`, and it only allows a
|
|
// single expression.
|
|
return `
|
|
process.getBuiltinModule("module").builtinModules
|
|
.filter((m) => !/\\/|crypto|process/.test(m))
|
|
.forEach((m) => { globalThis[m] = process.getBuiltinModule(m); }),
|
|
vm.runInThisContext(${JSON.stringify(sourceCode)})
|
|
`;
|
|
}
|
|
|
|
/** Returns deno args and NODE_OPTIONS for simulating Node.js cli */
|
|
function toDenoArgs(args: string[]): [string[], string[]] {
|
|
if (args.length === 0) {
|
|
return [args, args];
|
|
}
|
|
|
|
// Update this logic as more CLI arguments are mapped from Node to Deno.
|
|
const denoArgs: string[] = [];
|
|
const nodeOptions: 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) {
|
|
if (arg === "--no-warnings") {
|
|
denoArgs.push("--quiet");
|
|
nodeOptions.push(arg);
|
|
} else if (arg === "--expose-internals") {
|
|
// internals are always exposed in Deno.
|
|
} else if (arg === "--permission") {
|
|
// ignore --permission flag
|
|
} else if (arg === "--pending-deprecation") {
|
|
nodeOptions.push(arg);
|
|
} else {
|
|
// 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", wrapScriptForEval(flagValue));
|
|
useRunArgs = false;
|
|
} else if (flag === "-p" || flag === "--print") {
|
|
denoArgs.push("eval", "-p", wrapScriptForEval(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, nodeOptions];
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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") {
|
|
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) {
|
|
nextTick(callback, null);
|
|
}
|
|
}, (err: Error) => {
|
|
control.unrefCounted();
|
|
if (err instanceof Deno.errors.Interrupted) {
|
|
// Channel closed on us mid-write.
|
|
} else {
|
|
if (typeof callback === "function") {
|
|
nextTick(callback, err);
|
|
} else {
|
|
nextTick(() => target.emit("error", err));
|
|
}
|
|
}
|
|
});
|
|
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[kCanDisconnect] = false;
|
|
control[kControlDisconnect]();
|
|
nextTick(() => {
|
|
target.channel = null;
|
|
core.close(ipc);
|
|
target.emit("disconnect");
|
|
});
|
|
};
|
|
target[kCanDisconnect] = true;
|
|
|
|
// Start reading messages from the channel.
|
|
readLoop();
|
|
|
|
return control;
|
|
}
|
|
|
|
export default {
|
|
ChildProcess,
|
|
normalizeSpawnArguments,
|
|
stdioStringToArray,
|
|
spawnSync,
|
|
setupChannel,
|
|
};
|