in packages/core/src/shared/utilities/processUtils.ts [291:411]
public async run(params: ChildProcessRunOptions = {}): Promise<ChildProcessResult> {
if (this.#childProcess) {
throw new Error('process already started')
}
const options = {
collect: true,
waitForStreams: true,
...this.#baseOptions,
...params,
spawnOptions: { ...this.#baseOptions.spawnOptions, ...params.spawnOptions },
}
const { rejectOnError, rejectOnErrorCode, timeout } = options
const args = this.#args.concat(options.extraArgs ?? [])
const debugDetail = this.#log.logLevelEnabled('debug')
? ` (running processes: ${ChildProcess.#runningProcesses.size})`
: ''
this.#log.info(`Command: ${this.toString(options.logging === 'noparams')}${debugDetail}`)
const cleanup = () => {
this.#childProcess?.stdout?.removeAllListeners()
this.#childProcess?.stderr?.removeAllListeners()
}
return new Promise<ChildProcessResult>((resolve, reject) => {
const errorHandler = (error: Error, force = options.useForceStop) => {
this.#processErrors.push(error)
if (!this.stopped) {
this.stop(force)
}
if (rejectOnError) {
if (typeof rejectOnError === 'function') {
reject(rejectOnError(error))
} else {
reject(error)
}
}
}
const paramsContext: RunParameterContext = {
timeout,
logger: this.#log,
stop: this.stop.bind(this),
send: this.send.bind(this),
reportError: (err) => errorHandler(err instanceof Error ? err : new Error(err)),
}
if (timeout && timeout?.completed) {
throw new Error('Timeout token was already completed.')
}
// Async.
// See also crossSpawn.spawnSync().
// Arguments are forwarded[1] to node `child_process` module, see its documentation[2].
// [1] https://github.com/moxystudio/node-cross-spawn/blob/master/index.js
// [2] https://nodejs.org/api/child_process.html
try {
this.#childProcess = crossSpawn.spawn(this.#command, args, options.spawnOptions)
this.#registerLifecycleListeners(this.#childProcess, errorHandler, options)
} catch (err) {
return reject(err)
}
// Emitted whenever:
// 1. Process could not be spawned, or
// 2. Process could not be killed, or
// 3. Sending a message to the child process failed.
// https://nodejs.org/api/child_process.html#child_process_class_childprocess
// We also register error event handlers on the output/error streams in case a lower level library fails
this.#childProcess.on('error', errorHandler)
this.#childProcess.stdout?.on('error', errorHandler)
this.#childProcess.stderr?.on('error', errorHandler)
this.#childProcess.stdout?.on('data', (data: { toString(): string }) => {
if (options.collect) {
this.#stdoutChunks.push(data.toString())
}
options.onStdout?.(data.toString(), paramsContext)
})
this.#childProcess.stderr?.on('data', (data: { toString(): string }) => {
if (options.collect) {
this.#stderrChunks.push(data.toString())
}
options.onStderr?.(data.toString(), paramsContext)
})
// Emitted when streams are closed.
// This will not be fired if `waitForStreams` is false
this.#childProcess.once('close', (code, signal) => {
this.#processResult = this.#makeResult(code ?? -1, signal ?? undefined)
resolve(this.#processResult)
})
// Emitted when process exits or terminates.
// https://nodejs.org/api/child_process.html#child_process_class_childprocess
// - If the process exited, `code` is the final exit code of the process, else null.
// - If the process terminated because of a signal, `signal` is the name of the signal, else null.
// - One of `code` or `signal` will always be non-null.
this.#childProcess.once('exit', (code, signal) => {
this.#processResult = this.#makeResult(
typeof code === 'number' ? code : -1,
typeof signal === 'string' ? signal : undefined
)
if (code && rejectOnErrorCode) {
if (typeof rejectOnErrorCode === 'function') {
reject(rejectOnErrorCode(code))
} else {
reject(new Error(`Command exited with non-zero code (${code}): ${this.toString()}`))
}
}
if (options.waitForStreams === false) {
resolve(this.#processResult)
}
})
}).finally(() => cleanup())
}