in src/shared/utilities/childProcess.ts [104:225]
public async run(params: ChildProcessRunOptions = {}): Promise<ChildProcessResult> {
if (this.childProcess) {
throw new Error('process already started')
}
this.log.info(`Running: ${this.toString(this.options.logging === 'noparams')}`)
const cleanup = () => {
this.childProcess?.removeAllListeners()
this.childProcess?.stdout?.removeAllListeners()
this.childProcess?.stderr?.removeAllListeners()
}
const mergedOptions = {
...this.options,
...params,
spawnOptions: { ...this.options.spawnOptions, ...params.spawnOptions },
}
const { rejectOnError, rejectOnExit, timeout } = mergedOptions
const args = this.args.concat(mergedOptions.extraArgs ?? [])
// Defaults
mergedOptions.collect ??= true
mergedOptions.waitForStreams ??= true
return new Promise<ChildProcessResult>((resolve, reject) => {
const errorHandler = (error: Error, force = mergedOptions.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),
reportError: err => errorHandler(err instanceof Error ? err : new Error(err)),
}
if (timeout) {
if (timeout?.completed) {
throw new Error('Timeout token was already completed.')
}
timeout.timer.catch(err => errorHandler(err, true))
}
// 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, mergedOptions.spawnOptions)
} catch (err) {
return errorHandler(err as Error)
}
// 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 (mergedOptions.collect) {
this.stdoutChunks.push(data.toString())
}
mergedOptions.onStdout?.(data.toString(), paramsContext)
})
this.childProcess.stderr?.on('data', (data: { toString(): string }) => {
if (mergedOptions.collect) {
this.stderrChunks.push(data.toString())
}
mergedOptions.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, signal)
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 && rejectOnExit) {
if (typeof rejectOnExit === 'function') {
reject(rejectOnExit(code))
} else {
reject(new Error(`Command exited with non-zero code: ${code}`))
}
}
if (mergedOptions.waitForStreams === false) {
resolve(this.processResult)
}
})
}).finally(() => cleanup())
}