public async run()

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())
    }