UNPKG

promisify-child-process

Version:

seriously like the best async child process library

222 lines (204 loc) 6.72 kB
// @flow import type { ChildProcess } from 'child_process' const child_process = require('child_process') export type ChildProcessOutput = { stdout?: ?(string | Buffer), stderr?: ?(string | Buffer), code?: number | null, signal?: string | null, } export interface ExitReason { code?: number | null; signal?: string | null; } export type ErrorWithOutput = Error & ChildProcessOutput export type ChildProcessPromise = ChildProcess & Promise<ChildProcessOutput> type PromisifyChildProcessBaseOpts = { encoding?: $PropertyType<child_process$spawnSyncOpts, 'encoding'>, killSignal?: $PropertyType<child_process$spawnSyncOpts, 'killSignal'>, maxBuffer?: $PropertyType<child_process$spawnSyncOpts, 'maxBuffer'>, } export type SpawnOpts = child_process$spawnOpts & PromisifyChildProcessBaseOpts export type ForkOpts = child_process$forkOpts & PromisifyChildProcessBaseOpts const bindFinally = <T>(promise: Promise<T>) => ( handler: () => mixed ): Promise<T> => // don't assume we're running in an environment with Promise.finally promise.then( async (value: any): any => { await handler() return value }, async (reason: any): any => { await handler() throw reason } ) function joinChunks( chunks: $ReadOnlyArray<string | Buffer>, encoding: ?string ): string | Buffer { if (chunks[0] instanceof Buffer) { const buffer = Buffer.concat((chunks: any)) if (encoding) return buffer.toString((encoding: any)) return buffer } return chunks.join('') } export function promisifyChildProcess( child: ChildProcess, options: PromisifyChildProcessBaseOpts = {} ): ChildProcessPromise { const _promise = new Promise( ( resolve: (result: ChildProcessOutput) => void, reject: (error: ErrorWithOutput) => void ) => { const { encoding, killSignal } = options const captureStdio = encoding != null || options.maxBuffer != null const maxBuffer = options.maxBuffer != null ? options.maxBuffer : 200 * 1024 let error: ?ErrorWithOutput let bufferSize = 0 const stdoutChunks: Array<string | Buffer> = [] const stderrChunks: Array<string | Buffer> = [] const capture = (chunks: Array<string | Buffer>) => ( data: string | Buffer ) => { const remaining = Math.max(0, maxBuffer - bufferSize) const byteLength = Buffer.byteLength(data, 'utf8') bufferSize += Math.min(remaining, byteLength) if (byteLength > remaining) { error = new Error(`maxBuffer size exceeded`) // $FlowFixMe child.kill(killSignal ? killSignal : 'SIGTERM') data = data.slice(0, remaining) } chunks.push(data) } if (captureStdio) { if (child.stdout) child.stdout.on('data', capture(stdoutChunks)) if (child.stderr) child.stderr.on('data', capture(stderrChunks)) } child.on('error', reject) function done(code: ?number, signal: ?string) { if (!error) { if (code != null && code !== 0) { error = new Error(`Process exited with code ${code}`) } else if (signal != null) { error = new Error(`Process was killed with ${signal}`) } } function defineOutputs(obj: Object) { obj.code = code obj.signal = signal if (captureStdio) { obj.stdout = joinChunks(stdoutChunks, encoding) obj.stderr = joinChunks(stderrChunks, encoding) } else { const warn = (prop: 'stdout' | 'stderr') => ({ configurable: true, enumerable: true, get(): any { /* eslint-disable no-console */ console.error( new Error( `To get ${prop} from a spawned or forked process, set the \`encoding\` or \`maxBuffer\` option` ).stack.replace(/^Error/, 'Warning') ) /* eslint-enable no-console */ return null }, }) Object.defineProperties(obj, { stdout: warn('stdout'), stderr: warn('stderr'), }) } } const finalError: ?ErrorWithOutput = error if (finalError) { defineOutputs(finalError) reject(finalError) } else { const output: ChildProcessOutput = ({}: any) defineOutputs(output) resolve(output) } } child.on('close', done) } ) return (Object.create(child, { then: { value: _promise.then.bind(_promise) }, catch: { value: _promise.catch.bind(_promise) }, finally: { value: bindFinally(_promise), }, }): any) } export function spawn( command: string, args?: Array<string> | child_process$spawnOpts, options?: SpawnOpts ): ChildProcessPromise { return promisifyChildProcess( child_process.spawn(command, args, options), ((Array.isArray(args) ? options : args): any) ) } export function fork( module: string, args?: Array<string> | child_process$forkOpts, options?: ForkOpts ): ChildProcessPromise { return promisifyChildProcess( child_process.fork(module, args, options), ((Array.isArray(args) ? options : args): any) ) } function promisifyExecMethod(method: any): any { return (...args: Array<any>): ChildProcessPromise => { let child: ?ChildProcess const _promise = new Promise( ( resolve: (output: ChildProcessOutput) => void, reject: (error: ErrorWithOutput) => void ) => { child = method( ...args, ( err: ?ErrorWithOutput, stdout: ?(Buffer | string), stderr: ?(Buffer | string) ) => { if (err) { err.stdout = stdout err.stderr = stderr reject(err) } else { resolve({ code: 0, signal: null, stdout, stderr }) } } ) } ) if (!child) { throw new Error('unexpected error: child has not been initialized') } return (Object.create((child: any), { then: { value: _promise.then.bind(_promise) }, catch: { value: _promise.catch.bind(_promise) }, finally: { value: bindFinally(_promise) }, }): any) } } export const exec: ( command: string, options?: child_process$execOpts ) => ChildProcessPromise = promisifyExecMethod(child_process.exec) export const execFile: ( file: string, args?: Array<string> | child_process$execFileOpts, options?: child_process$execOpts ) => ChildProcessPromise = promisifyExecMethod(child_process.execFile)