2019-05-09 09:06:50 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
2019-05-09 09:29:19 +08:00
|
|
|
|
if(!args.program.endsWith('.js')) {
|
|
|
|
|
this.err(`${args.program} is not a javascript file`);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2019-05-09 09:06:50 +08:00
|
|
|
|
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());
|
|
|
|
|
} 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());
|
|
|
|
|
});
|
|
|
|
|
}
|