topjs-debugger/src/topjsDebug.js
2019-05-09 09:29:19 +08:00

429 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
if(!args.program.endsWith('.js')) {
this.err(`${args.program} is not a javascript file`);
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());
} 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());
});
}