Skip to main content
Module

std/node/child_process.ts

Deno standard library
Go to Latest
File
// Copyright 2018-2022 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.htmlimport { ChildProcess, ChildProcessOptions, stdioStringToArray,} from "./internal/child_process.ts";import { validateString } from "./internal/validators.mjs";import { ERR_CHILD_PROCESS_IPC_REQUIRED, ERR_CHILD_PROCESS_STDIO_MAXBUFFER, ERR_INVALID_ARG_VALUE, ERR_OUT_OF_RANGE,} from "./internal/errors.ts";import { getSystemErrorName } from "./util.ts";import { process } from "./process.ts";import { Buffer } from "./buffer.ts";
const MAX_BUFFER = 1024 * 1024;
const denoCompatArgv = [ "run", "--compat", "--unstable", "--no-check", "--allow-all",];/** * Spawns a new Node.js process + fork. * @param modulePath * @param args * @param option * @returns {ChildProcess} */export function fork( modulePath: string, /* args?: string[], options?: ForkOptions*/) { validateString(modulePath, "modulePath");
// Get options and args arguments. let execArgv; let options: SpawnOptions & { execArgv?: string; execPath?: string; silent?: boolean; } = {}; let args: string[] = []; let pos = 1; if (pos < arguments.length && Array.isArray(arguments[pos])) { args = arguments[pos++]; }
if (pos < arguments.length && arguments[pos] == null) { pos++; }
if (pos < arguments.length && arguments[pos] != null) { if (typeof arguments[pos] !== "object") { throw new ERR_INVALID_ARG_VALUE(`arguments[${pos}]`, arguments[pos]); }
options = { ...arguments[pos++] }; }
// Prepare arguments for fork: execArgv = options.execArgv || process.execArgv;
if (execArgv === process.execArgv && process._eval != null) { const index = execArgv.lastIndexOf(process._eval); if (index > 0) { // Remove the -e switch to avoid fork bombing ourselves. execArgv = execArgv.slice(0); execArgv.splice(index - 1, 2); } }
args = [...denoCompatArgv, ...execArgv, modulePath, ...args];
if (typeof options.stdio === "string") { options.stdio = stdioStringToArray(options.stdio, "ipc"); } else if (!Array.isArray(options.stdio)) { // Use a separate fd=3 for the IPC channel. Inherit stdin, stdout, // and stderr from the parent if silent isn't set. options.stdio = stdioStringToArray( options.silent ? "pipe" : "inherit", "ipc", ); } else if (!options.stdio.includes("ipc")) { throw new ERR_CHILD_PROCESS_IPC_REQUIRED("options.stdio"); }
options.execPath = options.execPath || Deno.execPath(); options.shell = false;
return spawn(options.execPath, args, options);}
// deno-lint-ignore no-empty-interfaceinterface SpawnOptions extends ChildProcessOptions {}export function spawn(command: string): ChildProcess;export function spawn(command: string, options: SpawnOptions): ChildProcess;export function spawn(command: string, args: string[]): ChildProcess;export function spawn( command: string, args: string[], options: SpawnOptions,): ChildProcess;/** * Spawns a child process using `command`. */export function spawn( command: string, argsOrOptions?: string[] | SpawnOptions, maybeOptions?: SpawnOptions,): ChildProcess { const args = Array.isArray(argsOrOptions) ? argsOrOptions : []; const options = !Array.isArray(argsOrOptions) && argsOrOptions != null ? argsOrOptions : maybeOptions; return new ChildProcess(command, args, options);}
interface ExecFileOptions extends ChildProcessOptions { encoding?: string; timeout?: number; maxBuffer?: number; killSignal?: string;}interface ChildProcessError extends Error { code?: string | number; killed?: boolean; signal?: string; cmd?: string;}class ExecFileError extends Error implements ChildProcessError { code?: string | number;
constructor(message: string) { super(message); this.code = "UNKNOWN"; }}type ExecFileCallback = ( error: ChildProcessError | null, stdout?: string | Buffer, stderr?: string | Buffer,) => void;export function execFile(file: string): ChildProcess;export function execFile( file: string, callback: ExecFileCallback,): ChildProcess;export function execFile(file: string, args: string[]): ChildProcess;export function execFile( file: string, args: string[], callback: ExecFileCallback,): ChildProcess;export function execFile(file: string, options: ExecFileOptions): ChildProcess;export function execFile( file: string, options: ExecFileOptions, callback: ExecFileCallback,): ChildProcess;export function execFile( file: string, args: string[], options: ExecFileOptions, callback: ExecFileCallback,): ChildProcess;export function execFile( file: string, argsOrOptionsOrCallback?: string[] | ExecFileOptions | ExecFileCallback, optionsOrCallback?: ExecFileOptions | ExecFileCallback, maybeCallback?: ExecFileCallback,): ChildProcess { let args: string[] = []; let options: ExecFileOptions = {}; let callback: ExecFileCallback | undefined;
if (Array.isArray(argsOrOptionsOrCallback)) { args = argsOrOptionsOrCallback; } else if (argsOrOptionsOrCallback instanceof Function) { callback = argsOrOptionsOrCallback; } else if (argsOrOptionsOrCallback) { options = argsOrOptionsOrCallback; } if (optionsOrCallback instanceof Function) { callback = optionsOrCallback; } else if (optionsOrCallback) { options = optionsOrCallback; callback = maybeCallback; }
const execOptions = { encoding: "utf8", timeout: 0, maxBuffer: MAX_BUFFER, killSignal: "SIGTERM", ...options, }; if (!Number.isInteger(execOptions.timeout) || execOptions.timeout < 0) { // In Node source, the first argument to error constructor is "timeout" instead of "options.timeout". // timeout is indeed a member of options object. throw new ERR_OUT_OF_RANGE( "timeout", "an unsigned integer", execOptions.timeout, ); } if (execOptions.maxBuffer < 0) { throw new ERR_OUT_OF_RANGE( "options.maxBuffer", "a positive number", execOptions.maxBuffer, ); } const spawnOptions = { shell: false, ...options, };
const child = spawn(file, args, spawnOptions);
let encoding: string | null; const _stdout: Uint8Array[] = []; const _stderr: Uint8Array[] = []; if ( execOptions.encoding !== "buffer" && Buffer.isEncoding(execOptions.encoding) ) { encoding = execOptions.encoding; } else { encoding = null; } let stdoutLen = 0; let stderrLen = 0; let exited = false; let timeoutId: number | null;
let ex: ChildProcessError | null = null;
let cmd = file;
function exithandler(code = 0, signal?: string) { if (exited) return; exited = true;
if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; }
if (!callback) return;
// merge chunks let stdout; let stderr; if ( encoding || ( child.stdout && child.stdout.readableEncoding ) ) { stdout = _stdout.join(""); } else { stdout = Buffer.concat(_stdout); } if ( encoding || ( child.stderr && child.stderr.readableEncoding ) ) { stderr = _stderr.join(""); } else { stderr = Buffer.concat(_stderr); }
if (!ex && code === 0 && signal === null) { callback(null, stdout, stderr); return; }
if (args?.length) { cmd += ` ${args.join(" ")}`; }
if (!ex) { ex = new ExecFileError( "Command failed: " + cmd + "\n" + stderr, ); ex.code = code < 0 ? getSystemErrorName(code) : code; ex.killed = child.killed; ex.signal = signal; }
ex.cmd = cmd; callback(ex, stdout, stderr); }
function errorhandler(e: ExecFileError) { ex = e;
if (child.stdout) { child.stdout.destroy(); }
if (child.stderr) { child.stderr.destroy(); }
exithandler(); }
function kill() { if (child.stdout) { child.stdout.destroy(); }
if (child.stderr) { child.stderr.destroy(); }
try { child.kill(/** TODO use execOptions.killSignal */); } catch (e) { if (e) { ex = e as ChildProcessError; } exithandler(); } }
if (execOptions.timeout > 0) { timeoutId = setTimeout(function delayedKill() { kill(); timeoutId = null; }, execOptions.timeout); }
if (child.stdout) { if (encoding) { child.stdout.setEncoding(encoding); }
child.stdout.on("data", function onChildStdout(chunk: string | Buffer) { const encoding = child.stdout?.readableEncoding;
let chunkBuffer: Buffer; if (Buffer.isBuffer(chunk)) { chunkBuffer = chunk; } else { if (encoding) { chunkBuffer = Buffer.from(chunk as string, encoding); } else { // TODO choose what to do if encoding is not set but chunk is a string (should not happen) chunkBuffer = Buffer.from(""); } }
const length = chunkBuffer.length; stdoutLen += length;
if (stdoutLen > execOptions.maxBuffer) { const truncatedLen = execOptions.maxBuffer - (stdoutLen - length); const truncatedSlice = chunkBuffer.slice(0, truncatedLen).valueOf(); _stdout.push(truncatedSlice);
ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER("stdout"); kill(); } else { _stdout.push(chunkBuffer.valueOf()); } }); }
if (child.stderr) { if (encoding) { child.stderr.setEncoding(encoding); }
child.stderr.on("data", function onChildStderr(chunk: string | Buffer) { const encoding = child.stderr?.readableEncoding;
let chunkBuffer: Buffer; if (Buffer.isBuffer(chunk)) { chunkBuffer = chunk; } else { if (encoding) { chunkBuffer = Buffer.from(chunk as string, encoding); } else { // TODO choose what to do if encoding is not set but chunk is a string (should not happen) chunkBuffer = Buffer.from(""); } }
const length = chunkBuffer.length; stderrLen += length;
if (stderrLen > execOptions.maxBuffer) { const truncatedLen = execOptions.maxBuffer - (stderrLen - length); const truncatedSlice = chunkBuffer.slice(0, truncatedLen).valueOf(); _stderr.push(truncatedSlice);
ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER("stderr"); kill(); } else { _stderr.push(chunkBuffer.valueOf()); } }); }
child.addListener("close", exithandler); child.addListener("error", errorhandler);
return child;}
export default { fork, spawn, execFile, ChildProcess };