Files
pioarduino-vscode-debug/src/backend/adapter.ts
T
2026-03-11 19:57:06 +01:00

1318 lines
51 KiB
TypeScript

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<string | VariableObject | ExtendedVariable>(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<string, any[]> = new Map();
private fileExistsCache: Map<string, boolean> = 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<void> {
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<any> {
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<any[]> {
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<any>[] = [];
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<any>[] = [];
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<void> {
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<void> {
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<Scope>();
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<VariableObject> {
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<void> {
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<void> {
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<void> {
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<void> {
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>',
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<void> {
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 === '<nullptr>') {
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<void> {
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<void> {
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<void> {
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<void> {
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<boolean> {
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);