@@ -0,0 +1,4 @@ | |||
*.vsix | |||
out | |||
package-lock.json | |||
node_modules |
@@ -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" | |||
} | |||
] | |||
} |
@@ -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 | |||
} |
@@ -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 | |||
} | |||
} | |||
] | |||
} |
@@ -0,0 +1,7 @@ | |||
const ncp = require('ncp'); | |||
ncp('./src', './out', (err) => { | |||
if(err) { | |||
return console.error(err); | |||
} | |||
}); |
@@ -0,0 +1,117 @@ | |||
{ | |||
"name": "topjs-debugger", | |||
"displayName": "TopJS Debugger", | |||
"version": "0.0.1", | |||
"publisher": "AbbyCin", | |||
"description": "TopJS debugger extension for VS Code.", | |||
"author": { | |||
"name": "AbbyCin", | |||
"email": "abbytsing@gmail.com" | |||
}, | |||
"license": "MIT", | |||
"keywords": [ | |||
"multi-root ready" | |||
], | |||
"engines": { | |||
"vscode": "^1.30.0", | |||
"node": "^8.9.3" | |||
}, | |||
"icon": "favicon.png", | |||
"categories": [ | |||
"Debuggers" | |||
], | |||
"private": true, | |||
"repository": { | |||
"type": "git", | |||
"url": "https://github.com/Microsoft/vscode-mock-debug.git" | |||
}, | |||
"bugs": { | |||
"url": "https://github.com/Microsoft/vscode-mock-debug/issues" | |||
}, | |||
"scripts": { | |||
"postinstall": "node ./node_modules/vscode/bin/install", | |||
"build": "node ./build.js", | |||
"package": "vsce package", | |||
"publish": "vsce publish" | |||
}, | |||
"dependencies": { | |||
"await-notify": "^1.0.1", | |||
"command-exists": "^1.2.8", | |||
"vscode-debugadapter": "^1.33.0" | |||
}, | |||
"devDependencies": { | |||
"@types/node": "8.9.3", | |||
"vscode": "1.1.21", | |||
"vscode-debugadapter-testsupport": "1.33.0", | |||
"vsce": "1.53.2", | |||
"ncp": "2.0.0" | |||
}, | |||
"main": "./src/extension", | |||
"activationEvents": [ | |||
"onDebug", | |||
"onCommand:extension.topjs-debugger.getProgramPath" | |||
], | |||
"contributes": { | |||
"breakpoints": [ | |||
{ | |||
"language": "javascript" | |||
} | |||
], | |||
"debuggers": [ | |||
{ | |||
"type": "topjs", | |||
"label": "TopJS Debugger", | |||
"program": "./src/debugAdapter.js", | |||
"runtime": "node", | |||
"configurationAttributes": { | |||
"launch": { | |||
"required": [ | |||
"program", | |||
"runtimeExecutable", | |||
"port" | |||
], | |||
"properties": { | |||
"program": { | |||
"type": "string", | |||
"description": "Absolute path to a javascript file.", | |||
"default": "${workspaceFolder}/${command:getProgramPath}" | |||
}, | |||
"port": { | |||
"type": "number", | |||
"descrition": "Port for debugger backend.", | |||
"default": 30992 | |||
}, | |||
"runtimeExecutable": { | |||
"type": "string", | |||
"default": "topjs3" | |||
} | |||
} | |||
} | |||
}, | |||
"initialConfigurations": [ | |||
{ | |||
"type": "topjs", | |||
"request": "launch", | |||
"name": "Launch Debugger", | |||
"program": "${workspaceFolder}/${command:getProgramPath}" | |||
} | |||
], | |||
"configurationSnippets": [ | |||
{ | |||
"label": "TopJS Debug: Launch", | |||
"description": "A new configuration for 'debugging' a user selected javascript file.", | |||
"body": { | |||
"type": "topjs", | |||
"request": "launch", | |||
"name": "Launch Debugger2", | |||
"program": "^\"\\${workspaceFolder}/\\${command:getProgramPath}\"" | |||
} | |||
} | |||
], | |||
"variables": { | |||
"getProgramPath": "extension.topjs-debugger.getProgramPath" | |||
} | |||
} | |||
] | |||
} | |||
} |
@@ -0,0 +1,2 @@ | |||
const topjsDebugger = require('./topjsDebug'); | |||
topjsDebugger.TopJSDebugSession.run(topjsDebugger.TopJSDebugSession); |
@@ -0,0 +1,64 @@ | |||
const vscode = require('vscode'); | |||
const topjsDebug = require('./topjsDebug'); | |||
const net = require('net'); | |||
const path = require('path'); | |||
function activate(context) { | |||
context.subscriptions.push(vscode.commands.registerCommand('extension.topjs-debugger.getProgramPath', config => { | |||
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(); | |||
} | |||
} | |||
} |
@@ -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()); | |||
}); | |||
} |
@@ -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; |