const events = require('events'); const net = require('net'); const process = require('process'); class dEvents { } // Initialized Event //initialize = "initialized"; // Stopped Event // stopOnStep = "stop on step"; // stopOnBreakpoint = "stop on breakpoint"; // including: breakpoint, function breakpoint, data breakpoint // stopOnException = "stop on exception"; // stopOnPause = "stop on pause"; // stopOnEntry = "stop on entry"; // stopOnGoto = "stop on goto"; // don't support?? // gather to one, call by emit(dEvent.stop, 'step') dEvents.stopped = "stopped"; // Continued Event dEvents.continued = "continued"; // Exited Event // no implementation in vscode-debugadapter //exited = "exited"; // Terminated Event dEvents.terminated = "terminated"; // Thread Event //thread = "thread"; // don't support // Output event dEvents.output = "output"; // stdoutOutput = "stdout output"; // stderrOutput = "stderr output"; // telemetryOutput = "telemetry output"; // Breakpoint Event // breakpointChanged = "breakpoint changed"; // breakpointNew = "breakpoint new"; // breakpointRemoved = "breakpoint removed"; // gather to one, call by emit(dEvent.breakpoint, 'changed') dEvents.breakpoint = "breakpoint"; // Module Event // don't support // LoadedSource Event // don't support // sourceNew = "source new"; // sourceChanged = "source changed"; // sourceRemoved = "source removed"; // gather to one, call by emit(dEvent.source, 'new') //source = "source"; // Process Event // don't support // The logical name of the process. This is usually the full path to process's executable file. Example: /home/example/myproj/program.js. //process = "process"; // custom event dEvents.end = "end"; class dCommand { } dCommand.initialize = "initialize"; dCommand.launch = "launch"; dCommand.attach = "attach"; dCommand.disconnect = "disconnect"; dCommand.terminate = "terminate"; dCommand.restart = "restart"; dCommand.setbreakpoints = "setBreakpoints"; dCommand.setfunctionbreakpoint = "setFunctionBreakpoints"; dCommand.configurationdone = "configurationDone"; dCommand.continue = "continue"; dCommand.next = "next"; dCommand.stepin = "stepIn"; dCommand.stepout = "stepOut"; // how could this possible ??? //stepback = "stepBack"; // reversecontinue = "reverseContinue"; // restartframe = "restartFrame"; // goto = "goto"; dCommand.pause = "pause"; dCommand.stackstrace = "stackTrace"; // what it mean??? dCommand.scopes = "scopes"; dCommand.variables = "variables"; // we don't support this action at this moment dCommand.setvariable = "setVariable"; // ???? dCommand.setexpression = "setExpression"; dCommand.source = "source"; dCommand.threads = "threads"; dCommand.terminatethreads = "terminateThreads"; dCommand.evaluate = "evaluate"; dCommand.stepintargets = "stepInTargets"; dCommand.complitions = "complitions"; dCommand.exceptioninfo = "exceptionInfo"; dCommand.loadedsources = "loadedSources"; dCommand.databreakpointinfo = "dataBreakpointInfo"; dCommand.setdatabreakpoints = "setDataBreakpoints"; // custom request... /// runtime arguments is pass by `vscode-debugadapter.DebuggSession.launchRequest` /// we can rewrite this function to get arguments. class topjsRuntime extends events.EventEmitter { /** * runtime is the debugger frontend which receive event and request * from debugg adapter and forward to debugger backend vice-versa. * * actually, there's only one tcp connection, which is for communicating * to debugger backend, for communicating with debugger adapter is happened * in memory. the runtime simply read request/event from adapter and send * to backend without any modification. * * messages all contain `type`, `seq` fileds. * for reqeusts, contain extra `command` and optional `aguments` fileds * for events, contain extra `event` and optional `body` fields * for responses, contain extra `request_seq`, `success`, `command` and optional `message`, `body` fields * * NOTE: sequence of meesage is a pitfall. */ constructor() { super(); this._seq = 0; this._cb = {}; this._data = []; this._sep = '\r\n\r\n'; this._sep_len = 4; this._started = false; } setLogger(l) { this._logger = l; } buildRequest(cmd, arg) { const data = JSON.stringify({ seq: this._seq, type: 'request', command: cmd, arguments: arg }, null, '\t'); return `Content-Length: ${data.length}\r\n\r\n${data}`; } /** * decode data into array, if data is not complete, save to this._data * this method assume that the sequence of data arrived is same to been sent. */ decodeResponse(buf) { let offset = 0; let res = []; while(true) { let idx = buf.indexOf(this._sep, offset); if(idx == 0) { throw 'invalid message'; } if(idx < 0) { this._data.push(buf.slice(offset)); break; // read more } let p = buf.slice(offset, idx).toString(); if(p.indexOf(':') < 0) { throw 'invalid message'; } let header = p.split(':'); if(header.length != 2) { throw 'invalid message'; } if(header[0].trim() !== 'Content-Length') { throw 'invalid message'; } let len = parseInt(header[1].trim()); if(Number.isNaN(len)) { throw 'invalid content-length'; } let end = idx + this._sep_len + len; let body = buf.slice(idx + this._sep_len, end); if(body.length < len) { this._data.push(buf.slice(offset)); break; // read more } offset = end; res.push(JSON.parse(body.toString())); } return res; } isStarted() { return this._started; } // args is active configuration in launch.json, the default value is set in package.json start(args) { if(!args.port) { this._logger.err('no port applied, exit'); process.exit(1); } //const remote = "192.168.2.145"; const local = "127.0.0.1"; this.client = net.createConnection({ port: args.port, host: local }); this.client.on('connect', () => { this._started = true; setImmediate(_ => { this.emit('connected', undefined); }); }); this.client.on('data', (data) => { this.onData(data); }); this.client.on('end', () => { //this._logger.log('end'); }); this.client.on('error', (err) => { // most of time, read error indecate backend closed connection. if(err.message.indexOf('read') < 0) { this._logger.err(err.message); } try { this.client.disconnect(); } catch(e) { } }); this.client.on('close', (e) => { // extension is prevented to call exit //process.exit(0); }); } write(data) { this.client.write(data); return this._seq++; } handleResponse(data) { if(data.seq in this._cb) { this._cb[data.seq](data); delete this._cb[data.seq]; } } handleEvent(data) { setImmediate(_ => { this.emit(data.event, data) }); } request(cmd, args, response, cb) { const data = this.buildRequest(cmd, args); const id = this.write(data); this._cb[id] = (json) => { cb(response, json); }; } onData(data) { this._data.push(data); let buf = Buffer.concat(this._data); this._data.splice(0, this._data.length); try { const r = this.decodeResponse(buf); for (var i = 0; i < r.length; ++i) { const j = r[i]; if (j.type === 'event') { this.handleEvent(j); } else if (j.type === 'response') { this.handleResponse(j); } else { this._logger.err(`unknown response ${data}`); } } } catch(e) { this._logger.err('exception: ', e); } } quit() { try { this.client.destroy('close'); } catch(e) { //this._logger.log(`disconnect: ${e}`); } this._started = false; } setBreakpointsRequest(args, response, cb) { this.request(dCommand.setbreakpoints, args, response, cb); } continueRequest(args, response, cb) { this.request(dCommand.continue, args, response, cb); } pauseRequest(args, response, cb) { this.request(dCommand.pause, args, response, cb); } stepInRequest(args, response, cb) { this.request(dCommand.stepin, args, response, cb); } stepOutRequest(args, response, cb) { this.request(dCommand.stepout, args, response, cb); } nextRequest(args, response, cb) { this.request(dCommand.next, args, response, cb); } variablesRequest(args, response, cb) { this.request(dCommand.variables, args, response, cb); } scopesRequest(args, response, cb) { this.request(dCommand.scopes, args, response, cb); } stackTraceRequest(args, response, cb) { this.request(dCommand.stackstrace, args, response, cb); } // handled in session, since we don't support multi-thread debugging. // threadsRequest(args, response, cb) { // this.request(dCommand.threads, args, response, cb); // } evaluateRequest(args, response, cb) { this.request(dCommand.evaluate, args, response, cb); } } exports.topjsRuntime = topjsRuntime; exports.dEvents = dEvents; exports.dCommand = dCommand;