import { Log , LogSocketCommands , LogSocket } from '../../log' ; import { Socket } from 'net' ; import { Settings } from '../../settings' ; import { Utility } from '../../misc/utility' ; const CONNECTION_TIMEOUT = 1000 ; const QUIT_TIMEOUT = 800 ; export const NO_TIMEOUT = 0 ; /**
 * A command send to Zesarux debugger as it is being put in the queue.
 */ class CommandEntry { public command : string | undefined ; public handler : ( data : string ) => void ; public suppressErrorHandling : boolean ; public timeout : number ; constructor ( command , handler , suppressErrorHandling , timeout ) { this . command = command ; this . handler = handler ; this . suppressErrorHandling = suppressErrorHandling ; this . timeout = timeout ; } } /**
 * The socket state.
 */ enum SocketState { UNCONNECTED , CONNECTING , CONNECTED_WAITING_ON_WELCOME_MSG , CONNECTED } /**
 * A socket to communicate with the Zesarux debugger.
 * Defines a queue that guarantees that each command is send one-by-one.
 */ export class ZesaruxSocket extends Socket { protected state : SocketState ; private queue : Array < CommandEntry > ; private lastCallQueue : Array < ( ) => void > ; public zesaruxState : string ; private receivedDataChunk : string ; private interruptableRunCmd : CommandEntry | undefined ; private interruptableRunCmdCriticalPhase : boolean ; protected MSG_TIMEOUT : number ; /**
     * Static init method. Creates a new socket object.
     * Used in the launchRequest.
     */ public static Init ( ) { zSocket = new ZesaruxSocket ( ) ; zSocket . init ( ) ; } /**
     * Initialize the socket.
     */ protected init ( ) { this . unref ( ) ; this . myRemoveAllListeners ( ) ; this . MSG_TIMEOUT = Settings . launch . zrcp . socketTimeout * 1000 ; this . receivedDataChunk = '' ; this . state = SocketState . UNCONNECTED ; this . queue = new Array < CommandEntry > ( ) ; this . lastCallQueue = new Array < ( ) => void > ( ) ; this . zesaruxState = 'unknown' ; this . interruptableRunCmd = undefined ; const cEntry = new CommandEntry ( 'connected' , data => { this . state = SocketState . CONNECTED ; LogSocket . log ( 'First text from ZEsarUX received!' ) ; this . emit ( 'connected' ) ; } , false , 0 ) ; this . queue . push ( cEntry ) ; this . emitQueueChanged ( ) ; } /**
    Connects to the Zesarux debug port and initializes it.
    hostname: The IP address, e.g. localhost
    port: The ZRCP port (usually 10000)
    */ public connectDebugger ( ) { this . state = SocketState . CONNECTING ; this . on ( 'data' , data => { this . receiveSocket ( data ) ; } ) ; this . on ( 'close' , ( ) => { LogSocket . log ( 'Socket close: disconnected from server' ) ; this . state = SocketState . UNCONNECTED ; } ) ; this . on ( 'error' , err => { LogSocket . log ( 'Socket: ' + err ) ; this . state = SocketState . UNCONNECTED ; } ) ; this . on ( 'timeout' , ( ) => { switch ( this . state ) { case SocketState . CONNECTING : { const err = new Error ( 'Connection timeout!' ) ; LogSocket . log ( 'Socket timeout: ' + err ) ; this . emit ( 'error' , err ) ; } break ; case SocketState . CONNECTED_WAITING_ON_WELCOME_MSG : { const err = new Error ( 'Connected ZEsarUX, but ZEsarUX does not communicate!' ) ; LogSocket . log ( 'ZEsarUX does not communicate: ' + err ) ; this . emit ( 'error' , err ) ; } break ; case SocketState . CONNECTED : { const err = new Error ( 'ZEsarUX did not answer in time!' ) ; LogSocket . log ( 'ZEsarUX did not answer in time: ' + err ) ; this . emit ( 'error' , err ) ; } break ; } } ) ; this . on ( 'end' , ( ) => { this . state = SocketState . UNCONNECTED ; LogSocket . log ( 'Socket end: disconnected from server' ) ; } ) ; this . setTimeout ( CONNECTION_TIMEOUT ) ; const port = Settings . launch . zrcp . port ; const hostname = Settings . launch . zrcp . hostname ; this . connect ( port , hostname , ( ) => { this . setTimeout ( this . MSG_TIMEOUT ) ; this . interruptableRunCmdCriticalPhase = false ; this . state = SocketState . CONNECTED_WAITING_ON_WELCOME_MSG ; LogSocket . log ( 'Socket: Connected to zesarux server!' ) ; } ) ; } /**
     * Checks if the queue is empty.
     * Calls the lastCallQueue handlers.
     */ private checkLastCommandCompleted ( ) { while ( true ) { if ( this . queue . length != 0 ) return ; const handler = this . lastCallQueue . shift ( ) ; if ( ! handler ) break ; handler ( ) ; } } param handler public async executeWhenQueueIsEmpty ( ) { if ( this . queue . length > 0 ) { return new Promise < void > ( resolve => { this . lastCallQueue . push ( resolve ) ; } ) ; } } param command param handler param suppressErrorHandling param timeout public send ( command , handler = ( data ) => { } , suppressErrorHandling = false ) { const timeout = this . MSG_TIMEOUT ; const cEntry = new CommandEntry ( command , handler , suppressErrorHandling , timeout ) ; this . queue . push ( cEntry ) ; this . emitQueueChanged ( ) ; if ( ! this . interruptableRunCmdCriticalPhase ) { if ( this . queue . length == 1 ) { if ( this . interruptableRunCmd ) { const cBreak = new CommandEntry ( '' , ( ) => { } , false , this . MSG_TIMEOUT ) ; this . queue . unshift ( cBreak ) ; this . emitQueueChanged ( ) ; } this . sendSocket ( ) ; } } } /**
     * Same send, but this one returns a promise.
     */ public async sendAwait ( command , suppressErrorHandling = false ) { return new Promise < any > ( resolve => { this . send ( command , data => { resolve ( data ) ; } , suppressErrorHandling ) ; } ) ; } param handler public sendInterruptableRunCmd ( handler ) { Utility . assert ( this . interruptableRunCmd == undefined ) ; this . interruptableRunCmd = new CommandEntry ( 'run' , handler , false , NO_TIMEOUT ) ; if ( this . queue . length == 0 ) { this . sendSocketCmd ( this . interruptableRunCmd ) ; } } param cmd private sendSocketCmd ( cmd ) { if ( this . state != SocketState . CONNECTED ) return ; if ( cmd == undefined ) return ; let command = cmd . command + '\n' ; this . log ( '=>' , "'" + cmd . command + "'" ) ; this . setTimeout ( cmd . timeout ) ; if ( cmd == this . interruptableRunCmd ) { this . interruptableRunCmdCriticalPhase = true ; } this . write ( command ) ; if ( LogSocketCommands ) LogSocketCommands . appendLine ( '>' + command + '<' ) ; } /**
     * Sends the oldest command in the queue through the socket.
     */ private sendSocket ( ) { if ( this . state != SocketState . CONNECTED ) return ; if ( this . queue . length == 0 ) return ; let cEntry = this . queue [ 0 ] ; this . sendSocketCmd ( cEntry ) ; } /**
     * Sends a blank string to zesarux. Used to stop zesarux if it is "run"ning.
     */ public sendBlank ( ) { if ( this . state != SocketState . CONNECTED ) return ; this . log ( '=>' , '\n' ) ; this . write ( '\n' ) ; } /**
     * Receives data from the socket.
     */ private receiveSocket ( data ) { const sData = data . toString ( ) ; if ( ! sData ) { LogSocket . log ( 'Error: Received ' + data . length + ' bytes of undefined data!' ) ; return ; } this . log ( '<=' , sData ) ; this . receivedDataChunk += sData ; let p = 0 ; let k ; const lenLog = 5 ; while ( ( k = this . receivedDataChunk . indexOf ( 'log> ' , p ) ) >= 0 ) { p = k ; if ( k > 0 && this . receivedDataChunk . charAt ( k - 1 ) != '\n' ) { p += lenLog ; continue ; } k = this . receivedDataChunk . indexOf ( '\n' , p ) ; if ( k < 0 ) { p += lenLog ; continue ; } const log = this . receivedDataChunk . substr ( p + lenLog , k - p - lenLog ) ; this . emit ( 'log' , log ) ; this . receivedDataChunk = this . receivedDataChunk . substring ( 0 , p ) + this . receivedDataChunk . substring ( k + 1 ) ; } const splitData = this . receivedDataChunk . split ( '\n' ) ; if ( splitData . length < 2 ) return ; if ( this . interruptableRunCmdCriticalPhase ) { this . interruptableRunCmdCriticalPhase = false ; const lineCount = splitData . length ; Utility . assert ( splitData [ 0 ] . startsWith ( "Running until" ) ) ; if ( lineCount > 2 ) { splitData . shift ( ) ; } else { if ( this . queue . length > 0 ) { const cBreak = new CommandEntry ( '' , ( ) => { } , false , this . MSG_TIMEOUT ) ; this . queue . unshift ( cBreak ) ; this . emitQueueChanged ( ) ; this . sendSocket ( ) ; } Utility . assert ( lineCount == 2 ) ; this . receivedDataChunk = splitData [ 1 ] ; return ; } } const lastLine = splitData [ splitData . length - 1 ] ; const bCommand1 = lastLine . startsWith ( 'command' ) ; const bCommand2 = lastLine . endsWith ( '> ' ) ; if ( bCommand1 && bCommand2 ) { this . setTimeout ( 0 ) ; this . receivedDataChunk = '' ; splitData . splice ( splitData . length - 1 , 1 ) ; let concData = splitData . join ( '\n' ) ; this . zesaruxState = lastLine . substring ( 8 ) ; let cEntry = this . queue . shift ( ) ; this . emitQueueChanged ( ) ; const iCmd = this . interruptableRunCmd ; if ( iCmd ) { if ( cEntry == undefined || sData . indexOf ( 'point hit at' ) >= 0 ) { this . interruptableRunCmd = undefined ; this . sendSocket ( ) ; iCmd . handler ( concData ) ; return ; } } this . sendSocket ( ) ; const interCmd = this . interruptableRunCmd ; if ( concData . substring ( 0 , 5 ) . toLowerCase ( ) == 'error' ) { if ( ! cEntry || ! ( cEntry . suppressErrorHandling ) ) { let msg = '' ; if ( cEntry ) msg = cEntry . command + ' => ' ; msg += concData ; this . emit ( 'warning' , msg ) ; } } if ( cEntry != undefined ) cEntry . handler ( concData ) ; this . checkLastCommandCompleted ( ) ; if ( this . queue . length == 0 && interCmd ) { this . sendSocketCmd ( interCmd ) ; } } } /**
     * removeAllListeners is broken in vscode 1.31.1 (Feb/2019).
     * Here is the correct call to remove all listeners if no argument is given.
     * Note: this is not a full replacement.
     * Reproducibility:
     * Start emulator, stop it. If you get a warning "ZEsarUX terminated
     * the connection" the error is still there.
     */ protected myRemoveAllListeners ( ) { const Stream = require ( 'stream' ) ; Stream . prototype . removeAllListeners . apply ( this ) ; } param handler public async quit ( handler = ( ) => { } ) { this . queue . length = 0 ; this . lastCallQueue . length = 0 ; this . myRemoveAllListeners ( ) ; this . on ( 'data' , data => { this . receiveSocket ( data ) ; } ) ; let handlerCalled = false ; const func = ( ) => { if ( handlerCalled ) return ; handlerCalled = true ; zSocket . myRemoveAllListeners ( ) ; handler ( ) ; } ; this . once ( 'error' , ( ) => { LogSocket . log ( 'Socket error (should be close).' ) ; func ( ) ; zSocket . end ( ) ; } ) ; this . once ( 'timeout' , ( ) => { LogSocket . log ( 'Socket timeout (should be close).' ) ; func ( ) ; zSocket . end ( ) ; } ) ; this . once ( 'close' , ( ) => { LogSocket . log ( 'Socket closed. OK.' ) ; this . state = SocketState . UNCONNECTED ; func ( ) ; } ) ; this . once ( 'end' , ( ) => { LogSocket . log ( 'Socket end. OK.' ) ; this . state = SocketState . UNCONNECTED ; func ( ) ; } ) ; if ( this . state == SocketState . CONNECTED ) { LogSocket . log ( 'Quitting:' ) ; this . setTimeout ( QUIT_TIMEOUT ) ; this . send ( '\n' ) ; this . send ( 'cpu-history enabled no' , ( ) => { } , true ) ; this . send ( 'cpu-code-coverage enabled no' , ( ) => { } , true ) ; this . send ( 'extended-stack enabled no' , ( ) => { } , true ) ; this . send ( 'clear-membreakpoints' ) ; this . send ( 'disable-breakpoints' , ( ) => { this . send ( 'quit' ) ; zSocket . destroy ( ) ; } ) ; return ; } this . end ( ) ; if ( this . destroyed ) handler ( ) ; else this . destroy ( ) ; } /**
     * Signals that the queue has changed.
     * Used by the Unit Tests to find out when to start the
     * unit tests.
     */ protected emitQueueChanged ( ) { this . emit ( 'queueChanged' , this . queue . length ) ; } param prefix param text protected log ( prefix , text ) { if ( ! LogSocket . isEnabled ( ) ) return ; prefix += ' ' ; const prefixLen = prefix . length ; const nextPrefix = ' ' . repeat ( prefixLen ) ; if ( text == undefined ) text = "(undefined)" ; const arr = text . split ( '\n' ) ; for ( const line of arr ) { LogSocket . log ( prefix + line ) ; prefix = nextPrefix ; } let globLine = prefix + arr [ 0 ] ; if ( arr . length > 1 ) globLine += ' ...' ; Log . log ( globLine ) ; } } export let zSocket ;