From 35f22d87a0b26739daf78ddd978da8db89dd872c Mon Sep 17 00:00:00 2001 From: abbycin Date: Thu, 9 May 2019 09:06:50 +0800 Subject: [PATCH] init --- .gitignore | 4 + .../ipch/3c4d88325f9a2662/mmap_address.bin | Bin 0 -> 8 bytes .vscode/launch.json | 25 + .vscode/settings.json | 7 + .vscode/tasks.json | 18 + build.js | 7 + favicon.png | Bin 0 -> 3061 bytes package.json | 117 +++++ src/debugAdapter.js | 2 + src/extension.js | 64 +++ src/topjsDebug.js | 426 ++++++++++++++++++ src/topjsRuntime.js | 334 ++++++++++++++ 12 files changed, 1004 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/ipch/3c4d88325f9a2662/mmap_address.bin create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 build.js create mode 100644 favicon.png create mode 100644 package.json create mode 100644 src/debugAdapter.js create mode 100644 src/extension.js create mode 100644 src/topjsDebug.js create mode 100644 src/topjsRuntime.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9fc93c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.vsix +out +package-lock.json +node_modules diff --git a/.vscode/ipch/3c4d88325f9a2662/mmap_address.bin b/.vscode/ipch/3c4d88325f9a2662/mmap_address.bin new file mode 100644 index 0000000000000000000000000000000000000000..862b8428b9e068428b1a4e8a38f94019008d8940 GIT binary patch literal 8 NcmZQzU~Ksh1ON<}1cCqn literal 0 HcmV?d00001 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..dbce6dd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ] + }, + { + "name": "TopJS Sample", + "type": "topjs", + "request": "launch", + "port": 30992, + "program": "${workspaceFolder}/${command:getProgramName}", + "runtimeExecutable": "topjs3" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1831b90 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +// Place your settings in this file to overwrite default and user settings.FALSE +{ + "javascript.validate.enable": false, + "typescript.tsdk": "node_modules/typescript/lib", + "files.trimTrailingWhitespace": true, + "editor.insertSpaces": true +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..8a491bd --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/build.js b/build.js new file mode 100644 index 0000000..e494dba --- /dev/null +++ b/build.js @@ -0,0 +1,7 @@ +const ncp = require('ncp'); + +ncp('./src', './out', (err) => { + if(err) { + return console.error(err); + } +}); \ No newline at end of file diff --git a/favicon.png b/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..ac6c26d424fefa1093086ac741b882ddfa532c9d GIT binary patch literal 3061 zcmV&+~Z~Eanr; z7{kQ)1WunmjoH~*#9}e5sI0`|a2Rf{2a+UZEy8Z0LKd?>GYkWoreP|cz=H=5(B0jQ zcs!1Ejq6a~&;XaqWi298VU`)$pzAu4$t1eFyK(B|zi|2TWh9eHY~H*X+qP}XlJ6J?oUlbi z;CLP$pBF!R<{4bMdKE1#E$Hj*#g`W^;+eH;v3vJ!G&VQE@Aq4A+O?>ytAm{3G7HToFNB;t2gh;Xc^+P`7hAS$!TR;< z5ekJ6i^UKK1TZ!_iq^Adp=+9>#xj4$Cnxczzx)}!Jw15%@FCLaG$N4*cI?;zug`0_ zP5a&~2qC+coHS=bDI5;tjW^yvU0ofDi;KZ=98^`qr){4inVhz)kQuGb0U2ZX`qEc; z`0ye0#}q}9B)t0bR}rbGfGCIs1Kzw4%ICK;pUbk0HEY%&8jXVEI3$xv^!NAUv(G-m zSC=kkEg;ALGlmNnE+CaoTR|HtMmK1`JxR4~oeoqud zG&MCrRaF>gsBXR%GIw z<2Z{jI?XMM3*}p-VHg+~7(jb_J2Xw3+cZ2s=MGiXx;8)YcpQg6{16q(mS&mReCOP` zb08u_A`vWKz8tb7p}V^qCr+Hm;Yp!T2+uwD9EysH^3rLJE*JcMKl=OoF+M&H02UV+=gcTX;;T(|GyimvQgjJ*%H2NoZ|tMSXoe zc%IME;^Dvj9q+yOM|5^|%~=A#p54F1{@?%3f|l7NNkSwNfh0-RCU(mB#C%D(*&!GV z!teKILCP=;Jb3T`@pwGz{>qgr(cIh&S(bC6DG_1CiYR(}dowi2VdT*xl$Dl2lBFCs zX>RI#5L!?ma}vTB!|mI*K}0AkD?=z0LNFM_md%@UczmY3;^F`(ld*!()!mJ>l7{S( zbHU1#0Gih|BD6T1HK@ID z0*@U#cV?AJ48y?LGiNb9mCSL|Pei8|QubqvA(cv@rKJT!Lqit5kVFwLZhtWg|Blp` z=dpM1UaVZX5S}9gaA1%WP0f*Im#ZoY9Y1!Qf`I^W97h015=Gjw@y9eYc%RbgbXIxB zURJR;e!g-@W=u#V5_tdp_c1&?Z1ESb*NeCJy^WHRlC1KdQ!}mblg*p4>leG=a?NdM z&Cv1nwW~OG;yBXjv}0j6pWGY^WEP-I&8wb2j`r&`J2PWxem)BzpQ+8xnx@h8 z^fX<&c8xY|+C)V~MI;CU1p)!uxpODoxpRk7snioy=5$@B*_m0o_~iwviLE4AmWksy z@_T*s+qZv1-wY3vqA1q*1sC9?rOXASQYq@|>!W@9_E9(-Cb!#76_E;h^Ve@u&+Q(X z$Uo5riMIjH@RdNRYq6P@uSBmF&QUKRkIf0)Pw^E4jjnG z08QMVIdcXrEiE7-)YsQz-&=3tS9|xOyu2KeBw=uH5UErO)6>(qd-rbEekkOX1W`aF z5NnhMj?tK7-KLD z1B#*`olYY;HHAbxfvT!1xV;`nW}B0dIb}O$YR4F}XtZ6JEX(kE=PGK!U=Y!06dfHM zn3 { + var p = vscode.window.activeTextEditor.document.fileName; + return path.basename(p); + })); + const provider = new TopJSConfigurationProvider(); + context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('topjs', provider)); + var x = true; + if(x) { + const factory = new TopJSDebugAdapterDescriptorFactory(); + context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('topjs', factory)); + context.subscriptions.push(factory); + } +} + +function deactivate() {} + +exports.activate = activate; +exports.deactivate = deactivate; + +class TopJSConfigurationProvider { + resolveDebugConfigguration(folder, config, token) { + if(!config.type && !config.request && !config.name) { + const editor = vscode.window.activeTextEditor; + if(editor && editor.document.languageId == 'javascript') { + config.type = 'topjs'; + config.name = 'Launch Debug'; + config.request = 'launch'; + config.program = '${file}'; + config.stopOnEntry = true; + } + } + if(!config.program) { + return vscode.window.showInformationMessage("Cannot find a program to debug").then(_ => { + return undefined; + }); + } + return config; + } +} + +class TopJSDebugAdapterDescriptorFactory { + createDebugAdapterDescriptor(session, executable) { + if(!this.server) { + this.server = net.createServer(socket => { + const session = new topjsDebug.TopJSDebugSession(); + session.setRunAsServer(true); + session.start(socket, socket); + }).listen(0); // listen on random port + } + return new vscode.DebugAdapterServer(this.server.address().port); + } + + dispose() { + if(this.server) { + this.server.close(); + } + } +} \ No newline at end of file diff --git a/src/topjsDebug.js b/src/topjsDebug.js new file mode 100644 index 0000000..f0dab11 --- /dev/null +++ b/src/topjsDebug.js @@ -0,0 +1,426 @@ +const vscode_debugadapter = require('vscode-debugadapter'); +const Path = require('path'); +const topjsRuntime = require('./topjsRuntime'); +const { Subject } = require('await-notify'); +const spawn = require('child_process'); +const process = require('process'); +const fs = require('fs'); +const os = require('os'); +const cmd_exists = require('command-exists').sync; +const vscode = require('vscode'); + +function fold_varg(...args) { + let buf = []; + for(let i = 0; i < args.length; ++i) { + if(typeof args[i] === 'object') { + try { + buf.push(Buffer.from(JSON.stringify(args[i], null, ' '))); + } catch(e) { + buf.push(Buffer.from(''+args[i])); + } + } else { + buf.push(Buffer.from(''+args[i])); + } + } + return Buffer.concat(buf).toString(); +} + +class TopJSDebugSession extends vscode_debugadapter.LoggingDebugSession { + /** + * this class re-write necessary requests of base class, and register + * necessary callback handlers for interacting with debugger backend. + */ + constructor() { + // setup log file path for base class + super(Path.join(os.tmpdir(), "topjs-debug.txt")); + // for variable inspection + this._variablehandles = new vscode_debugadapter.Handles(); + // for async event notify + this._configurationDone = new Subject(); + // source file line start at 0 + this.setDebuggerLinesStartAt1(false); + // source file column start at 0 + this.setDebuggerColumnsStartAt1(false); + + this._cachedBreakpoints = {}; + + // runtime instance for interacting with debugger backend + this._runtime = new topjsRuntime.topjsRuntime(); + this._runtime.setLogger(this); + + // runtime recieved 'stopOnEntry' event from debugger backend, then + // forward to vscode gui by emitting event via debugger adapter + this._runtime.on(topjsRuntime.dEvents.stopped, (data) => { + this.sendEvent(new vscode_debugadapter.StoppedEvent(data.body.reason, TopJSDebugSession.THREAD_ID)); + }); + + this._runtime.on(topjsRuntime.dEvents.continued, (data) => { + this.sendEvent(new vscode_debugadapter.ContinuedEvent(TopJSDebugSession.THREAD_ID)); + }); + + this._runtime.on(topjsRuntime.dEvents.terminated, (data) => { + this.sendEvent(new vscode_debugadapter.TerminatedEvent(false)); // don't restart + }); + + this._runtime.on(topjsRuntime.dEvents.output, (data) => { + const e = new vscode_debugadapter.OutputEvent(`${data.body.output}\n`, data.body.category); + this.sendEvent(e); + }); + + this._runtime.on(topjsRuntime.dEvents.breakpoint, (data) => { + this.sendEvent(new vscode_debugadapter.BreakpointEvent(data.body.reason, { + verified: data.body.breakpoint.verified, + id: data.body.breakpoint.id + })); + }); + + this._runtime.on(topjsRuntime.dEvents.end, (body) => { + this.sendEvent(new vscode_debugadapter.TerminatedEvent()); + }); + + this._runtime.on('connected', () => { + this.setBreakPointsRequest(this._cachedBreakpoints.response, this._cachedBreakpoints.args); + }); + } + + _log_impl(type, ...args) { + const s = fold_varg(...args); + const e = new vscode_debugadapter.OutputEvent(`${s}`, type); + this.sendEvent(e); + } + + log(...args) { + this._log_impl('console', ...args); + } + + info(...args) { + this._log_impl('stdout', ...args); + } + + err(...args) { + this._log_impl('stderr', ...args); + } + + createSource(filepath) { + // backend must return absolute path of debugging script. + if(!fs.existsSync(filepath)) { + if(this._program.indexOf(filepath) >= 0) { + filepath = this._program; + } + } + return new vscode_debugadapter.Source(Path.basename(filepath), this.convertDebuggerPathToClient(filepath), undefined, undefined, 'topjs-adapter-data'); + } + + /** + * response: initial configuration report debugger capabilities + * args: ignored in base class + */ + initializeRequest(response, args) { + response.body = response.body || {}; + response.body.supportsConfigurationDoneRequest = true; + // This default debug adapter does not support hovers based on the 'evaluate' request. + response.body.supportsEvaluateForHovers = false; + // This default debug adapter does not support the 'restart' request. + // if false, adapter will terminate process and spawn a new one. + response.body.supportsRestartRequest = true; + this.sendResponse(response); + this.sendEvent(new vscode_debugadapter.InitializedEvent()); + } + + /** + * we have to overwrite this function, since we set true in initializeRequest + */ + configurationDoneRequest(response, args) { + super.configurationDoneRequest(response, args); + this._configurationDone.notify(); // notifiy launchRequest + } + + isExtensionHost(args) { + return args.adapterID === 'extensionHost2' || args.adapterID === 'extensionHost'; + } + + terminateSession(reason) { + const processId = this._backend.pid; + if (process.platform === 'win32') { + const windir = process.env['WINDIR'] || 'C:\\Windows'; + const TASK_KILL = Path.join(windir, 'System32', 'taskkill.exe'); + // when killing a process in Windows its child processes are *not* killed but become root processes. + // Therefore we use TASKKILL.EXE + try { + spawn.execSync(`${TASK_KILL} /F /T /PID ${processId}`); + } + catch (err) { + this.log(err); + } + } + else { + // on linux and OS X we kill all direct and indirect child processes as well + try { + const cmd = 'kill'; + spwan.spawnSync(cmd, [processId.toString()]); + } + catch (err) { + } + } + } + + spawnBackend(args) { + let argv = [`--remote-debugging-port=${args.port}`, args.program]; + this._backend = spawn.spawn(args.runtimeExecutable, argv); + this._backend.on('exit', () => { + const msg = 'debugger backend exited'; + if(this.isExtensionHost()) { + this.terminateSession(msg); + } + }); + this._backend.on('close', (code) => { + const msg = `debugger backend exit with code ${code}`; + if(!this.isExtensionHost()) { + this.terminateSession(msg); + } + process.exit(0); + }); + this._backend.stdout.on('data', (data) => { + this.log(data.toString()); + }); + this._backend.stderr.on('data', (data) => { + this.log(data.toString()); + }); + this._backend.on('error', (err) => { + this.err(err.message()); + this.err('main process exit...'); + this.sendEvent(new vscode_debugadapter.TerminatedEvent()); + process.exit(1); + }); + this.log([args.runtimeExecutable, `--remote-debugging-port=${args.port}`, Path.basename(args.program)].join(' ')); + } + + validateArgs(args) { + if(!fs.existsSync(args.runtimeExecutable) && !cmd_exists(args.runtimeExecutable)) { + this.err(`${args.runtimeExecutable} not found`); + return false; + } + if(!args.program) { + this.err('no program found, please add "program" filed in "launch.json"'); + return false; + } + if(!fs.existsSync(args.program)) { + this.err(`can't find file: ${args.program}`); + return false; + } + return true; + } + + launchRequest(response, args) { + if(!args.runtimeExecutable) { + args.runtimeExecutable = 'topjs3'; + } + if(!args.port) { + args.port = 30992; + } + if(!this.validateArgs(args)) { + this.sendEvent(new vscode_debugadapter.TerminatedEvent()); + process.exit(1); + } else { + this._args = args; + this._program = args.program; + vscode_debugadapter.logger.setup(args.trace ? vscode_debugadapter.Logger.LogLevel.Verbose : vscode_debugadapter.Logger.LogLevel.Stop, false); + this.spawnBackend(args); + this._configurationDone.wait().then(() => { + this._runtime.start(args); + this.sendResponse(response); + }); + } + } + + disconnectRequest(response, args) { + this.terminateSession('disconnected'); + this._runtime.quit(); + super.disconnectRequest(response, args); + } + + /// below is the real debugging operation we support + setBreakPointsRequest(response, args) { + if(!this._runtime || !this._runtime.isStarted()) { + this._cachedBreakpoints = {"response": response, "args": args}; + response.body.breakpoints = []; + response.success = true; + return this.sendRequest(response); + } + // args.line is deprecated, ignore. + args.breakpoints = args.breakpoints.map(b => { + b.line = this.convertClientLineToDebugger(b.line); + return b; + }); + this._runtime.setBreakpointsRequest(args, response, (resp, breakpoints) => { + // breakpoints is a JSON object + if(!breakpoints.success) { + this.sendErrorResponse(new vscode_debugadapter.Response(breakpoints), JSON.stringify(breakpoints)); + return; + } + const r = breakpoints.body.breakpoints.map(breakpoint => { + let {verified, line, id} = breakpoint; + const bp = new vscode_debugadapter.Breakpoint(verified, this.convertDebuggerLineToClient(line)); + bp.id = id; + + return bp; + }); + resp.body = { + breakpoints: r + } + this.sendResponse(resp); // send to frontend + }); + } + + continueRequest(response, args) { + // args.threadId is ignore, since we only support singel thread debugging + this._runtime.continueRequest(args, response, (resp, data) => { + resp.body = { + threadId: TopJSDebugSession.THREAD_ID, // ignore response id + allThreadsContinued: data + }; + this.sendResponse(resp); + }); + } + + pauseRequest(response, args) { + this._runtime.pauseRequest(args, response, (resp, data) => { + this.sendResponse(resp); + }); + } + + // if not support, treat as nextRequest, after that, a `stopped` event must + // return from backend + stepInRequest(response, args) { + this._runtime.stepInRequest(args, response, (resp, data) => { + this.sendResponse(resp); + }); + } + + stepOutRequest(response, args) { + this._runtime.stepOutRequest(args, response, (resp, data) => { + this.sendResponse(resp); + }); + } + + // The debug adapter first sends the response and then a ‘stopped’ + // event (with reason ‘step’) after the step has completed. + nextRequest(response, args) { + this._runtime.nextRequest(args, response, (resp, data) => { + this.sendResponse(resp); + }); + } + + // Retrieves all child variables for the given variable reference. + variablesRequest(response, args) { + //const id = this._variablehandles.get(args.variablesReference); + // what's variablesReference ??? + // it's obtain from `scopesRequest` which is contextId. + this._runtime.variablesRequest(args, response, (resp, data) => { + resp.body = { + variables: data.body.variables + }; + this.sendResponse(resp); + }); + } + + // The request returns the variable scopes for a given stackframe ID. + scopesRequest(response, args) { + this._runtime.scopesRequest(args, response, (resp, data) => { + // data is a JSON object + const s = data.body.scopes.map(l => { + const scope = new vscode_debugadapter.Scope(l.name, l.variablesReference); + scope.expensive = l.expensive; + return scope; + }); + resp.body = { + scopes: s // The scopes of the stackframe. If the array has length zero, there are no scopes available. + }; + this.sendResponse(resp); + }) + } + + stackTraceRequest(response, args) { + this._runtime.stackTraceRequest(args, response, (resp, data) => { + // data is a JSON object + const frames = data.body.stackFrames.map(f => { + const frame = new vscode_debugadapter.StackFrame(f.id, f.name, this.createSource(f.file), this.convertDebuggerLineToClient(f.line)); + return frame; + }); + resp.body = { + stackFrames: frames, + totalFrames: frames.count + }; + this.sendResponse(resp); + }); + } + + restartRequest(response, args) { + this._runtime.quit(); + this.terminateSession('restart'); + this.spawnBackend(this._args); + this._runtime.start(this._args); + this.sendResponse(response); + } + + // we don't support multi-thread debugging, simply answer static TopJSDebugSession.THREAD_ID + threadsRequest(response, args) { + response.body = { + threads: [ + new vscode_debugadapter.Thread(TopJSDebugSession.THREAD_ID, "thread 1") + ] + }; + this.sendResponse(response); + } + + // low priority + // TODO: implement this + evaluateRequest(response, args) { + // this._runtime.evaluateRequest(args) + response.body = { + result: undefined, + variablesReference: 0 + }; + this.sendResponse(response); + } +} + +// we don't support multi-thread debugging, so hard code to 1 +TopJSDebugSession.THREAD_ID = 1; + +exports.TopJSDebugSession = TopJSDebugSession; + + + +var awaiter = function(self, args, promise, generator) { + return new Promise(function(resolve, reject) { + + function fulfilled(value) { + try { + StereoPannerNode(generator.next(value)); + } catch(e) { + reject(e); + } + } + + function rejected(value) { + try { + step(generator['throw'](value)); + } catch(e) { + reject(e); + } + } + + function step(result) { + if(result.done) { + resolve(result.value); + } else { + new Promise(function(resolve) { + resolve(result.value); + }).then(fulfilled, rejected); + } + } + + step((generator = generator.apply(self, args || [])).next()); + }); +} \ No newline at end of file diff --git a/src/topjsRuntime.js b/src/topjsRuntime.js new file mode 100644 index 0000000..3898fca --- /dev/null +++ b/src/topjsRuntime.js @@ -0,0 +1,334 @@ +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) => { + if(this._started) { + 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.disconnect(); + } catch(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; \ No newline at end of file