diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2023-02-14 17:38:45 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-14 17:38:45 +0100 |
commit | d47147fb6ad229b1c039aff9d0959b6e281f4df5 (patch) | |
tree | 6e9e790f2b9bc71b5f0c9c7e64b95cae31579d58 /ext/node/polyfills/child_process.ts | |
parent | 1d00bbe47e2ca14e2d2151518e02b2324461a065 (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/child_process.ts')
-rw-r--r-- | ext/node/polyfills/child_process.ts | 832 |
1 files changed, 832 insertions, 0 deletions
diff --git a/ext/node/polyfills/child_process.ts b/ext/node/polyfills/child_process.ts new file mode 100644 index 000000000..06269e025 --- /dev/null +++ b/ext/node/polyfills/child_process.ts @@ -0,0 +1,832 @@ +// 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 { core } from "internal:deno_node/polyfills/_core.ts"; +import { + ChildProcess, + ChildProcessOptions, + normalizeSpawnArguments, + type SpawnOptions, + spawnSync as _spawnSync, + type SpawnSyncOptions, + type SpawnSyncResult, + stdioStringToArray, +} from "internal:deno_node/polyfills/internal/child_process.ts"; +import { + validateAbortSignal, + validateFunction, + validateObject, + validateString, +} from "internal:deno_node/polyfills/internal/validators.mjs"; +import { + ERR_CHILD_PROCESS_IPC_REQUIRED, + ERR_CHILD_PROCESS_STDIO_MAXBUFFER, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_OUT_OF_RANGE, + genericNodeError, +} from "internal:deno_node/polyfills/internal/errors.ts"; +import { + ArrayIsArray, + ArrayPrototypeJoin, + ArrayPrototypePush, + ArrayPrototypeSlice, + ObjectAssign, + StringPrototypeSlice, +} from "internal:deno_node/polyfills/internal/primordials.mjs"; +import { + getSystemErrorName, + promisify, +} from "internal:deno_node/polyfills/util.ts"; +import { createDeferredPromise } from "internal:deno_node/polyfills/internal/util.mjs"; +import { process } from "internal:deno_node/polyfills/process.ts"; +import { Buffer } from "internal:deno_node/polyfills/buffer.ts"; +import { + convertToValidSignal, + kEmptyObject, +} from "internal:deno_node/polyfills/internal/util.mjs"; + +const MAX_BUFFER = 1024 * 1024; + +type ForkOptions = ChildProcessOptions; + +/** + * Spawns a new Node.js process + fork. + * @param modulePath + * @param args + * @param option + * @returns + */ +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); + } + } + + // TODO(bartlomieju): this is incomplete, currently only handling a single + // V8 flag to get Prisma integration running, we should fill this out with + // more + const v8Flags: string[] = []; + if (Array.isArray(execArgv)) { + for (let index = 0; index < execArgv.length; index++) { + const flag = execArgv[index]; + if (flag.startsWith("--max-old-space-size")) { + execArgv.splice(index, 1); + v8Flags.push(flag); + } + } + } + const stringifiedV8Flags: string[] = []; + if (v8Flags.length > 0) { + stringifiedV8Flags.push("--v8-flags=" + v8Flags.join(",")); + } + args = [ + "run", + "--unstable", // TODO(kt3k): Remove when npm: is stable + "--node-modules-dir", + "-A", + ...stringifiedV8Flags, + ...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; + + Object.assign(options.env ??= {}, { + DENO_DONT_USE_INTERNAL_NODE_COMPAT_STATE: core.ops + .op_npm_process_state(), + }); + + return spawn(options.execPath, args, options); +} + +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 : []; + let options = !Array.isArray(argsOrOptions) && argsOrOptions != null + ? argsOrOptions + : maybeOptions as SpawnOptions; + + options = normalizeSpawnArguments(command, args, options); + + validateAbortSignal(options?.signal, "options.signal"); + return new ChildProcess(command, args, options); +} + +function validateTimeout(timeout?: number) { + if (timeout != null && !(Number.isInteger(timeout) && timeout >= 0)) { + throw new ERR_OUT_OF_RANGE("timeout", "an unsigned integer", timeout); + } +} + +function validateMaxBuffer(maxBuffer?: number) { + if ( + maxBuffer != null && + !(typeof maxBuffer === "number" && maxBuffer >= 0) + ) { + throw new ERR_OUT_OF_RANGE( + "options.maxBuffer", + "a positive number", + maxBuffer, + ); + } +} + +function sanitizeKillSignal(killSignal?: string | number) { + if (typeof killSignal === "string" || typeof killSignal === "number") { + return convertToValidSignal(killSignal); + } else if (killSignal != null) { + throw new ERR_INVALID_ARG_TYPE( + "options.killSignal", + ["string", "number"], + killSignal, + ); + } +} + +export function spawnSync( + command: string, + argsOrOptions?: string[] | SpawnSyncOptions, + maybeOptions?: SpawnSyncOptions, +): SpawnSyncResult { + const args = Array.isArray(argsOrOptions) ? argsOrOptions : []; + let options = !Array.isArray(argsOrOptions) && argsOrOptions + ? argsOrOptions + : maybeOptions as SpawnSyncOptions; + + options = { + maxBuffer: MAX_BUFFER, + ...normalizeSpawnArguments(command, args, options), + }; + + // Validate the timeout, if present. + validateTimeout(options.timeout); + + // Validate maxBuffer, if present. + validateMaxBuffer(options.maxBuffer); + + // Validate and translate the kill signal, if present. + sanitizeKillSignal(options.killSignal); + + return _spawnSync(command, args, options); +} + +interface ExecOptions extends + Pick< + ChildProcessOptions, + | "env" + | "signal" + | "uid" + | "gid" + | "windowsHide" + > { + cwd?: string | URL; + encoding?: string; + /** + * Shell to execute the command with. + */ + shell?: string; + timeout?: number; + /** + * Largest amount of data in bytes allowed on stdout or stderr. If exceeded, the child process is terminated and any output is truncated. + */ + maxBuffer?: number; + killSignal?: string | number; +} +type ExecException = ChildProcessError; +type ExecCallback = ( + error: ExecException | null, + stdout?: string | Buffer, + stderr?: string | Buffer, +) => void; +type ExecSyncOptions = SpawnSyncOptions; +type ExecFileSyncOptions = SpawnSyncOptions; +function normalizeExecArgs( + command: string, + optionsOrCallback?: ExecOptions | ExecSyncOptions | ExecCallback, + maybeCallback?: ExecCallback, +) { + let callback: ExecFileCallback | undefined = maybeCallback; + + if (typeof optionsOrCallback === "function") { + callback = optionsOrCallback; + optionsOrCallback = undefined; + } + + // Make a shallow copy so we don't clobber the user's options object. + const options: ExecOptions | ExecSyncOptions = { ...optionsOrCallback }; + options.shell = typeof options.shell === "string" ? options.shell : true; + + return { + file: command, + options: options!, + callback: callback!, + }; +} + +/** + * Spawns a shell executing the given command. + */ +export function exec(command: string): ChildProcess; +export function exec(command: string, options: ExecOptions): ChildProcess; +export function exec(command: string, callback: ExecCallback): ChildProcess; +export function exec( + command: string, + options: ExecOptions, + callback: ExecCallback, +): ChildProcess; +export function exec( + command: string, + optionsOrCallback?: ExecOptions | ExecCallback, + maybeCallback?: ExecCallback, +): ChildProcess { + const opts = normalizeExecArgs(command, optionsOrCallback, maybeCallback); + return execFile(opts.file, opts.options as ExecFileOptions, opts.callback); +} + +interface PromiseWithChild<T> extends Promise<T> { + child: ChildProcess; +} +type ExecOutputForPromisify = { + stdout?: string | Buffer; + stderr?: string | Buffer; +}; +type ExecExceptionForPromisify = ExecException & ExecOutputForPromisify; + +const customPromiseExecFunction = (orig: typeof exec) => { + return (...args: [command: string, options: ExecOptions]) => { + const { promise, resolve, reject } = createDeferredPromise() as unknown as { + promise: PromiseWithChild<ExecOutputForPromisify>; + resolve?: (value: ExecOutputForPromisify) => void; + reject?: (reason?: ExecExceptionForPromisify) => void; + }; + + promise.child = orig(...args, (err, stdout, stderr) => { + if (err !== null) { + const _err: ExecExceptionForPromisify = err; + _err.stdout = stdout; + _err.stderr = stderr; + reject && reject(_err); + } else { + resolve && resolve({ stdout, stderr }); + } + }); + + return promise; + }; +}; + +Object.defineProperty(exec, promisify.custom, { + enumerable: false, + value: customPromiseExecFunction(exec), +}); + +interface ExecFileOptions extends ChildProcessOptions { + encoding?: string; + timeout?: number; + maxBuffer?: number; + killSignal?: string | number; +} +interface ChildProcessError extends Error { + code?: string | number; + killed?: boolean; + signal?: AbortSignal; + 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", + shell: false, + ...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: SpawnOptions = { + cwd: execOptions.cwd, + env: execOptions.env, + gid: execOptions.gid, + shell: execOptions.shell, + signal: execOptions.signal, + uid: execOptions.uid, + windowsHide: !!execOptions.windowsHide, + windowsVerbatimArguments: !!execOptions.windowsVerbatimArguments, + }; + + const child = spawn(file, args, spawnOptions); + + let encoding: string | null; + const _stdout: (string | Uint8Array)[] = []; + const _stderr: (string | Uint8Array)[] = []; + if ( + execOptions.encoding !== "buffer" && Buffer.isEncoding(execOptions.encoding) + ) { + encoding = execOptions.encoding; + } else { + encoding = null; + } + let stdoutLen = 0; + let stderrLen = 0; + let killed = false; + let exited = false; + let timeoutId: number | null; + + let ex: ChildProcessError | null = null; + + let cmd = file; + + function exithandler(code = 0, signal?: AbortSignal) { + 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 as Buffer[]); + } + if ( + encoding || + ( + child.stderr && + child.stderr.readableEncoding + ) + ) { + stderr = _stderr.join(""); + } else { + stderr = Buffer.concat(_stderr as Buffer[]); + } + + 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 || 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(); + } + + killed = true; + try { + child.kill(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) { + // Do not need to count the length + if (execOptions.maxBuffer === Infinity) { + ArrayPrototypePush(_stdout, chunk); + return; + } + + const encoding = child.stdout?.readableEncoding; + const length = encoding + ? Buffer.byteLength(chunk, encoding) + : chunk.length; + const slice = encoding + ? StringPrototypeSlice + : (buf: string | Buffer, ...args: number[]) => buf.slice(...args); + stdoutLen += length; + + if (stdoutLen > execOptions.maxBuffer) { + const truncatedLen = execOptions.maxBuffer - (stdoutLen - length); + ArrayPrototypePush(_stdout, slice(chunk, 0, truncatedLen)); + + ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER("stdout"); + kill(); + } else { + ArrayPrototypePush(_stdout, chunk); + } + }); + } + + if (child.stderr) { + if (encoding) { + child.stderr.setEncoding(encoding); + } + + child.stderr.on("data", function onChildStderr(chunk: string | Buffer) { + // Do not need to count the length + if (execOptions.maxBuffer === Infinity) { + ArrayPrototypePush(_stderr, chunk); + return; + } + + const encoding = child.stderr?.readableEncoding; + const length = encoding + ? Buffer.byteLength(chunk, encoding) + : chunk.length; + const slice = encoding + ? StringPrototypeSlice + : (buf: string | Buffer, ...args: number[]) => buf.slice(...args); + stderrLen += length; + + if (stderrLen > execOptions.maxBuffer) { + const truncatedLen = execOptions.maxBuffer - (stderrLen - length); + ArrayPrototypePush(_stderr, slice(chunk, 0, truncatedLen)); + + ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER("stderr"); + kill(); + } else { + ArrayPrototypePush(_stderr, chunk); + } + }); + } + + child.addListener("close", exithandler); + child.addListener("error", errorhandler); + + return child; +} + +type ExecFileExceptionForPromisify = ExecFileError & ExecOutputForPromisify; + +const customPromiseExecFileFunction = ( + orig: ( + file: string, + argsOrOptionsOrCallback?: string[] | ExecFileOptions | ExecFileCallback, + optionsOrCallback?: ExecFileOptions | ExecFileCallback, + maybeCallback?: ExecFileCallback, + ) => ChildProcess, +) => { + return ( + ...args: [ + file: string, + argsOrOptions?: string[] | ExecFileOptions, + options?: ExecFileOptions, + ] + ) => { + const { promise, resolve, reject } = createDeferredPromise() as unknown as { + promise: PromiseWithChild<ExecOutputForPromisify>; + resolve?: (value: ExecOutputForPromisify) => void; + reject?: (reason?: ExecFileExceptionForPromisify) => void; + }; + + promise.child = orig(...args, (err, stdout, stderr) => { + if (err !== null) { + const _err: ExecFileExceptionForPromisify = err; + _err.stdout = stdout; + _err.stderr = stderr; + reject && reject(_err); + } else { + resolve && resolve({ stdout, stderr }); + } + }); + + return promise; + }; +}; + +Object.defineProperty(execFile, promisify.custom, { + enumerable: false, + value: customPromiseExecFileFunction(execFile), +}); + +function checkExecSyncError( + ret: SpawnSyncResult, + args: string[], + cmd?: string, +) { + let err; + if (ret.error) { + err = ret.error; + ObjectAssign(err, ret); + } else if (ret.status !== 0) { + let msg = "Command failed: "; + msg += cmd || ArrayPrototypeJoin(args, " "); + if (ret.stderr && ret.stderr.length > 0) { + msg += `\n${ret.stderr.toString()}`; + } + err = genericNodeError(msg, ret); + } + return err; +} + +export function execSync(command: string, options: ExecSyncOptions) { + const opts = normalizeExecArgs(command, options); + const inheritStderr = !(opts.options as ExecSyncOptions).stdio; + + const ret = spawnSync(opts.file, opts.options as SpawnSyncOptions); + + if (inheritStderr && ret.stderr) { + process.stderr.write(ret.stderr); + } + + const err = checkExecSyncError(ret, [], command); + + if (err) { + throw err; + } + + return ret.stdout; +} + +function normalizeExecFileArgs( + file: string, + args?: string[] | null | ExecFileSyncOptions | ExecFileCallback, + options?: ExecFileSyncOptions | null | ExecFileCallback, + callback?: ExecFileCallback, +): { + file: string; + args: string[]; + options: ExecFileSyncOptions; + callback?: ExecFileCallback; +} { + if (ArrayIsArray(args)) { + args = ArrayPrototypeSlice(args); + } else if (args != null && typeof args === "object") { + callback = options as ExecFileCallback; + options = args as ExecFileSyncOptions; + args = null; + } else if (typeof args === "function") { + callback = args; + options = null; + args = null; + } + + if (args == null) { + args = []; + } + + if (typeof options === "function") { + callback = options as ExecFileCallback; + } else if (options != null) { + validateObject(options, "options"); + } + + if (options == null) { + options = kEmptyObject; + } + + args = args as string[]; + options = options as ExecFileSyncOptions; + + if (callback != null) { + validateFunction(callback, "callback"); + } + + // Validate argv0, if present. + if (options.argv0 != null) { + validateString(options.argv0, "options.argv0"); + } + + return { file, args, options, callback }; +} + +export function execFileSync(file: string): string | Buffer; +export function execFileSync(file: string, args: string[]): string | Buffer; +export function execFileSync( + file: string, + options: ExecFileSyncOptions, +): string | Buffer; +export function execFileSync( + file: string, + args: string[], + options: ExecFileSyncOptions, +): string | Buffer; +export function execFileSync( + file: string, + args?: string[] | ExecFileSyncOptions, + options?: ExecFileSyncOptions, +): string | Buffer { + ({ file, args, options } = normalizeExecFileArgs(file, args, options)); + + const inheritStderr = !options.stdio; + const ret = spawnSync(file, args, options); + + if (inheritStderr && ret.stderr) { + process.stderr.write(ret.stderr); + } + + const errArgs: string[] = [options.argv0 || file, ...(args as string[])]; + const err = checkExecSyncError(ret, errArgs); + + if (err) { + throw err; + } + + return ret.stdout as string | Buffer; +} + +export default { + fork, + spawn, + exec, + execFile, + execFileSync, + execSync, + ChildProcess, + spawnSync, +}; +export { ChildProcess }; |