import { ChildProcess , spawn } from 'child_process' ; import * as Bromise from 'bluebird' ; import originalSplit = require ( 'split' ) ; import { Result , ResultSpecialValues } from './enums' ; import { defer } from './utils' ; import { IConsole } from './console' ; export interface CmdOptions { rejectOnNonZeroExit : boolean ; silent ? : boolean ; collectLogs : boolean ; prefixer ? : ( basePath : string , pkg : string , line : string ) => string ; pathRewriter ? : ( currentPath : string , line : string ) => string ; doneCriteria ? : string ; path : string ; } const SPLIT_OPTIONS = { trailing : false } ; const SPLIT_MAPPER = ( x ) => x ; const split = ( ) => originalSplit ( /\r?\n/ , SPLIT_MAPPER , SPLIT_OPTIONS as any ) ; export class CmdProcess { private cp ! : ChildProcess ; private _closed = defer < number > ( ) ; private _finished = defer < void > ( ) ; private _exitCode = defer < number > ( ) ; private _cancelled = defer < Result > ( ) ; private doneCriteria ? : RegExp ; /**
     * Finished will return true even if the process hasn't exited, if doneCriteria was found in
     * the output. Useful for watch processes that have initialization.
     *
     * It will also get rejected if there is a non-favorable exit code.
     */ get finished ( ) { return this . _finished . promise ; } /**
     * Exitcode is always resolved with the exit code, never rejected.
     */ get exitCode ( ) { return this . _exitCode . promise ; } get result ( ) { return Bromise . race ( [ this . _exitCode . promise , this . _cancelled . promise ] ) ; } /**
     * ExitError is like exitCode, except it gets rejected when the exit code is nonzero
     */ get exitError ( ) { return this . exitCode . then ( c => { if ( c != 0 ) throw new Error ( '`' + this . cmdString + '` failed with exit code ' + c ) ; } ) ; } get cmdString ( ) { return this . cmd . join ( ' ' ) ; } constructor ( private console , private cmd , private pkgName , private opts ) { this . pkgName = pkgName ; this . opts = opts ; if ( this . opts . doneCriteria ) this . doneCriteria = new RegExp ( this . opts . doneCriteria ) ; } start ( ) { this . _start ( this . cmd ) ; this . cp . once ( 'close' , code => { this . _closed . resolve ( code ) ; this . _exitCode . resolve ( code ) ; } ) ; this . cp . once ( 'exit' , code => this . _exitCode . resolve ( code ) ) ; this . exitCode . then ( code => { if ( code > 0 ) { const msg = '`' + this . cmdString + '` failed with exit code ' + code ; if ( ! this . opts . silent ) this . console . error ( this . autoAugmentLine ( msg ) ) ; if ( this . opts . rejectOnNonZeroExit ) return this . _finished . reject ( new Error ( msg ) ) ; } this . _finished . resolve ( ) ; } ) ; this . _finished . promise . catch ( ( ) => { } ) ; } stop ( ) { if ( this . cp ) { this . cp . removeAllListeners ( 'close' ) ; this . cp . removeAllListeners ( 'exit' ) ; this . cp . kill ( 'SIGINT' ) ; } this . _cancelled . resolve ( ResultSpecialValues . Cancelled ) ; } private autoPrefix ( line ) { return this . opts . prefixer ? this . opts . prefixer ( this . opts . path , this . pkgName , line ) : line ; } private autoPathRewrite ( line ) { return this . opts . pathRewriter ? this . opts . pathRewriter ( this . opts . path , line ) : line ; } private autoAugmentLine ( line ) { line = this . autoPathRewrite ( line ) ; line = this . autoPrefix ( line ) ; return line ; } private _start ( cmd ) { let sh ; let args ; if ( process . platform === 'win32' ) { sh = 'cmd' ; args = [ '/c' ] . concat ( cmd ) ; } else { ; [ sh , ... args ] = cmd ; } this . cmd = cmd ; this . cp = spawn ( sh , args , { cwd : this . opts . path || ( ( process . versions . node < '8.0.0' ? process . cwd : process . cwd ( ) ) as string ) , env : Object . assign ( process . env , process . stdout . isTTY ? { FORCE_COLOR : process . env . FORCE_COLOR || '1' } : { } ) , stdio : this . opts . collectLogs || this . opts . prefixer != null || this . opts . doneCriteria ? 'pipe' : 'inherit' } ) ; if ( this . cp . stdout ) this . cp . stdout . pipe ( split ( ) ) . on ( 'data' , ( line ) => { this . console . log ( this . autoAugmentLine ( line ) ) ; if ( this . doneCriteria && this . doneCriteria . test ( line ) ) this . _finished . resolve ( ) ; } ) ; if ( this . cp . stderr ) this . cp . stderr . pipe ( split ( ) ) . on ( 'data' , ( line ) => { this . console . error ( this . autoAugmentLine ( line ) ) ; if ( this . doneCriteria && this . doneCriteria . test ( line ) ) this . _finished . resolve ( ) ; } ) ; } }