import { OctaveLogger } from './OctaveLogger' ; import { spawn , ChildProcess } from 'child_process' ; import { ReadLine , createInterface } from 'readline' ; import { EventEmitter } from 'events' ; import * as Constants from './Constants' ; import { functionFromPath } from './Utils/fsutils' ; import { Commands , CommandInterface } from './Commands' ; enum Status { NOT_CONSUMING = 0x00000000 , BEGUN_CONSUMING = 0x00000001 , ENDED_CONSUMING = 0x00000002 } ; enum MatchIndex { FULL_MATCH , STATUS , CMD_NUMBER , LENGTH } ; export class Runtime extends EventEmitter implements CommandInterface { private static readonly PROMPT = /(?:debug|octave:\d+)> / ; private static readonly SEP = '::' ; private static readonly SYNC = Constants . MODULE_NAME ; private static readonly SYNC_REGEX = new RegExp ( `${ Runtime . SYNC }${ Runtime . SEP }(\\d+)${ Runtime . SEP }(\\d+)` ) ; private static readonly TERMINATOR = `${ Runtime . SYNC }${ Runtime . SEP }end` ; private static readonly TERMINATOR_REGEX = new RegExp ( Runtime . TERMINATOR ) ; private _inputHandler = new Array < ( str : string ) => number > ( ) ; private _stderrHandler = new Array < ( str : string ) => boolean > ( ) ; private _commandNumber = 0 ; private _processName : string ; private _process : ChildProcess ; private _processStdout : ReadLine ; private _processStderr : ReadLine ; private _program : string ; private _autoTerminate : boolean ; private _arguments : string [ ] ; private _environment : any ; private _shell : boolean ; private static createCommand ( expression , command ) { const vars = new Array < string > ( MatchIndex . LENGTH ) ; vars [ MatchIndex . CMD_NUMBER ] = `${ Runtime . SEP }${ command }` ; vars [ MatchIndex . STATUS ] = `${ Runtime . SEP }${ Status . BEGUN_CONSUMING }` ; const syncBeginCmd = Runtime . echo ( `${ Runtime . SYNC }${ vars . join ( '' ) }` ) ; vars [ MatchIndex . STATUS ] = `${ Runtime . SEP }${ Status . ENDED_CONSUMING }` ; const syncEndCmd = Runtime . echo ( `${ Runtime . SYNC }${ vars . join ( '' ) }` ) ; return `${ syncBeginCmd }${ expression }\n${ syncEndCmd }` ; } private static split ( str ) { return str . split ( this . PROMPT ) . filter ( ( val ) => val ) ; } private connect ( ) { OctaveLogger . debug ( `Runtime: connecting to '${ this . _processName }'.` ) ; let options = { env : Object . assign ( process . env , this . _environment ) , shell : this . _shell } ; this . _process = spawn ( this . _processName , this . _arguments , options ) ; if ( this . connected ( ) ) { this . _processStdout = createInterface ( { input : this . _process . stdout , terminal : false , crlfDelay : Infinity } ) ; this . _processStderr = createInterface ( { input : this . _process . stderr , terminal : false , crlfDelay : Infinity } ) ; this . _process . on ( 'close' , code => { this . onExit ( code ) ; } ) ; this . _process . on ( 'error' , err => { this . onError ( err ) ; } ) ; this . _processStdout . on ( 'line' , data => { this . onStdout ( data ) ; } ) ; this . _processStderr . on ( 'line' , data => { this . onStderr ( data ) ; } ) ; } } private isConsumed ( data ) { let consumeCallbackResult = Status . NOT_CONSUMING ; while ( this . _inputHandler . length !== 0 ) { consumeCallbackResult = this . _inputHandler [ 0 ] ( data ) ; if ( consumeCallbackResult === Status . ENDED_CONSUMING ) { this . _inputHandler . shift ( ) ; } else { break ; } } return consumeCallbackResult !== Status . NOT_CONSUMING ; } private isSync ( data ) { const match = data . match ( Runtime . SYNC_REGEX ) ; return match !== null ; } private onStdout ( data ) { const lines = Runtime . split ( data ) ; lines . forEach ( line => { if ( ! this . terminated ( line ) && ! this . isConsumed ( line ) && ! this . isSync ( line ) ) { OctaveLogger . log ( line ) ; } } ) ; } private onStderr ( data ) { this . _stderrHandler . some ( callback => { return callback ( data ) ; } ) ; if ( data . startsWith ( Constants . ERROR_EXP ) ) { OctaveLogger . error ( data ) ; } else { OctaveLogger . warn ( data ) ; } } private onExit ( code ) { const msg = `Runtime: ${ this . _processName } exited with code: ${ code }` ; if ( code !== 0 && code != null ) { OctaveLogger . error ( msg ) ; } else { OctaveLogger . debug ( msg ) ; } this . emit ( Constants . eEXIT ) ; } private onError ( err ) { let msg = err . toString ( ) ; if ( ! this . _process . connected ) { msg += `\nCould not connect to '${ this . _processName }'.` ; this . emit ( Constants . eEXIT ) ; } if ( this . _process . killed ) { msg += `\nProcess '${ this . _processName }' was killed.` ; this . emit ( Constants . eEXIT ) ; } OctaveLogger . debug ( msg ) ; if ( this . autoTerminate ( ) ) { this . emit ( Constants . eERROR ) ; } } private terminated ( data ) { if ( data . match ( Runtime . TERMINATOR_REGEX ) !== null ) { OctaveLogger . debug ( `Runtime: program ${ this . _program } exited normally.` ) ; if ( this . autoTerminate ( ) ) { this . emit ( Constants . eEND ) ; } else { this . emit ( Constants . eBREAK ) ; } return true ; } return false ; } public checkForBreaks ( line ) { const BREAK_REGEX = /^stopped in .*? at line \d+.*$/ ; if ( line . match ( BREAK_REGEX ) !== null ) { this . emit ( Constants . eBREAK ) ; return true ; } return false ; } public constructor ( processName , processArguments , processEnvironment , sourceFolders , workingDirectory , autoTerminate , shell ) { super ( ) ; this . _processName = processName ; this . _autoTerminate = autoTerminate ; this . _arguments = processArguments ; this . _environment = processEnvironment ; this . _shell = shell ; this . connect ( ) ; if ( this . connected ( ) ) { Commands . addFolder ( this , sourceFolders ) ; Commands . cwd ( this , workingDirectory ) ; } this . addStderrHandler ( ( line ) => this . checkForBreaks ( line ) ) ; } public connected ( ) { return this . _process . pid !== undefined ; } public disconnect ( ) { OctaveLogger . debug ( "Killing Runtime." ) ; this . execute ( 'quit' ) ; this . _process . kill ( 'SIGKILL' ) ; } public pause ( ) { OctaveLogger . debug ( "Pausing Runtime." ) ; this . _process . kill ( 'SIGINT' ) ; } public start ( program ) { this . _program = program ; const terminator = Runtime . echo ( Runtime . TERMINATOR ) ; const command = functionFromPath ( program ) ; if ( command . length != 0 ) this . execute ( `${ functionFromPath ( program ) };${ terminator }` ) ; else this . execute ( `${ terminator }` ) ; } public addStderrHandler ( callback ) { this . _stderrHandler . push ( callback ) ; } public evaluateAsLine ( expression , callback ) { this . evaluate ( expression , ( output ) => { callback ( output . join ( '\n' ) ) ; } ) ; } public evaluate ( expression , callback ) { let commandNumber = this . _commandNumber ; let lines = [ ] ; let status = Status . NOT_CONSUMING ; this . _inputHandler . push ( ( line ) => { const match = line . match ( Runtime . SYNC_REGEX ) ; if ( match !== null && match . length === MatchIndex . LENGTH ) { const cmdNum = parseInt ( match [ MatchIndex . CMD_NUMBER ] ) ; if ( cmdNum > commandNumber ) { return Status . ENDED_CONSUMING ; } else if ( cmdNum < commandNumber ) { return Status . NOT_CONSUMING ; } status = parseInt ( match [ MatchIndex . STATUS ] ) ; if ( status === Status . ENDED_CONSUMING ) { callback ( lines ) ; OctaveLogger . debug ( lines . join ( '\n' ) ) ; } } else if ( status === Status . BEGUN_CONSUMING ) { lines . push ( line ) ; } return status ; } ) ; this . execute ( Runtime . createCommand ( expression , commandNumber ) ) ; } public execute ( expression ) { OctaveLogger . debug ( `${ this . _processName }:${ this . _commandNumber }> ${ expression }` ) ; this . _process . stdin . write ( `${ expression }\n` ) ; ++ this . _commandNumber ; } public autoTerminate ( ) { return this . _autoTerminate ; } public static echo ( str ) { return ` printf("\\n${ str }\\n");` ; } }