import * as crypto from 'crypto'; import * as fs from 'fs'; import { setTimeout } from 'timers'; import { DebugSession, Event, Handles, InitializedEvent, OutputEvent, TerminatedEvent, ThreadEvent, Thread, StackFrame, Scope, Source, ContinuedEvent, } from '@vscode/debugadapter'; import { StoppedEvent, AdapterOutputEvent } from '../common'; import { hexFormat, encodeDisassembly, parseQuery } from '../utils'; import { VariableObject, MIError } from './mi2/types'; import { expandValue } from './expand_value'; import { MI2 } from './mi2/mi2'; import { MINode } from './mi_parse'; import { SymbolTable } from './symbols'; /** Wraps a variable reference with options. */ class ExtendedVariable { constructor( public name: string, public options: any ) {} } const STATIC_HANDLES_START = 65536; // 0x10000 const STATIC_HANDLES_END = 131071; // 0x1FFFF class CustomStopEvent extends Event { constructor(reason: string, threadID: number) { super('custom-stop', { reason, threadID }); } } class CustomContinuedEvent extends Event { constructor(threadID: number, allThreads: boolean = true) { super('custom-continued', { threadID, allThreads }); } } /** DAP implementation on top of GDB/MI2. */ export class GDBDebugSession extends DebugSession { private variableHandles = new Handles(131072); private variableHandlesReverse: { [name: string]: number } = {}; private forceDisassembly: boolean = false; private activeEditorPath: string | null = null; private currentThreadId: number = 0; private stopped: boolean = false; private stoppedReason: string = ''; private breakpointMap: Map = new Map(); private fileExistsCache: Map = new Map(); private miDebugger: MI2; private args: any; private quit: boolean; private attached: boolean; private started: boolean; private crashed: boolean; private debugReady: boolean; private symbolTable: SymbolTable; constructor(debuggerLinesStartAt1?: boolean, isServer?: boolean) { super(debuggerLinesStartAt1, isServer); } /** Attaches MI2 listeners and sends InitializedEvent. */ initDebugger(): void { this.miDebugger.on('launcherror', this.launchError.bind(this)); this.miDebugger.on('quit', this.quitEvent.bind(this)); this.miDebugger.on('exited-normally', this.quitEvent.bind(this)); this.miDebugger.on('stopped', this.stopEvent.bind(this)); this.miDebugger.on('msg', this.handleMsg.bind(this)); this.miDebugger.on('breakpoint', this.handleBreakpoint.bind(this)); this.miDebugger.on('step-end', this.handleBreak.bind(this)); this.miDebugger.on('step-out-end', this.handleBreak.bind(this)); this.miDebugger.on('signal-stop', this.handlePause.bind(this)); this.miDebugger.on('running', this.handleRunning.bind(this)); this.miDebugger.on('thread-created', this.handleThreadCreated.bind(this)); this.miDebugger.on('thread-exited', this.handleThreadExited.bind(this)); this.miDebugger.on('thread-selected', this.handleThreadSelected.bind(this)); this.sendEvent(new InitializedEvent()); } /** Responds to DAP initialize with capabilities. */ protected initializeRequest(response: any, args: any): void { response.body.supportsConditionalBreakpoints = true; response.body.supportsConfigurationDoneRequest = true; response.body.supportsEvaluateForHovers = true; response.body.supportsFunctionBreakpoints = true; response.body.supportsHitConditionalBreakpoints = true; response.body.supportsRestartRequest = true; response.body.supportsSetVariable = true; response.body.supportsTerminateRequest = true; response.body.supportsValueFormattingOptions = true; this.sendResponse(response); } /** Handles DAP launch request. */ protected launchRequest(response: any, args: any): void { this.args = args; this.processLaunchAttachRequest(response, false); } /** Handles DAP attach request. */ protected attachRequest(response: any, args: any): void { this.args = args; this.processLaunchAttachRequest(response, true); } /** Core launch/attach handler. */ private processLaunchAttachRequest(response: any, isAttach: boolean): void { this.quit = false; this.attached = false; this.started = false; this.crashed = false; this.debugReady = false; this.stopped = false; this.breakpointMap = new Map(); this.fileExistsCache = new Map(); const pioArgs = ['debug']; if (this.args.projectEnvName) { pioArgs.push('-e', this.args.projectEnvName); } if (this.args.loadMode) { pioArgs.push('--load-mode', this.args.loadMode); } pioArgs.push('--interface', 'gdb', '--interpreter=mi2', '-q'); this.miDebugger = new MI2('platformio', pioArgs); this.initDebugger(); this.miDebugger.printCalls = !!this.args.showDevDebugOutput; this.miDebugger.debugOutput = !!this.args.showDevDebugOutput; this.miDebugger.once('debug-ready', () => { this.debugReady = true; this.stopped = true; this.stoppedReason = 'start'; this.sendEvent(new StoppedEvent('start', this.currentThreadId, true)); this.sendEvent(new CustomStopEvent('start', this.currentThreadId)); }); this.miDebugger .connect(this.args.cwd, [ 'interpreter-exec console "source .pioinit"', 'enable-pretty-printing', ]) .then( () => { this.symbolTable = new SymbolTable(this.args.toolchainBinDir, this.args.executable); try { this.symbolTable.loadSymbols(); this.started = true; this.sendResponse(response); } catch (err) { if (this.args.toolchainBinDir) { this.sendErrorResponse( response, 102, `Failed to load symbols from executable file: ${err.toString()}` ); } } }, (err) => { this.sendErrorResponse(response, 103, `Failed to launch GDB: ${err.toString()}`); } ); } /** Handles custom DAP requests. */ protected customRequest(command: string, response: any, args: any): void { switch (command) { case 'set-force-disassembly': response.body = { success: true }; this.forceDisassembly = args.force; if (this.stopped) { this.activeEditorPath = null; this.sendEvent(new ContinuedEvent(this.currentThreadId, true)); this.sendEvent(new StoppedEvent(this.stoppedReason, this.currentThreadId, true)); } this.sendResponse(response); break; case 'load-function-symbols': response.body = { functionSymbols: this.symbolTable.getFunctionSymbols() }; this.sendResponse(response); break; case 'set-active-editor': this.activeEditorPath = args.path; response.body = {}; this.sendResponse(response); break; case 'get-arguments': response.body = this.args; this.sendResponse(response); break; case 'read-memory': this.customReadMemoryRequest(response, args.address, args.length); break; case 'write-memory': this.customWriteMemoryRequest(response, args.address, args.data); break; case 'read-registers': this.customReadRegistersRequest(response); break; case 'read-register-list': this.customReadRegisterListRequest(response); break; case 'disassemble': this.disassembleRequest(response, args); break; case 'execute-command': let cmd: string = args.command; cmd = cmd.startsWith('-') ? cmd.substring(1) : `interpreter-exec console "${cmd}"`; this.miDebugger.sendCommand(cmd).then( (result) => { response.body = result.resultRecords; this.sendResponse(response); }, (err) => { response.body = err; this.sendErrorResponse(response, 110, 'Unable to execute command'); } ); break; default: response.body = { error: 'Invalid command.' }; this.sendResponse(response); } } /** Handles disassemble custom request. */ protected async disassembleRequest(response: any, args: any): Promise { if (args.function) { try { const result = await this.getDisassemblyForFunction(args.function, args.file); response.body = { instructions: result.instructions, name: result.name, file: result.file, address: result.address, length: result.length, }; this.sendResponse(response); } catch (err) { this.sendErrorResponse(response, 1, `Unable to disassemble: ${err.toString()}`); } } else if (args.startAddress) { try { let func = this.symbolTable.getFunctionAtAddress(args.startAddress); if (func) { func = await this.getDisassemblyForFunction(func.name, func.file); response.body = { instructions: func.instructions, name: func.name, file: func.file, address: func.address, length: func.length, }; this.sendResponse(response); } else { const instructions = await this.getDisassemblyForAddresses( args.startAddress, args.length || 256 ); response.body = { instructions }; this.sendResponse(response); } } catch (err) { this.sendErrorResponse(response, 1, `Unable to disassemble: ${err.toString()}`); } } else { this.sendErrorResponse(response, 1, 'Unable to disassemble; invalid parameters.'); } } /** Gets/caches function disassembly. */ private async getDisassemblyForFunction(functionName: string, file: string): Promise { const func = this.symbolTable.getFunctionByName(functionName, file); if (!func) { throw new Error(`Unable to find function with name ${functionName}.`); } if (func.instructions) { return func; } const startAddress = func.address; const endAddress = func.address + func.length; const result = await this.miDebugger.sendCommand( `data-disassemble -s ${hexFormat(startAddress, 8)} -e ${hexFormat(endAddress, 8)} -- 2` ); const instructions = result.result('asm_insns').map((entry: any) => ({ address: MINode.valueOf(entry, 'address'), functionName: MINode.valueOf(entry, 'func-name'), offset: parseInt(MINode.valueOf(entry, 'offset')), instruction: MINode.valueOf(entry, 'inst'), opcodes: MINode.valueOf(entry, 'opcodes'), })); func.instructions = instructions; return func; } /** Gets disassembly for address range. */ private async getDisassemblyForAddresses(startAddress: number, length: number): Promise { const endAddress = startAddress + length; const result = await this.miDebugger.sendCommand( `data-disassemble -s ${hexFormat(startAddress, 8)} -e ${hexFormat(endAddress, 8)} -- 2` ); return result.result('asm_insns').map((entry: any) => ({ address: MINode.valueOf(entry, 'address'), functionName: MINode.valueOf(entry, 'func-name'), offset: parseInt(MINode.valueOf(entry, 'offset')), instruction: MINode.valueOf(entry, 'inst'), opcodes: MINode.valueOf(entry, 'opcodes'), })); } /** Reads memory via MI. */ private customReadMemoryRequest(response: any, address: number, length: number): void { this.miDebugger.examineMemory(address, length).then( (data) => { const bytes = data.match(/[0-9a-f]{2}/g).map((hex: string) => parseInt(hex, 16)); response.body = { startAddress: address, endAddress: address + bytes.length, bytes, }; this.sendResponse(response); }, (err) => { response.body = { error: err }; this.sendErrorResponse(response, 114, `Unable to read memory: ${err.toString()}`); } ); } /** Writes memory via MI. */ private customWriteMemoryRequest(response: any, address: number, data: string): void { const hexAddr = hexFormat(address, 8); this.miDebugger.sendCommand(`data-write-memory-bytes ${hexAddr} ${data}`).then( (result) => { this.sendResponse(response); }, (err) => { response.body = { error: err }; this.sendErrorResponse(response, 114, `Unable to write memory: ${err.toString()}`); } ); } /** Reads CPU registers via MI. */ private customReadRegistersRequest(response: any): void { this.miDebugger.sendCommand('data-list-register-values x').then( (result) => { if (result.resultRecords.resultClass === 'done') { const registers = result.resultRecords.results[0][1]; response.body = registers.map((reg: any) => { const obj: any = {}; reg.forEach((pair: any) => { obj[pair[0]] = pair[1]; }); return obj; }); } else { response.body = { error: 'Unable to parse response' }; } this.sendResponse(response); }, (err) => { response.body = { error: err }; this.sendErrorResponse(response, 115, `Unable to read registers: ${err.toString()}`); } ); } /** Reads register names via MI. */ private customReadRegisterListRequest(response: any): void { this.miDebugger.sendCommand('data-list-register-names').then( (result) => { if (result.resultRecords.resultClass === 'done') { let names: string[]; result.resultRecords.results.forEach((entry: any) => { if (entry[0] === 'register-names') { names = entry[1]; } }); response.body = names; } else { response.body = { error: result.resultRecords.results }; } this.sendResponse(response); }, (err) => { response.body = { error: err }; this.sendErrorResponse(response, 116, `Unable to read register list: ${err.toString()}`); } ); } /** Handles DAP disconnect. */ protected disconnectRequest(response: any, args: any): void { if (this.miDebugger) { if (this.attached) { this.miDebugger.detach(); } else { this.miDebugger.stop(true); } } this.sendResponse(response); } /** Handles DAP terminate. */ protected terminateRequest(response: any, args: any): void { if (this.miDebugger) { this.miDebugger.stop(); } this.sendResponse(response); } /** Handles DAP restart. */ protected restartRequest(response: any, args: any): void { const doRestart = () => { this.miDebugger .restart(['interpreter-exec console "pio_restart_target"']) .then( (result) => { this.sendResponse(response); setTimeout(() => { this.stopped = true; this.stoppedReason = 'restart'; this.sendEvent(new ContinuedEvent(this.currentThreadId, true)); this.sendEvent(new StoppedEvent('restart', this.currentThreadId, true)); }, 50); }, (err) => { this.sendErrorResponse(response, 6, `Could not restart: ${err}`); } ); }; if (this.stopped) { doRestart(); } else { this.miDebugger.once('generic-stopped', doRestart); this.miDebugger.sendCommand('exec-interrupt'); } } /** Emits adapter output event. */ private handleAdapterOutput(text: string): void { this.sendEvent(new AdapterOutputEvent(text, 'out')); } /** Forwards GDB console/log messages. */ private handleMsg(type: string, message: string): void { if (type === 'target') { type = 'stdout'; } if (type === 'log') { type = 'stderr'; } this.sendEvent(new OutputEvent(message, type)); } /** Emits continued events. */ private handleRunning(info: any): void { this.stopped = false; this.sendEvent(new ContinuedEvent(this.currentThreadId, true)); this.sendEvent(new CustomContinuedEvent(this.currentThreadId, true)); } /** Emits stopped events for breakpoints. */ private handleBreakpoint(info: any): void { const threadId = parseInt(info.record('thread-id') || this.currentThreadId); this.stopped = true; this.stoppedReason = 'breakpoint'; this.sendEvent(new StoppedEvent('breakpoint', threadId, true)); this.sendEvent(new CustomStopEvent('breakpoint', threadId)); } /** Emits stopped events for step end. */ private handleBreak(info: any): void { this.stopped = true; this.stoppedReason = 'step'; this.sendEvent(new StoppedEvent('step', this.currentThreadId, true)); this.sendEvent(new CustomStopEvent('step', this.currentThreadId)); } /** Emits stopped events for user pause. */ private handlePause(info: any): void { this.stopped = true; this.stoppedReason = 'user request'; this.sendEvent(new StoppedEvent('user request', this.currentThreadId, true)); this.sendEvent(new CustomStopEvent('user request', this.currentThreadId)); } /** Emits thread started. */ private handleThreadCreated(info: any): void { this.sendEvent(new ThreadEvent('started', info.threadId)); } /** Emits thread exited. */ private handleThreadExited(info: any): void { this.sendEvent(new ThreadEvent('exited', info.threadId)); } /** Emits thread selected. */ private handleThreadSelected(info: any): void { this.currentThreadId = info.threadId; this.sendEvent(new ThreadEvent('selected', info.threadId)); } /** Handles unexpected stop. */ private stopEvent(info: any): void { if (!this.started) { this.crashed = true; } if (!this.quit) { this.stopped = true; this.stoppedReason = 'exception'; this.sendEvent(new StoppedEvent('exception', this.currentThreadId, true)); this.sendEvent(new CustomStopEvent('exception', this.currentThreadId)); } } /** Handles GDB quit. */ private quitEvent(): void { this.quit = true; this.sendEvent(new TerminatedEvent()); } /** Handles launch error. */ private launchError(err: any): void { this.handleMsg('stderr', `Could not start debugger process > ${err.toString()}\n`); this.quitEvent(); } /** Handles function breakpoints. */ protected setFunctionBreakPointsRequest(response: any, args: any): void { if (!args.breakpoints || !args.breakpoints.length) { return; } const setBreakpoints = async (shouldContinue: boolean) => { const promises: Promise[] = []; args.breakpoints.forEach((bp: any) => { promises.push( this.miDebugger.addBreakPoint({ raw: bp.name, condition: bp.condition, countCondition: bp.hitCondition, }) ); }); try { const results = await Promise.all(promises); const breakpoints: any[] = []; results.forEach((result) => { if (result[0]) { breakpoints.push({ line: result[1].line }); } }); response.body = { breakpoints }; this.sendResponse(response); } catch (err) { this.sendErrorResponse(response, 10, err.toString()); } if (shouldContinue) { await this.miDebugger.sendCommand('exec-continue'); } }; const doSet = async () => { if (this.stopped) { await setBreakpoints(false); } else { this.miDebugger.sendCommand('exec-interrupt'); this.miDebugger.once('generic-stopped', () => { setBreakpoints(true); }); } }; if (this.debugReady) { doSet(); } else { this.miDebugger.once('debug-ready', doSet); } } /** Handles source/disassembly breakpoints. */ protected setBreakPointsRequest(response: any, args: any): void { const setBreakpoints = async (shouldContinue: boolean) => { this.debugReady = true; const existingBreakpoints = (this.breakpointMap.get(args.source.path) || []).map( (bp: any) => bp.number ); try { await this.miDebugger.removeBreakpoints(existingBreakpoints); this.breakpointMap.set(args.source.path, []); const promises: Promise[] = []; const decodedPath = decodeURIComponent(args.source.path); if (decodedPath.startsWith('disassembly:/')) { const params = parseQuery(decodedPath.substr(decodedPath.indexOf('?'))); const func = await this.getDisassemblyForFunction(params.func, params.file); args.breakpoints.forEach((bp: any) => { if (bp.line <= func.instructions.length) { const instruction = func.instructions[bp.line - 1]; promises.push( this.miDebugger.addBreakPoint({ file: args.source.path, line: bp.line, condition: bp.condition, countCondition: bp.hitCondition, raw: instruction.address, }) ); } }); } else { args.breakpoints.forEach((bp: any) => { promises.push( this.miDebugger.addBreakPoint({ file: args.source.path, line: bp.line, condition: bp.condition, countCondition: bp.hitCondition, }) ); }); } const results = (await Promise.all(promises)).filter((r) => r !== null); response.body = { breakpoints: results.map((bp: any) => ({ line: bp.line, id: bp.number, verified: true, })), }; this.breakpointMap.set(args.source.path, results); this.sendResponse(response); } catch (err) { this.sendErrorResponse(response, 9, err.toString()); } if (shouldContinue) { await this.miDebugger.sendCommand('exec-continue'); } }; const doSet = async () => { if (this.stopped) { await setBreakpoints(false); } else { await this.miDebugger.sendCommand('exec-interrupt'); this.miDebugger.once('generic-stopped', () => { setBreakpoints(true); }); } }; if (this.debugReady) { doSet(); } else { this.miDebugger.once('debug-ready', doSet); } } protected async threadsRequest(response: any): Promise { if (!this.stopped) { response.body = { threads: [] }; this.sendResponse(response); return; } try { const result = await this.miDebugger.sendCommand('thread-list-ids'); const threadIds: number[] = result.result('thread-ids').map((t: any) => parseInt(t[1])); const currentThreadId = result.result('current-thread-id'); if (currentThreadId) { this.currentThreadId = parseInt(currentThreadId); } else { await this.miDebugger.sendCommand(`thread-select ${threadIds[0]}`); this.currentThreadId = threadIds[0]; } const threadInfoResults = await Promise.all( threadIds.map((id) => this.miDebugger.sendCommand(`thread-info ${id}`)) ); const threads = threadInfoResults .map((info) => { let thread = info.result('threads'); if (thread.length === 1) { thread = thread[0]; const id = parseInt(MINode.valueOf(thread, 'id')); const targetId = MINode.valueOf(thread, 'target-id'); const details = MINode.valueOf(thread, 'details'); return new Thread(id, details || targetId); } return null; }) .filter((t) => t !== null); response.body = { threads }; this.sendResponse(response); } catch (err) { this.sendErrorResponse(response, 1, `Unable to get thread information: ${err}`); } } protected async stackTraceRequest(response: any, args: any): Promise { try { const stack = await this.miDebugger.getStack(args.threadId, args.startFrame, args.levels); const frames: StackFrame[] = []; for (const frame of stack) { const frameIndex = 0xffff & ((args.threadId << 8) | (0xff & frame.level)); const filePath = frame.file; let useDisassembly = this.forceDisassembly || !filePath; if (!useDisassembly) { useDisassembly = !(await this.checkFileExists(filePath)); } if ( !useDisassembly && this.activeEditorPath && this.activeEditorPath.startsWith('disassembly://') ) { const func = this.symbolTable.getFunctionByName(frame.function, frame.fileName); if (func && encodeDisassembly(func.name, func.file) === this.activeEditorPath) { useDisassembly = true; } } try { if (useDisassembly) { const disassembly = await this.getDisassemblyForFunction( frame.function, frame.fileName ); let lineNumber = -1; disassembly.instructions.forEach((instruction: any, index: number) => { if (instruction.address === frame.address) { lineNumber = index + 1; } }); if (lineNumber !== -1) { const uri = encodeDisassembly(disassembly.name, disassembly.file); frames.push( new StackFrame( frameIndex, `${frame.function}@${frame.address}`, new Source(disassembly.name, uri), lineNumber, 0 ) ); } else { frames.push( new StackFrame( frameIndex, frame.function + '@' + frame.address, null, frame.line, 0 ) ); } } else { frames.push( new StackFrame( frameIndex, frame.function + '@' + frame.address, new Source(frame.fileName, filePath), frame.line, 0 ) ); } } catch (err) { frames.push( new StackFrame( frameIndex, frame.function + '@' + frame.address, null, frame.line, 0 ) ); } } response.body = { stackFrames: frames }; this.sendResponse(response); } catch (err) { this.sendErrorResponse(response, 12, `Failed to get Stack Trace: ${err.toString()}`); } } /** Handles configurationDone. */ protected configurationDoneRequest(response: any, args: any): void { this.sendResponse(response); } /** Handles scopes request. */ protected scopesRequest(response: any, args: any): void { const scopes = new Array(); scopes.push(new Scope('Local', parseInt(args.frameId), false)); scopes.push(new Scope('Global', 254, false)); scopes.push(new Scope('Static', STATIC_HANDLES_START + parseInt(args.frameId), false)); response.body = { scopes }; this.sendResponse(response); } /** Creates a numeric handle for a variable. */ private createVariable(name: string | VariableObject, options?: any): number { if (options) { return this.variableHandles.create(new ExtendedVariable(name as string, options)); } return this.variableHandles.create(name as any); } /** Finds/creates a handle for a VariableObject. */ private findOrCreateVariable(variable: VariableObject): number { let id: number; if (this.variableHandlesReverse.hasOwnProperty(variable.name)) { id = this.variableHandlesReverse[variable.name]; } else { id = this.createVariable(variable); this.variableHandlesReverse[variable.name] = id; } return variable.isCompound() ? id : 0; } /** Fetches/creates a MI var object, applies updates. */ private async getVarObjByName(expression: string, name: string): Promise { let varObj: VariableObject; try { const updateResult = await this.miDebugger.varUpdate(name); updateResult.result('changelist').forEach((change: any) => { const changeName = MINode.valueOf(change, 'name'); const handle = this.variableHandlesReverse[changeName]; (this.variableHandles.get(handle) as VariableObject).applyChanges(change); }); const handle = this.variableHandlesReverse[name]; varObj = this.variableHandles.get(handle) as VariableObject; } catch (err) { if (!(err instanceof MIError && err.message === 'Variable object not found')) { throw err; } varObj = await this.miDebugger.varCreate(expression, name); const id = this.findOrCreateVariable(varObj); varObj.exp = expression; varObj.id = id; } return varObj; } /** Returns local stack variables. */ private async stackVariablesRequest( threadId: number, frameLevel: number, response: any, args: any ): Promise { const variables: any[] = []; try { const stackVars = await this.miDebugger.getStackVariables(threadId, frameLevel); for (const stackVar of stackVars) { try { const varName = `var_local_${stackVar.name}`; const varObj = await this.getVarObjByName(stackVar.name, varName); variables.push(varObj.toProtocolVariable()); } catch (err) { variables.push({ name: stackVar.name, value: `<${err}>`, variablesReference: 0, }); } } response.body = { variables }; this.sendResponse(response); } catch (err) { this.sendErrorResponse(response, 1, `Could not expand variable: ${err}`); } } /** Returns global variables. */ private async globalVariablesRequest(response: any, args: any): Promise { const globalVars = this.symbolTable.getGlobalVariables(); const variables: any[] = []; try { for (const globalVar of globalVars) { const varName = `var_global_${globalVar.name}`; const varObj = await this.getVarObjByName(globalVar.name, varName); variables.push(varObj.toProtocolVariable()); } response.body = { variables }; this.sendResponse(response); } catch (err) { this.sendErrorResponse(response, 1, `Could not get global variable information: ${err}`); } } /** Returns static variables for the file. */ private async staticVariablesRequest( threadId: number, frameLevel: number, response: any, args: any ): Promise { const variables: any[] = []; try { const frameInfo = await this.miDebugger.getFrame(threadId, frameLevel); const fileName = frameInfo.fileName; const staticVars = this.symbolTable.getStaticVariables(fileName); for (const staticVar of staticVars) { const varName = `var_static_${fileName}_${staticVar.name}`; const varObj = await this.getVarObjByName(staticVar.name, varName); variables.push(varObj.toProtocolVariable()); } response.body = { variables }; this.sendResponse(response); } catch (err) { this.sendErrorResponse(response, 1, `Could not get global variable information: ${err}`); } } /** Expands children for a GDB expression. */ private async variableMembersRequest( expression: string, response: any, args: any ): Promise { let evalResult: MINode; try { evalResult = await this.miDebugger.evalExpression(JSON.stringify(expression)); try { let expanded: any = expandValue( this.createVariable.bind(this), evalResult.result('value'), expression, evalResult ); if (expanded) { if (typeof expanded[0] === 'string') { const formatValue = (v: any): string => { if (typeof v === 'object') { if (v.length !== undefined) { return v.join(', '); } return JSON.stringify(v); } return v; }; expanded = [ { name: '', value: formatValue(expanded), variablesReference: 0, }, ]; } response.body = { variables: expanded }; this.sendResponse(response); } else { this.sendErrorResponse(response, 2, 'Could not expand variable'); } } catch (err) { this.sendErrorResponse(response, 2, `Could not expand variable: ${err}`); } } catch (err) { this.sendErrorResponse(response, 1, `Could not expand variable: ${err}`); } } /** Main variables dispatcher. */ protected async variablesRequest(response: any, args: any): Promise { let varRef: any; if (args.variablesReference === 254) { return this.globalVariablesRequest(response, args); } if (args.variablesReference >= 256 && args.variablesReference < STATIC_HANDLES_START) { const frameLevel = 0xff & args.variablesReference; const threadId = (0xff00 & args.variablesReference) >>> 8; return this.stackVariablesRequest(threadId, frameLevel, response, args); } if ( args.variablesReference >= STATIC_HANDLES_START && args.variablesReference <= STATIC_HANDLES_END ) { const frameLevel = 0xff & args.variablesReference; const threadId = (0xff00 & args.variablesReference) >>> 8; return this.staticVariablesRequest(threadId, frameLevel, response, args); } varRef = this.variableHandles.get(args.variablesReference); if (typeof varRef === 'string') { return this.variableMembersRequest(varRef, response, args); } if (typeof varRef === 'object') { if (varRef instanceof VariableObject) { const parent = varRef; try { const children = await this.miDebugger.varListChildren(varRef.name); const variables = children.map((child) => { const id = this.findOrCreateVariable(child); child.id = id; if (/^\d+$/.test(child.exp)) { child.fullExp = `${parent.fullExp || parent.exp}[${child.exp}]`; } else { child.fullExp = `${parent.fullExp || parent.exp}.${child.exp}`; } return child.toProtocolVariable(); }); response.body = { variables }; this.sendResponse(response); } catch (err) { this.sendErrorResponse(response, 1, `Could not expand variable: ${err}`); } } else if (varRef instanceof ExtendedVariable) { const extVar = varRef; if (extVar.options.arg) { const variables: any[] = []; let firstNull = true; let index = 0; const done = () => { response.body = { variables }; this.sendResponse(response); }; const fetchNext = async () => { const evalResult = await this.miDebugger.evalExpression( JSON.stringify(`${extVar.name}+${index})`) ); try { const expanded = expandValue( this.createVariable.bind(this), evalResult.result('value'), extVar.name, evalResult ); if (expanded) { if (typeof expanded === 'string') { if (expanded === '') { if (!firstNull) { return done(); } firstNull = false; } else if (expanded[0] !== '"') { variables.push({ name: '[err]', value: expanded, variablesReference: 0, }); return done(); } variables.push({ name: `[${index++}]`, value: expanded, variablesReference: 0, }); fetchNext(); } else { variables.push({ name: '[err]', value: expanded, variablesReference: 0, }); done(); } } else { this.sendErrorResponse(response, 15, 'Could not expand variable'); } } catch (err) { this.sendErrorResponse(response, 14, `Could not expand variable: ${err}`); } }; fetchNext(); } else { this.sendErrorResponse( response, 13, `Unimplemented variable request options: ${JSON.stringify(extVar.options)}` ); } } else { response.body = { variables: varRef }; this.sendResponse(response); } } else { response.body = { variables: [] }; this.sendResponse(response); } } /** Handles evaluate for watch/hover/REPL. */ protected async evaluateRequest(response: any, args: any): Promise { if (args.context === 'watch') { try { const varName = `watch_${crypto.createHash('md5').update(args.expression).digest('hex')}}`; const varObj = await this.getVarObjByName(args.expression, varName); response.body = { result: varObj.value, variablesReference: varObj.id, }; this.sendResponse(response); } catch (err) { response.body = { result: `<${err.toString()}>`, variablesReference: 0, }; this.sendErrorResponse(response, 7, err.toString()); } } else if (args.context === 'hover') { try { const result = await this.miDebugger.evalExpression(args.expression); response.body = { variablesReference: 0, result: result.result('value'), }; this.sendResponse(response); } catch (err) { this.sendErrorResponse(response, 7, err.toString()); } } else { this.miDebugger.sendUserInput(args.expression).then( (result) => { response.body = result === undefined ? { result: '', variablesReference: 0 } : { result: JSON.stringify(result), variablesReference: 0 }; this.sendResponse(response); }, (err) => { this.sendErrorResponse(response, 8, err.toString()); } ); } } /** Handles setVariable via -var-assign. */ protected async setVariableRequest(response: any, args: any): Promise { try { let varName = args.name; const varRef = this.variableHandles.get(args.variablesReference); if (varRef) { varName = `${(varRef as VariableObject).name}.${args.name}`; } else if (args.variablesReference === 254) { varName = `var_global_${args.name}`; } else if (args.variablesReference >= 256 && args.variablesReference < STATIC_HANDLES_START) { varName = `var_local_${args.name}`; } else if ( args.variablesReference >= STATIC_HANDLES_START && args.variablesReference <= STATIC_HANDLES_END ) { varName = `var_static_${args.name}`; } const result = await this.miDebugger.varAssign(varName, args.value); response.body = { value: result.result('value') }; this.sendResponse(response); } catch (err) { this.sendErrorResponse(response, 11, `Could not update variable: ${err}`); } } /** Sends exec-interrupt. */ protected pauseRequest(response: any, args: any): void { this.miDebugger.interrupt(args.threadId).then( (result) => { this.sendResponse(response); }, (err) => { this.sendErrorResponse(response, 3, `Could not pause: ${err}`); } ); } /** Sends exec-continue. */ protected continueRequest(response: any, args: any): void { this.miDebugger.continue(args.threadId).then( (result) => { response.body = { allThreadsContinued: true }; this.sendResponse(response); }, (err) => { this.sendErrorResponse(response, 2, `Could not continue: ${err}`); } ); } /** Step in; uses instruction-level as needed. */ protected async stepInRequest(response: any, args: any): Promise { try { let useDisassembly = this.forceDisassembly; if (!useDisassembly) { const frame = await this.miDebugger.getFrame(args.threadId, 0); useDisassembly = !(await this.checkFileExists(frame.file)); if ( this.activeEditorPath && this.activeEditorPath.startsWith('disassembly://') ) { const func = this.symbolTable.getFunctionByName(frame.function, frame.fileName); if (encodeDisassembly(func.name, func.file) === this.activeEditorPath) { useDisassembly = true; } } } await this.miDebugger.step(args.threadId, useDisassembly); this.sendResponse(response); } catch (err) { this.sendErrorResponse(response, 6, `Could not step over: ${err}`); } } /** Sends exec-finish. */ protected stepOutRequest(response: any, args: any): void { this.miDebugger.stepOut(args.threadId).then( (result) => { this.sendResponse(response); }, (err) => { this.sendErrorResponse(response, 5, `Could not step out: ${err}`); } ); } /** Next; uses instruction-level as needed. */ protected async nextRequest(response: any, args: any): Promise { try { let useDisassembly = this.forceDisassembly; if (!useDisassembly) { const frame = await this.miDebugger.getFrame(args.threadId, 0); useDisassembly = !(await this.checkFileExists(frame.file)); if ( this.activeEditorPath && this.activeEditorPath.startsWith('disassembly://') ) { const func = this.symbolTable.getFunctionByName(frame.function, frame.fileName); if (encodeDisassembly(func.name, func.file) === this.activeEditorPath) { useDisassembly = true; } } } await this.miDebugger.next(args.threadId, useDisassembly); this.sendResponse(response); } catch (err) { this.sendErrorResponse(response, 6, `Could not step over: ${err}`); } } /** Checks file existence with caching. */ private checkFileExists(file: string): Promise { if (!file) { return Promise.resolve(false); } if (this.fileExistsCache.has(file)) { return Promise.resolve(this.fileExistsCache.get(file)); } return new Promise((resolve, reject) => { fs.exists(file, (exists) => { this.fileExistsCache.set(file, exists); resolve(exists); }); }); } } DebugSession.run(GDBDebugSession);