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()); }); }