Decompile minified dist/ bundles into human-readable TypeScript source
Reconstructed the original TypeScript source code from the two minified webpack bundles (dist/adapter.js and dist/extension.js). Source structure: - src/common.ts: Shared types (NumberFormat, events, SymbolType/Scope) - src/utils.ts: Utility functions (hexFormat, binaryFormat, etc.) - src/extension.ts: VS Code extension entry point - src/backend/: Debug adapter (GDB/MI2 protocol, symbol table, etc.) - src/frontend/: VS Code UI providers (peripherals, registers, memory, disassembly)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
out/
|
||||
.vscode-test/
|
||||
@@ -178,7 +178,7 @@
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
+2342
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "platformio-vscode-debug",
|
||||
"version": "1.4.1",
|
||||
"description": "PlatformIO Debugger for VSCode",
|
||||
"main": "dist/extension.js",
|
||||
"engines": {
|
||||
"vscode": "^1.35.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/platformio/platformio-vscode-ide.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack --mode production",
|
||||
"lint": "tslint ./src/**/*.ts",
|
||||
"postinstall": "node ./node_modules/@types/vscode/bin/install || true"
|
||||
},
|
||||
"dependencies": {
|
||||
"copy-paste": "^1.3.0",
|
||||
"vscode-debugadapter": "1.35.0",
|
||||
"vscode-debugprotocol": "1.35.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/vscode": "~1.44.0",
|
||||
"ts-loader": "^9.2.6",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "^4.5.5",
|
||||
"webpack": "~5.68.0",
|
||||
"webpack-cli": "~4.9.2"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,284 @@
|
||||
import { MINode } from './mi_parse';
|
||||
|
||||
const VARIABLE_ASSIGNMENT_REGEX = /^([a-zA-Z_\-][a-zA-Z0-9_\-]*|\[\d+\])\s*=\s*/;
|
||||
const VARIABLE_NAME_REGEX = /^[a-zA-Z_\-][a-zA-Z0-9_\-]*/;
|
||||
const ANGLE_BRACKET_REGEX = /^\<.+?\>/;
|
||||
const HEX_STRING_REGEX = /^(0x[0-9a-fA-F]+\s*)"/;
|
||||
const HEX_REGEX = /^0x[0-9a-fA-F]+/;
|
||||
const NULL_PTR_REGEX = /^0x0+\b/;
|
||||
const CHAR_CODE_STRING_REGEX = /^(\d+) ['"]/;
|
||||
const NUMBER_REGEX = /^\d+(\.\d+)?/;
|
||||
|
||||
export function isExpandable(value: string): number {
|
||||
value = value.trim();
|
||||
if (value.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (value.startsWith('{...}')) {
|
||||
return 2;
|
||||
}
|
||||
if (value[0] === '{') {
|
||||
return 1;
|
||||
}
|
||||
if (value.startsWith('true') || value.startsWith('false') || NULL_PTR_REGEX.exec(value) || HEX_STRING_REGEX.exec(value)) {
|
||||
return 0;
|
||||
}
|
||||
if (HEX_REGEX.exec(value)) {
|
||||
return 2;
|
||||
}
|
||||
// Check other patterns but always return 0 for them
|
||||
CHAR_CODE_STRING_REGEX.exec(value) || NUMBER_REGEX.exec(value) || VARIABLE_NAME_REGEX.exec(value) || ANGLE_BRACKET_REGEX.exec(value);
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function expandValue(
|
||||
createVariableReference: (name: string | any, options?: any) => number,
|
||||
value: string,
|
||||
root: string = '',
|
||||
extra?: any
|
||||
): any {
|
||||
const parseQuotedString = (): string => {
|
||||
value = value.trim();
|
||||
if (value[0] !== '"' && value[0] !== "'") {
|
||||
return '';
|
||||
}
|
||||
|
||||
let pos = 1;
|
||||
let scanning = true;
|
||||
const quote = value[0];
|
||||
let remaining = value.substr(1);
|
||||
let escaped = false;
|
||||
|
||||
while (scanning) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (remaining[0] === '\\') {
|
||||
escaped = true;
|
||||
} else if (remaining[0] === quote) {
|
||||
scanning = false;
|
||||
}
|
||||
remaining = remaining.substr(1);
|
||||
pos++;
|
||||
}
|
||||
|
||||
const result = value.substr(0, pos).trim();
|
||||
value = value.substr(pos).trim();
|
||||
return result;
|
||||
};
|
||||
|
||||
const stack = [root];
|
||||
let lastAssignedVariable = '';
|
||||
|
||||
const buildFullExpression = (name: string): string => {
|
||||
let fullPath = '';
|
||||
let derefPrefix = '';
|
||||
stack.push(name);
|
||||
stack.forEach((part) => {
|
||||
derefPrefix = '';
|
||||
if (part === '') {
|
||||
// skip
|
||||
} else if (part.startsWith('[')) {
|
||||
fullPath += part;
|
||||
} else if (fullPath) {
|
||||
while (part.startsWith('*')) {
|
||||
derefPrefix += '*';
|
||||
part = part.substr(1);
|
||||
}
|
||||
fullPath = fullPath + '.' + part;
|
||||
} else {
|
||||
fullPath = part;
|
||||
}
|
||||
});
|
||||
stack.pop();
|
||||
return derefPrefix + fullPath;
|
||||
};
|
||||
|
||||
let parseValue: () => any;
|
||||
let parseCommaValue: () => any;
|
||||
let parseNamedValue: (pushToStack?: boolean) => any;
|
||||
let parseCommaNamedValue: (pushToStack?: boolean) => any;
|
||||
let createResult: (name: string, val: any) => any;
|
||||
|
||||
parseValue = (): any => {
|
||||
value = value.trim();
|
||||
|
||||
if (value[0] === '"') {
|
||||
return parseQuotedString();
|
||||
}
|
||||
|
||||
if (value[0] === '{') {
|
||||
// Parse object/array
|
||||
return (() => {
|
||||
value = value.trim();
|
||||
if (value[0] !== '{') {
|
||||
return;
|
||||
}
|
||||
value = value.substr(1).trim();
|
||||
if (value[0] === '}') {
|
||||
value = value.substr(1).trim();
|
||||
return [];
|
||||
}
|
||||
if (value.startsWith('...')) {
|
||||
value = value.substr(3).trim();
|
||||
if (value[0] === '}') {
|
||||
value = value.substr(1).trim();
|
||||
return '<...>';
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is an array or named values
|
||||
const equalsIdx = value.indexOf('=');
|
||||
const braceIdx = value.indexOf('{');
|
||||
const commaIdx = value.indexOf(',');
|
||||
|
||||
let checkIdx = braceIdx;
|
||||
if (commaIdx !== -1 && commaIdx < braceIdx) {
|
||||
checkIdx = commaIdx;
|
||||
}
|
||||
|
||||
if ((checkIdx !== -1 && equalsIdx > checkIdx) || equalsIdx === -1) {
|
||||
// Array of values
|
||||
const arr: any[] = [];
|
||||
stack.push('[0]');
|
||||
let val = parseValue();
|
||||
stack.pop();
|
||||
arr.push(createResult('[0]', val));
|
||||
|
||||
let index = 0;
|
||||
for (;;) {
|
||||
stack.push('[' + (++index) + ']');
|
||||
val = parseCommaValue();
|
||||
if (!val) {
|
||||
stack.pop();
|
||||
break;
|
||||
}
|
||||
stack.pop();
|
||||
arr.push(createResult('[' + index + ']', val));
|
||||
}
|
||||
value = value.substr(1).trim();
|
||||
return arr;
|
||||
}
|
||||
|
||||
// Named values
|
||||
let named = parseNamedValue(true);
|
||||
if (named) {
|
||||
const arr: any[] = [];
|
||||
arr.push(named);
|
||||
while ((named = parseCommaNamedValue(true))) {
|
||||
arr.push(named);
|
||||
}
|
||||
value = value.substr(1).trim();
|
||||
return arr;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// Parse primitive value
|
||||
return (() => {
|
||||
value = value.trim();
|
||||
let match: RegExpExecArray;
|
||||
let result: any;
|
||||
|
||||
if (value.length === 0) {
|
||||
result = undefined;
|
||||
} else if (value.startsWith('true')) {
|
||||
result = 'true';
|
||||
value = value.substr(4).trim();
|
||||
} else if (value.startsWith('false')) {
|
||||
result = 'false';
|
||||
value = value.substr(5).trim();
|
||||
} else if ((match = NULL_PTR_REGEX.exec(value))) {
|
||||
result = '<nullptr>';
|
||||
value = value.substr(match[0].length).trim();
|
||||
} else if ((match = HEX_STRING_REGEX.exec(value))) {
|
||||
value = value.substr(match[1].length).trim();
|
||||
result = parseQuotedString();
|
||||
} else if ((match = HEX_REGEX.exec(value))) {
|
||||
result = '*' + match[0];
|
||||
value = value.substr(match[0].length).trim();
|
||||
} else if ((match = CHAR_CODE_STRING_REGEX.exec(value))) {
|
||||
result = match[1];
|
||||
value = value.substr(match[0].length - 1);
|
||||
result += ' ' + parseQuotedString();
|
||||
} else if (
|
||||
(match = NUMBER_REGEX.exec(value)) ||
|
||||
(match = VARIABLE_NAME_REGEX.exec(value)) ||
|
||||
(match = ANGLE_BRACKET_REGEX.exec(value))
|
||||
) {
|
||||
result = match[0];
|
||||
value = value.substr(match[0].length).trim();
|
||||
} else {
|
||||
result = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
})();
|
||||
};
|
||||
|
||||
parseNamedValue = (pushToStack: boolean = false): any => {
|
||||
value = value.trim();
|
||||
const match = VARIABLE_ASSIGNMENT_REGEX.exec(value);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
value = value.substr(match[0].length).trim();
|
||||
const name = (lastAssignedVariable = match[1]);
|
||||
if (pushToStack) {
|
||||
stack.push(lastAssignedVariable);
|
||||
}
|
||||
const val = parseValue();
|
||||
if (pushToStack) {
|
||||
stack.pop();
|
||||
}
|
||||
return createResult(name, val);
|
||||
};
|
||||
|
||||
createResult = (name: string, val: any): any => {
|
||||
let variablesReference = 0;
|
||||
|
||||
if (typeof val === 'object') {
|
||||
variablesReference = createVariableReference(val);
|
||||
val = 'Object';
|
||||
}
|
||||
|
||||
if (typeof val === 'string' && val.startsWith('*0x')) {
|
||||
if (extra && MINode.valueOf(extra, 'arg') === '1') {
|
||||
variablesReference = createVariableReference(buildFullExpression('*(' + name), { arg: true });
|
||||
val = '<args>';
|
||||
} else {
|
||||
variablesReference = createVariableReference(buildFullExpression('*' + name));
|
||||
val = 'Object@' + val;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof val === 'string' && val.startsWith('<...>')) {
|
||||
variablesReference = createVariableReference(buildFullExpression(name));
|
||||
val = '...';
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
value: val,
|
||||
variablesReference,
|
||||
};
|
||||
};
|
||||
|
||||
parseCommaValue = (): any => {
|
||||
value = value.trim();
|
||||
if (value[0] === ',') {
|
||||
value = value.substr(1).trim();
|
||||
return parseValue();
|
||||
}
|
||||
};
|
||||
|
||||
parseCommaNamedValue = (pushToStack: boolean = false): any => {
|
||||
value = value.trim();
|
||||
if (value[0] === ',') {
|
||||
value = value.substr(1).trim();
|
||||
return parseNamedValue(pushToStack);
|
||||
}
|
||||
};
|
||||
|
||||
value = value.trim();
|
||||
return parseValue();
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
import * as childProcess from 'child_process';
|
||||
import { EventEmitter } from 'events';
|
||||
import { VariableObject, MIError } from './types';
|
||||
import { parseMI, MINode } from '../mi_parse';
|
||||
|
||||
export function escape(str: string): string {
|
||||
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
const MI_OUTPUT_REGEX = /^(?:\d*|undefined)[\*\+\=]|[\~\@\&\^]/;
|
||||
const GDB_PROMPT_REGEX = /(?:\d*|undefined)\(gdb\)/;
|
||||
const BREAK_COUNT_REGEX = /\d+/;
|
||||
|
||||
function isPlainOutput(line: string): boolean {
|
||||
return !MI_OUTPUT_REGEX.exec(line);
|
||||
}
|
||||
|
||||
export class MI2 extends EventEmitter {
|
||||
private process: childProcess.ChildProcess;
|
||||
private currentToken: number = 1;
|
||||
private handlers: { [token: number]: (result: MINode) => void } = {};
|
||||
public printCalls: boolean;
|
||||
public debugOutput: boolean;
|
||||
private debugReadyFired: boolean = false;
|
||||
private debugReadyTimeout: any;
|
||||
private buffer: string;
|
||||
private errbuf: string;
|
||||
|
||||
constructor(
|
||||
public application: string,
|
||||
public args: string[]
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(cwd: string, commands: string[]): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = [...this.args];
|
||||
const env = Object.create(process.env);
|
||||
if (process.env.PLATFORMIO_PATH) {
|
||||
env.PATH = process.env.PLATFORMIO_PATH;
|
||||
env.Path = process.env.PLATFORMIO_PATH;
|
||||
}
|
||||
|
||||
this.process = childProcess.spawn(this.application, args, {
|
||||
cwd,
|
||||
env,
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
|
||||
this.process.stdout.on('data', this.stdout.bind(this));
|
||||
this.process.stderr.on('data', this.stderr.bind(this));
|
||||
this.process.on('exit', (() => {
|
||||
this.emit('quit');
|
||||
}).bind(this));
|
||||
this.process.on('error', ((err: Error) => {
|
||||
this.emit('launcherror', err);
|
||||
}).bind(this));
|
||||
|
||||
const initCommands = [
|
||||
this.sendCommand('gdb-set target-async on', true),
|
||||
...commands.map((cmd) => this.sendCommand(cmd)),
|
||||
];
|
||||
Promise.all(initCommands).then(() => {
|
||||
resolve(true);
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
stdout(data: any): void {
|
||||
this.buffer += typeof data === 'string' ? data : data.toString('utf8');
|
||||
const newlineIndex = this.buffer.lastIndexOf('\n');
|
||||
if (newlineIndex !== -1) {
|
||||
this.onOutput(this.buffer.substr(0, newlineIndex));
|
||||
this.buffer = this.buffer.substr(newlineIndex + 1);
|
||||
}
|
||||
if (this.buffer.length) {
|
||||
if (this.onOutputPartial(this.buffer)) {
|
||||
this.buffer = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr(data: any): void {
|
||||
this.errbuf += typeof data === 'string' ? data : data.toString('utf8');
|
||||
const newlineIndex = this.errbuf.lastIndexOf('\n');
|
||||
if (newlineIndex !== -1) {
|
||||
this.onOutputStderr(this.errbuf.substr(0, newlineIndex));
|
||||
this.errbuf = this.errbuf.substr(newlineIndex + 1);
|
||||
}
|
||||
if (this.errbuf.length) {
|
||||
this.logNoNewLine('stderr', this.errbuf);
|
||||
this.errbuf = '';
|
||||
}
|
||||
}
|
||||
|
||||
onOutputStderr(output: string): void {
|
||||
const lines = output.split('\n');
|
||||
lines.forEach((line) => {
|
||||
this.log('stderr', line);
|
||||
});
|
||||
}
|
||||
|
||||
onOutputPartial(line: string): boolean {
|
||||
if (isPlainOutput(line)) {
|
||||
this.logNoNewLine('stdout', line);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
onOutput(output: string): void {
|
||||
const lines = output.split('\n');
|
||||
lines.forEach((line) => {
|
||||
if (isPlainOutput(line)) {
|
||||
if (!GDB_PROMPT_REGEX.exec(line)) {
|
||||
this.log('stdout', line);
|
||||
}
|
||||
} else {
|
||||
const parsed = parseMI(line);
|
||||
if (this.debugOutput) {
|
||||
this.log('log', 'GDB -> App: ' + JSON.stringify(parsed));
|
||||
}
|
||||
|
||||
let handled = false;
|
||||
|
||||
if (parsed.token !== undefined && this.handlers[parsed.token]) {
|
||||
this.handlers[parsed.token](parsed);
|
||||
delete this.handlers[parsed.token];
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if (!handled && parsed.resultRecords && parsed.resultRecords.resultClass === 'error') {
|
||||
this.log('stderr', parsed.result('msg') || line);
|
||||
}
|
||||
|
||||
if (parsed.outOfBandRecord) {
|
||||
parsed.outOfBandRecord.forEach((record: any) => {
|
||||
if (record.isStream) {
|
||||
if (record.content.includes('PlatformIO: Initialization completed')) {
|
||||
this.debugReadyTimeout = setTimeout(() => {
|
||||
this.debugReadyFired = true;
|
||||
this.emit('debug-ready');
|
||||
}, 200);
|
||||
this.once('generic-stopped', () => {
|
||||
if (!this.debugReadyFired) {
|
||||
clearTimeout(this.debugReadyTimeout);
|
||||
this.emit('debug-ready');
|
||||
}
|
||||
});
|
||||
}
|
||||
this.log(record.type, record.content);
|
||||
} else if (record.type === 'exec') {
|
||||
this.emit('exec-async-output', parsed);
|
||||
if (record.asyncClass === 'running') {
|
||||
this.emit('running', parsed);
|
||||
} else if (record.asyncClass === 'stopped') {
|
||||
const reason = parsed.record('reason');
|
||||
if (reason === 'breakpoint-hit') {
|
||||
this.emit('breakpoint', parsed);
|
||||
} else if (reason === 'end-stepping-range') {
|
||||
this.emit('step-end', parsed);
|
||||
} else if (reason === 'function-finished') {
|
||||
this.emit('step-out-end', parsed);
|
||||
} else if (reason === 'signal-received') {
|
||||
this.emit('signal-stop', parsed);
|
||||
} else if (reason === 'exited-normally') {
|
||||
this.emit('exited-normally', parsed);
|
||||
} else if (reason === 'exited') {
|
||||
this.log('stderr', 'Program exited with code ' + parsed.record('exit-code'));
|
||||
this.emit('exited-normally', parsed);
|
||||
} else {
|
||||
if (this.debugReadyFired) {
|
||||
this.log('console', 'Not implemented stop reason (assuming exception): ' + reason);
|
||||
}
|
||||
this.emit('stopped', parsed);
|
||||
}
|
||||
this.emit('generic-stopped', parsed);
|
||||
} else {
|
||||
this.log('log', JSON.stringify(parsed));
|
||||
}
|
||||
} else if (record.type === 'notify') {
|
||||
if (record.asyncClass === 'thread-created') {
|
||||
const threadId = parsed.result('id');
|
||||
const threadGroupId = parsed.result('group-id');
|
||||
this.emit('thread-created', { threadId, threadGroupId });
|
||||
} else if (record.asyncClass === 'thread-exited') {
|
||||
const threadId = parsed.result('id');
|
||||
const threadGroupId = parsed.result('group-id');
|
||||
this.emit('thread-exited', { threadId, threadGroupId });
|
||||
} else if (record.asyncClass === 'thread-selected') {
|
||||
const threadId = parsed.result('id');
|
||||
this.emit('thread-selected', { threadId });
|
||||
}
|
||||
}
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if (parsed.token === undefined && parsed.resultRecords === undefined && parsed.outOfBandRecord.length === 0) {
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
this.log('log', 'Unhandled: ' + JSON.stringify(parsed));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stop(forceKill: boolean = false): void {
|
||||
if (forceKill) {
|
||||
const proc = this.process;
|
||||
const killTimeout = setTimeout(() => {
|
||||
process.kill(-proc.pid);
|
||||
}, 1000);
|
||||
this.process.on('exit', (code: any) => {
|
||||
clearTimeout(killTimeout);
|
||||
});
|
||||
}
|
||||
this.sendRaw('-gdb-exit');
|
||||
}
|
||||
|
||||
detach(): void {
|
||||
const proc = this.process;
|
||||
const killTimeout = setTimeout(() => {
|
||||
process.kill(-proc.pid);
|
||||
}, 1000);
|
||||
this.process.on('exit', (code: any) => {
|
||||
clearTimeout(killTimeout);
|
||||
});
|
||||
this.sendRaw('-target-detach');
|
||||
}
|
||||
|
||||
interrupt(threadId: number): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.sendCommand(`exec-interrupt --thread ${threadId}`).then(
|
||||
(result) => {
|
||||
resolve(result.resultRecords.resultClass === 'done');
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
continue(threadId: number): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.sendCommand(`exec-continue --thread ${threadId}`).then(
|
||||
(result) => {
|
||||
resolve(result.resultRecords.resultClass === 'running');
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
next(threadId: number, instruction: boolean): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const command = instruction ? 'exec-next-instruction' : 'exec-next';
|
||||
this.sendCommand(`${command} --thread ${threadId}`).then(
|
||||
(result) => {
|
||||
resolve(result.resultRecords.resultClass === 'running');
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
step(threadId: number, instruction: boolean): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const command = instruction ? 'exec-step-instruction' : 'exec-step';
|
||||
this.sendCommand(`${command} --thread ${threadId}`).then(
|
||||
(result) => {
|
||||
resolve(result.resultRecords.resultClass === 'running');
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
stepOut(threadId: number): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.sendCommand(`exec-finish --thread ${threadId}`).then(
|
||||
(result) => {
|
||||
resolve(result.resultRecords.resultClass === 'running');
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
restart(commands: string[]): Promise<boolean> {
|
||||
return this._sendCommandSequence(commands);
|
||||
}
|
||||
|
||||
_sendCommandSequence(commands: string[]): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const executeNext = ((remaining: string[]) => {
|
||||
if (remaining.length === 0) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
const cmd = remaining[0];
|
||||
this.sendCommand(cmd).then(
|
||||
(result) => {
|
||||
executeNext(remaining.slice(1));
|
||||
},
|
||||
reject
|
||||
);
|
||||
}).bind(this);
|
||||
executeNext(commands);
|
||||
});
|
||||
}
|
||||
|
||||
changeVariable(name: string, rawValue: string): Promise<MINode> {
|
||||
return this.sendCommand('gdb-set var ' + name + '=' + rawValue);
|
||||
}
|
||||
|
||||
setBreakPointCondition(breakpointNumber: number, condition: string): Promise<MINode> {
|
||||
return this.sendCommand('break-condition ' + breakpointNumber + ' ' + condition);
|
||||
}
|
||||
|
||||
addBreakPoint(breakpoint: any): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let args = '';
|
||||
|
||||
if (breakpoint.countCondition) {
|
||||
if (breakpoint.countCondition[0] === '>') {
|
||||
args += '-i ' + BREAK_COUNT_REGEX.exec(breakpoint.countCondition.substr(1))[0] + ' ';
|
||||
} else {
|
||||
const count = BREAK_COUNT_REGEX.exec(breakpoint.countCondition)[0];
|
||||
if (count.length !== breakpoint.countCondition.length) {
|
||||
this.log(
|
||||
'stderr',
|
||||
"Unsupported break count expression: '" +
|
||||
breakpoint.countCondition +
|
||||
"'. Only supports 'X' for breaking once after X times or '>X' for ignoring the first X breaks"
|
||||
);
|
||||
args += '-t ';
|
||||
} else if (parseInt(count) !== 0) {
|
||||
args += '-t -i ' + parseInt(count) + ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (breakpoint.raw) {
|
||||
args += '*' + escape(breakpoint.raw);
|
||||
} else {
|
||||
args += '"' + escape(breakpoint.file) + ':' + breakpoint.line + '"';
|
||||
}
|
||||
|
||||
this.sendCommand(`break-insert ${args}`).then(
|
||||
(result) => {
|
||||
if (result.resultRecords.resultClass === 'done') {
|
||||
const bkptNumber = parseInt(result.result('bkpt.number'));
|
||||
breakpoint.number = bkptNumber;
|
||||
if (breakpoint.condition) {
|
||||
this.setBreakPointCondition(bkptNumber, breakpoint.condition).then(
|
||||
(condResult) => {
|
||||
if (condResult.resultRecords.resultClass === 'done') {
|
||||
resolve(breakpoint);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
},
|
||||
reject
|
||||
);
|
||||
} else {
|
||||
resolve(breakpoint);
|
||||
}
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
removeBreakpoints(breakpointNumbers: number[]): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (breakpointNumbers.length === 0) {
|
||||
resolve(true);
|
||||
} else {
|
||||
const cmd = 'break-delete ' + breakpointNumbers.join(' ');
|
||||
this.sendCommand(cmd).then(
|
||||
(result) => {
|
||||
resolve(result.resultRecords.resultClass === 'done');
|
||||
},
|
||||
reject
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getFrame(threadId: number, frameLevel: number): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cmd = `stack-info-frame --thread ${threadId} --frame ${frameLevel}`;
|
||||
this.sendCommand(cmd).then(
|
||||
(result) => {
|
||||
const frame = result.result('frame');
|
||||
const level = MINode.valueOf(frame, 'level');
|
||||
const address = MINode.valueOf(frame, 'addr');
|
||||
const func = MINode.valueOf(frame, 'func');
|
||||
const file = MINode.valueOf(frame, 'file');
|
||||
const fullname = MINode.valueOf(frame, 'fullname');
|
||||
let line = 0;
|
||||
const lineStr = MINode.valueOf(frame, 'line');
|
||||
if (lineStr) {
|
||||
line = parseInt(lineStr);
|
||||
}
|
||||
resolve({
|
||||
address,
|
||||
fileName: file,
|
||||
file: fullname,
|
||||
function: func,
|
||||
level,
|
||||
line,
|
||||
});
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getStack(threadId: number, startFrame: number, levels: number): Promise<any[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.sendCommand(`stack-list-frames --thread ${threadId} ${startFrame} ${levels}`).then(
|
||||
(result) => {
|
||||
const stack = result.result('stack');
|
||||
const frames: any[] = [];
|
||||
stack.forEach((entry: any) => {
|
||||
const level = MINode.valueOf(entry, '@frame.level');
|
||||
const address = MINode.valueOf(entry, '@frame.addr');
|
||||
const func = MINode.valueOf(entry, '@frame.func');
|
||||
const file = MINode.valueOf(entry, '@frame.file');
|
||||
const fullname = MINode.valueOf(entry, '@frame.fullname');
|
||||
let line = 0;
|
||||
const lineStr = MINode.valueOf(entry, '@frame.line');
|
||||
if (lineStr) {
|
||||
line = parseInt(lineStr);
|
||||
}
|
||||
const from = parseInt(MINode.valueOf(entry, '@frame.from'));
|
||||
frames.push({
|
||||
address,
|
||||
fileName: file,
|
||||
file: fullname,
|
||||
function: func || from,
|
||||
level,
|
||||
line,
|
||||
});
|
||||
});
|
||||
resolve(frames);
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async getStackVariables(threadId: number, frameLevel: number): Promise<any[]> {
|
||||
const result = await this.sendCommand(
|
||||
`stack-list-variables --thread ${threadId} --frame ${frameLevel} --simple-values`
|
||||
);
|
||||
const variables = result.result('variables');
|
||||
const vars: any[] = [];
|
||||
for (const variable of variables) {
|
||||
const name = MINode.valueOf(variable, 'name');
|
||||
const valueStr = MINode.valueOf(variable, 'value');
|
||||
const type = MINode.valueOf(variable, 'type');
|
||||
vars.push({ name, valueStr, type, raw: variable });
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
|
||||
async examineMemory(address: number, length: number): Promise<string> {
|
||||
let result = '';
|
||||
let currentAddress = address;
|
||||
while (length > 0) {
|
||||
const chunkSize = length > 1024 ? 1024 : length;
|
||||
const response = await this.sendCommand(
|
||||
`data-read-memory-bytes 0x${currentAddress.toString(16)} ${chunkSize}`
|
||||
);
|
||||
result += response.result('memory[0].contents');
|
||||
length -= chunkSize;
|
||||
currentAddress += chunkSize;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
evalExpression(expression: string): Promise<MINode> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.sendCommand('data-evaluate-expression ' + expression).then(
|
||||
(result) => {
|
||||
resolve(result);
|
||||
},
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async varCreate(expression: string, name: string = '-'): Promise<VariableObject> {
|
||||
const result = await this.sendCommand(`var-create ${name} @ "${expression}"`);
|
||||
return new VariableObject(result.result(''));
|
||||
}
|
||||
|
||||
async varEvalExpression(name: string): Promise<MINode> {
|
||||
return this.sendCommand(`var-evaluate-expression ${name}`);
|
||||
}
|
||||
|
||||
async varListChildren(name: string): Promise<VariableObject[]> {
|
||||
const result = await this.sendCommand(`var-list-children --all-values ${name}`);
|
||||
return (result.result('children') || []).map((child: any) => new VariableObject(child[1]));
|
||||
}
|
||||
|
||||
async varUpdate(name: string = '*'): Promise<MINode> {
|
||||
return this.sendCommand(`var-update --all-values ${name}`);
|
||||
}
|
||||
|
||||
async varAssign(name: string, value: string): Promise<MINode> {
|
||||
return this.sendCommand(`var-assign ${name} ${value}`);
|
||||
}
|
||||
|
||||
logNoNewLine(type: string, message: string): void {
|
||||
this.emit('msg', type, message);
|
||||
}
|
||||
|
||||
log(type: string, message: string): void {
|
||||
this.emit('msg', type, message[message.length - 1] === '\n' ? message : message + '\n');
|
||||
}
|
||||
|
||||
sendUserInput(command: string): Promise<MINode> {
|
||||
if (command.startsWith('-')) {
|
||||
return this.sendCommand(command.substr(1));
|
||||
}
|
||||
return this.sendCommand(`interpreter-exec console "${command}"`);
|
||||
}
|
||||
|
||||
sendRaw(raw: string): void {
|
||||
if (this.printCalls) {
|
||||
this.log('log', raw);
|
||||
}
|
||||
this.process.stdin.write(raw + '\n');
|
||||
}
|
||||
|
||||
sendCommand(command: string, suppressErrors: boolean = false): Promise<MINode> {
|
||||
const token = this.currentToken++;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.handlers[token] = (result: MINode) => {
|
||||
if (result && result.resultRecords && result.resultRecords.resultClass === 'error') {
|
||||
if (suppressErrors) {
|
||||
this.log('stderr', `WARNING: Error executing command '${command}'`);
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(new MIError(result.result('msg') || 'Internal error', command));
|
||||
}
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
this.sendRaw(token + '-' + command);
|
||||
});
|
||||
}
|
||||
|
||||
isReady(): boolean {
|
||||
return !!this.process;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { MINode } from '../mi_parse';
|
||||
|
||||
export class VariableObject {
|
||||
public name: string;
|
||||
public exp: string;
|
||||
public numchild: number;
|
||||
public type: string;
|
||||
public value: string;
|
||||
public threadId: string;
|
||||
public frozen: boolean;
|
||||
public dynamic: boolean;
|
||||
public displayhint: string;
|
||||
public hasMore: boolean;
|
||||
public id: number;
|
||||
public fullExp: string;
|
||||
|
||||
constructor(node: any) {
|
||||
this.name = MINode.valueOf(node, 'name');
|
||||
this.exp = MINode.valueOf(node, 'exp');
|
||||
this.numchild = parseInt(MINode.valueOf(node, 'numchild'));
|
||||
this.type = MINode.valueOf(node, 'type');
|
||||
this.value = MINode.valueOf(node, 'value');
|
||||
this.threadId = MINode.valueOf(node, 'thread-id');
|
||||
this.frozen = !!MINode.valueOf(node, 'frozen');
|
||||
this.dynamic = !!MINode.valueOf(node, 'dynamic');
|
||||
this.displayhint = MINode.valueOf(node, 'displayhint');
|
||||
this.hasMore = !!MINode.valueOf(node, 'has_more');
|
||||
}
|
||||
|
||||
applyChanges(node: any): void {
|
||||
this.value = MINode.valueOf(node, 'value');
|
||||
if (MINode.valueOf(node, 'type_changed')) {
|
||||
this.type = MINode.valueOf(node, 'new_type');
|
||||
}
|
||||
this.dynamic = !!MINode.valueOf(node, 'dynamic');
|
||||
this.displayhint = MINode.valueOf(node, 'displayhint');
|
||||
this.hasMore = !!MINode.valueOf(node, 'has_more');
|
||||
}
|
||||
|
||||
isCompound(): boolean {
|
||||
return (
|
||||
this.numchild > 0 ||
|
||||
this.value === '{...}' ||
|
||||
(this.dynamic && (this.displayhint === 'array' || this.displayhint === 'map'))
|
||||
);
|
||||
}
|
||||
|
||||
toProtocolVariable(): any {
|
||||
return {
|
||||
name: this.exp,
|
||||
evaluateName: this.fullExp || this.exp,
|
||||
value: this.value === undefined ? '<unknown>' : this.value,
|
||||
type: this.type,
|
||||
presentationHint: { kind: this.displayhint },
|
||||
variablesReference: this.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class MIError {
|
||||
public readonly message: string;
|
||||
public readonly source: string;
|
||||
|
||||
constructor(message: string, source: string) {
|
||||
Object.defineProperty(this, 'name', {
|
||||
get: () => this.constructor.name,
|
||||
});
|
||||
Object.defineProperty(this, 'message', {
|
||||
get: () => message,
|
||||
});
|
||||
Object.defineProperty(this, 'source', {
|
||||
get: () => source,
|
||||
});
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `${(this as any).message} (from ${(this as any).source})`;
|
||||
}
|
||||
}
|
||||
|
||||
Object.setPrototypeOf(MIError, Object.create(Error.prototype));
|
||||
(MIError as any).prototype.constructor = MIError;
|
||||
@@ -0,0 +1,317 @@
|
||||
const OCTAL_ESCAPE_REGEX = /^[0-7]{3}/;
|
||||
|
||||
export class MINode {
|
||||
public token: number;
|
||||
public outOfBandRecord: any[];
|
||||
public resultRecords: any;
|
||||
|
||||
constructor(token: number, outOfBandRecord: any[], resultRecords: any) {
|
||||
this.token = token;
|
||||
this.outOfBandRecord = outOfBandRecord;
|
||||
this.resultRecords = resultRecords;
|
||||
}
|
||||
|
||||
static valueOf(startNode: any, path: string): any {
|
||||
if (!startNode) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const KEY_REGEX = /^\.?([a-zA-Z_\-][a-zA-Z0-9_\-]*)/;
|
||||
const INDEX_REGEX = /^\[(\d+)\](?:$|\.)/;
|
||||
|
||||
path = path.trim();
|
||||
if (!path) {
|
||||
return startNode;
|
||||
}
|
||||
|
||||
let current = startNode;
|
||||
|
||||
do {
|
||||
let match = KEY_REGEX.exec(path);
|
||||
if (match) {
|
||||
path = path.substr(match[0].length);
|
||||
if (!current.length || typeof current === 'string') {
|
||||
return undefined;
|
||||
} else {
|
||||
const matches: any[] = [];
|
||||
for (const item of current) {
|
||||
if (item[0] === match[1]) {
|
||||
matches.push(item[1]);
|
||||
}
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
current = matches;
|
||||
} else if (matches.length === 1) {
|
||||
current = matches[0];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
} else if (path[0] === '@') {
|
||||
current = [current];
|
||||
path = path.substr(1);
|
||||
} else {
|
||||
match = INDEX_REGEX.exec(path);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
path = path.substr(match[0].length);
|
||||
const index = parseInt(match[1]);
|
||||
if (current.length && typeof current !== 'string' && index >= 0 && index < current.length) {
|
||||
current = current[index];
|
||||
} else if (index !== 0) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
path = path.trim();
|
||||
} while (path);
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
record(path: string): any {
|
||||
if (this.outOfBandRecord) {
|
||||
return MINode.valueOf(this.outOfBandRecord[0].output, path);
|
||||
}
|
||||
}
|
||||
|
||||
result(path: string): any {
|
||||
if (this.resultRecords) {
|
||||
return MINode.valueOf(this.resultRecords.results, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const OUT_OF_BAND_REGEX = /^(?:(\d*|undefined)([\*\+\=])|([\~\@\&]))/;
|
||||
const RESULT_REGEX = /^(\d*)\^(done|running|connected|error|exit)/;
|
||||
const NEWLINE_REGEX = /^\r\n?/;
|
||||
const VARIABLE_NAME_REGEX = /^([a-zA-Z_\-][a-zA-Z0-9_\-]*)/;
|
||||
const ASYNC_CLASS_REGEX = /^(.*?),/;
|
||||
|
||||
function parseString(input: string): [string, string] {
|
||||
if (input[0] !== '"') {
|
||||
return ['', input];
|
||||
}
|
||||
|
||||
let pos = 1;
|
||||
let scanning = true;
|
||||
let str = input.substr(1);
|
||||
let escaped = false;
|
||||
|
||||
while (scanning) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (str[0] === '\\') {
|
||||
escaped = true;
|
||||
} else if (str[0] === '"') {
|
||||
scanning = false;
|
||||
}
|
||||
str = str.substr(1);
|
||||
pos++;
|
||||
}
|
||||
|
||||
let result: string;
|
||||
try {
|
||||
result = parseCString(input.substr(0, pos));
|
||||
} catch (e) {
|
||||
result = input.substr(0, pos);
|
||||
}
|
||||
|
||||
return [result, input.substr(pos)];
|
||||
}
|
||||
|
||||
function parseCString(str: string): string {
|
||||
const buffer = Buffer.alloc(str.length * 4);
|
||||
let offset = 0;
|
||||
|
||||
if (str[0] !== '"' || str[str.length - 1] !== '"') {
|
||||
throw new Error('Not a valid string');
|
||||
}
|
||||
str = str.slice(1, -1);
|
||||
|
||||
let escaped = false;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (escaped) {
|
||||
let octalMatch;
|
||||
if (str[i] === '\\') {
|
||||
offset += buffer.write('\\', offset);
|
||||
} else if (str[i] === '"') {
|
||||
offset += buffer.write('"', offset);
|
||||
} else if (str[i] === "'") {
|
||||
offset += buffer.write("'", offset);
|
||||
} else if (str[i] === 'n') {
|
||||
offset += buffer.write('\n', offset);
|
||||
} else if (str[i] === 'r') {
|
||||
offset += buffer.write('\r', offset);
|
||||
} else if (str[i] === 't') {
|
||||
offset += buffer.write('\t', offset);
|
||||
} else if (str[i] === 'b') {
|
||||
offset += buffer.write('\b', offset);
|
||||
} else if (str[i] === 'f') {
|
||||
offset += buffer.write('\f', offset);
|
||||
} else if (str[i] === 'v') {
|
||||
offset += buffer.write('\v', offset);
|
||||
} else if (str[i] === '0') {
|
||||
offset += buffer.write('\0', offset);
|
||||
} else if ((octalMatch = OCTAL_ESCAPE_REGEX.exec(str.substr(i)))) {
|
||||
buffer.writeUInt8(parseInt(octalMatch[0], 8), offset++);
|
||||
i += 2;
|
||||
} else {
|
||||
offset += buffer.write(str[i], offset);
|
||||
}
|
||||
escaped = false;
|
||||
} else if (str[i] === '\\') {
|
||||
escaped = true;
|
||||
} else {
|
||||
if (str[i] === '"') {
|
||||
throw new Error('Not a valid string');
|
||||
}
|
||||
offset += buffer.write(str[i], offset);
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.slice(0, offset).toString('utf8');
|
||||
}
|
||||
|
||||
export function parseMI(output: string): MINode {
|
||||
let token: number;
|
||||
const outOfBandRecords: any[] = [];
|
||||
let resultRecords: any;
|
||||
|
||||
const asyncClassMap: { [key: string]: string } = {
|
||||
'*': 'exec',
|
||||
'+': 'status',
|
||||
'=': 'notify',
|
||||
};
|
||||
|
||||
const streamClassMap: { [key: string]: string } = {
|
||||
'~': 'console',
|
||||
'@': 'target',
|
||||
'&': 'log',
|
||||
};
|
||||
|
||||
let parseValue: () => any;
|
||||
let parseResult: () => any;
|
||||
let parseCommaValue: () => any;
|
||||
let parseCommaResult: () => any;
|
||||
|
||||
parseValue = (): any => {
|
||||
if (output[0] === '"') {
|
||||
const [str, rest] = parseString(output);
|
||||
output = rest;
|
||||
return str;
|
||||
} else if (output[0] === '{' || output[0] === '[') {
|
||||
const isList = output[0] === '[';
|
||||
output = output.substr(1);
|
||||
if (output[0] === '}' || output[0] === ']') {
|
||||
output = output.substr(1);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isList) {
|
||||
let val = parseValue();
|
||||
if (val) {
|
||||
const arr: any[] = [];
|
||||
arr.push(val);
|
||||
while ((val = parseCommaValue()) !== undefined) {
|
||||
arr.push(val);
|
||||
}
|
||||
output = output.substr(1);
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
let result = parseResult();
|
||||
if (result) {
|
||||
const arr: any[] = [];
|
||||
arr.push(result);
|
||||
while ((result = parseCommaResult())) {
|
||||
arr.push(result);
|
||||
}
|
||||
output = output.substr(1);
|
||||
return arr;
|
||||
}
|
||||
|
||||
output = (isList ? '[' : '{') + output;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
parseResult = (): any => {
|
||||
const match = VARIABLE_NAME_REGEX.exec(output);
|
||||
if (match) {
|
||||
output = output.substr(match[0].length + 1); // +1 for '='
|
||||
return [match[1], parseValue()];
|
||||
}
|
||||
};
|
||||
|
||||
parseCommaValue = (): any => {
|
||||
if (output[0] === ',') {
|
||||
output = output.substr(1);
|
||||
return parseValue();
|
||||
}
|
||||
};
|
||||
|
||||
parseCommaResult = (): any => {
|
||||
if (output[0] === ',') {
|
||||
output = output.substr(1);
|
||||
return parseResult();
|
||||
}
|
||||
};
|
||||
|
||||
let match: RegExpExecArray;
|
||||
|
||||
while ((match = OUT_OF_BAND_REGEX.exec(output))) {
|
||||
output = output.substr(match[0].length);
|
||||
|
||||
if (match[1] && token === undefined && match[1] !== 'undefined') {
|
||||
token = parseInt(match[1]);
|
||||
}
|
||||
|
||||
if (match[2]) {
|
||||
const classMatch = ASYNC_CLASS_REGEX.exec(output);
|
||||
output = output.substr(classMatch[1].length);
|
||||
const record: any = {
|
||||
isStream: false,
|
||||
type: asyncClassMap[match[2]],
|
||||
asyncClass: classMatch[1],
|
||||
output: [],
|
||||
};
|
||||
let res;
|
||||
while ((res = parseCommaResult())) {
|
||||
record.output.push(res);
|
||||
}
|
||||
outOfBandRecords.push(record);
|
||||
} else if (match[3]) {
|
||||
const [content, rest] = parseString(output);
|
||||
output = rest;
|
||||
const record: any = {
|
||||
isStream: true,
|
||||
type: streamClassMap[match[3]],
|
||||
content,
|
||||
};
|
||||
outOfBandRecords.push(record);
|
||||
}
|
||||
|
||||
output = output.replace(NEWLINE_REGEX, '');
|
||||
}
|
||||
|
||||
if ((match = RESULT_REGEX.exec(output))) {
|
||||
output = output.substr(match[0].length);
|
||||
if (match[1] && token === undefined) {
|
||||
token = parseInt(match[1]);
|
||||
}
|
||||
resultRecords = {
|
||||
resultClass: match[2],
|
||||
results: [],
|
||||
};
|
||||
let res;
|
||||
while ((res = parseCommaResult())) {
|
||||
resultRecords.results.push(res);
|
||||
}
|
||||
output = output.replace(NEWLINE_REGEX, '');
|
||||
}
|
||||
|
||||
return new MINode(token, outOfBandRecords || [], resultRecords);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import * as childProcess from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { SymbolType, SymbolScope } from '../common';
|
||||
|
||||
const SYMBOL_REGEX = /^([0-9a-f]{8})\s([lg\ !])([w\ ])([C\ ])([W\ ])([I\ ])([dD\ ])([FfO\ ])\s([^\s]+)\s([0-9a-f]+)\s(.*)\r?$/;
|
||||
const DEMANGLED_NAME_REGEX = /^_Z[^\d]*(\d+)(.+)$/;
|
||||
|
||||
const TYPE_MAP: { [key: string]: SymbolType } = {
|
||||
'F': SymbolType.Function,
|
||||
'f': SymbolType.File,
|
||||
'O': SymbolType.Object,
|
||||
' ': SymbolType.Normal,
|
||||
};
|
||||
|
||||
const SCOPE_MAP: { [key: string]: SymbolScope } = {
|
||||
'l': SymbolScope.Local,
|
||||
'g': SymbolScope.Global,
|
||||
' ': SymbolScope.Neither,
|
||||
'!': SymbolScope.Both,
|
||||
};
|
||||
|
||||
export interface SymbolInformation {
|
||||
address: number;
|
||||
type: SymbolType;
|
||||
scope: SymbolScope;
|
||||
section: string;
|
||||
length: number;
|
||||
name: string;
|
||||
file: string | null;
|
||||
instructions: any[] | null;
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
export class SymbolTable {
|
||||
private symbols: SymbolInformation[] = [];
|
||||
|
||||
constructor(
|
||||
private toolchainBinDir: string,
|
||||
private executable: string
|
||||
) {}
|
||||
|
||||
loadSymbols(): void {
|
||||
let objdumpPath = '';
|
||||
fs.readdirSync(this.toolchainBinDir).forEach((file) => {
|
||||
if (file.includes('objdump') && fs.existsSync(path.join(this.toolchainBinDir, file))) {
|
||||
objdumpPath = path.join(this.toolchainBinDir, file);
|
||||
}
|
||||
});
|
||||
|
||||
if (!objdumpPath) {
|
||||
throw new Error('Could not find "objdump" program');
|
||||
}
|
||||
|
||||
const lines = childProcess
|
||||
.spawnSync(objdumpPath, ['--syms', this.executable])
|
||||
.stdout.toString()
|
||||
.split('\n');
|
||||
|
||||
let currentFile: string | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(SYMBOL_REGEX);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = TYPE_MAP[match[8]];
|
||||
const scope = SCOPE_MAP[match[2]];
|
||||
let name = match[11].trim();
|
||||
let hidden = false;
|
||||
|
||||
if (match[7] === 'd' && match[8] === 'f') {
|
||||
currentFile = name;
|
||||
} else {
|
||||
if (name.startsWith('.hidden')) {
|
||||
name = name.substring(7).trim();
|
||||
hidden = true;
|
||||
}
|
||||
|
||||
const demangledMatch = name.match(DEMANGLED_NAME_REGEX);
|
||||
if (demangledMatch) {
|
||||
if (type !== SymbolType.Function) {
|
||||
continue;
|
||||
}
|
||||
const nameLength = parseInt(demangledMatch[1]);
|
||||
name = demangledMatch[2].substr(0, nameLength);
|
||||
|
||||
if (demangledMatch[2].length > nameLength) {
|
||||
const nestedMatch = demangledMatch[2].substr(nameLength).match(/^(\d+)(.+)$/);
|
||||
if (nestedMatch) {
|
||||
name += '::' + nestedMatch[2].substr(0, parseInt(nestedMatch[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.symbols.push({
|
||||
address: parseInt(match[1], 16),
|
||||
type,
|
||||
scope,
|
||||
section: match[9].trim(),
|
||||
length: parseInt(match[10], 16),
|
||||
name,
|
||||
file: scope === SymbolScope.Local ? currentFile : null,
|
||||
instructions: null,
|
||||
hidden,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getFunctionAtAddress(address: number): SymbolInformation | undefined {
|
||||
const matches = this.symbols.filter(
|
||||
(sym) =>
|
||||
sym.type === SymbolType.Function &&
|
||||
sym.address <= address &&
|
||||
sym.address + sym.length > address
|
||||
);
|
||||
if (matches && matches.length !== 0) {
|
||||
return matches[0];
|
||||
}
|
||||
}
|
||||
|
||||
getFunctionSymbols(): SymbolInformation[] {
|
||||
return this.symbols.filter((sym) => sym.type === SymbolType.Function);
|
||||
}
|
||||
|
||||
getGlobalVariables(): SymbolInformation[] {
|
||||
return this.symbols.filter(
|
||||
(sym) => sym.type === SymbolType.Object && sym.scope === SymbolScope.Global
|
||||
);
|
||||
}
|
||||
|
||||
getStaticVariables(file: string): SymbolInformation[] {
|
||||
return this.symbols.filter(
|
||||
(sym) =>
|
||||
sym.type === SymbolType.Object &&
|
||||
sym.scope === SymbolScope.Local &&
|
||||
sym.file === file
|
||||
);
|
||||
}
|
||||
|
||||
getFunctionByName(name: string, file: string): SymbolInformation | null {
|
||||
let matches = this.symbols.filter(
|
||||
(sym) =>
|
||||
sym.type === SymbolType.Function &&
|
||||
sym.scope === SymbolScope.Local &&
|
||||
sym.name === name &&
|
||||
sym.file === file
|
||||
);
|
||||
if (matches.length !== 0) {
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
matches = this.symbols.filter(
|
||||
(sym) =>
|
||||
sym.type === SymbolType.Function &&
|
||||
sym.scope !== SymbolScope.Local &&
|
||||
sym.name === name
|
||||
);
|
||||
if (matches.length !== 0) {
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Event } from 'vscode-debugadapter';
|
||||
|
||||
export enum NumberFormat {
|
||||
Auto = 0,
|
||||
Hexidecimal = 1,
|
||||
Decimal = 2,
|
||||
Binary = 3,
|
||||
}
|
||||
|
||||
export class AdapterOutputEvent extends Event {
|
||||
constructor(content: string, type: string) {
|
||||
super('adapter-output', { content, type });
|
||||
}
|
||||
}
|
||||
|
||||
export class StoppedEvent extends Event {
|
||||
constructor(reason: string, threadId: number, allThreadsStopped: boolean) {
|
||||
super('stopped', { reason, threadId, allThreadsStopped });
|
||||
}
|
||||
}
|
||||
|
||||
export class TelemetryEvent extends Event {
|
||||
constructor(category: string, action: string, label: string, parameters: any = {}) {
|
||||
super('record-event', { category, action, label, parameters });
|
||||
}
|
||||
}
|
||||
|
||||
export enum SymbolType {
|
||||
Function = 0,
|
||||
File = 1,
|
||||
Object = 2,
|
||||
Normal = 3,
|
||||
}
|
||||
|
||||
export enum SymbolScope {
|
||||
Local = 0,
|
||||
Global = 1,
|
||||
Neither = 2,
|
||||
Both = 3,
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
import * as copyPaste from 'copy-paste';
|
||||
import * as vscode from 'vscode';
|
||||
import { NumberFormat, SymbolScope } from './common';
|
||||
import { encodeDisassembly } from './utils';
|
||||
import { PlatformIODebugConfigurationProvider } from './frontend/configprovider';
|
||||
import { DisassemblyContentProvider } from './frontend/disassembly_content_provider';
|
||||
import { DisassemblyTreeProvider } from './frontend/disassembly_tree_provider';
|
||||
import { MemoryContentProvider } from './frontend/memory_content_provider';
|
||||
import { MemoryTreeProvider } from './frontend/memory_tree_provider';
|
||||
import { PeripheralTreeProvider, RecordType as PeripheralRecordType } from './frontend/peripheral';
|
||||
import { RegisterTreeProvider, RecordType as RegisterRecordType } from './frontend/registers';
|
||||
|
||||
class PlatformIODebugExtension {
|
||||
private adapterOutputChannel: vscode.OutputChannel = null;
|
||||
private functionSymbols: any[] = null;
|
||||
private context: vscode.ExtensionContext;
|
||||
private registerProvider: RegisterTreeProvider;
|
||||
private peripheralProvider: PeripheralTreeProvider;
|
||||
private memoryTreeProvider: MemoryTreeProvider;
|
||||
private disassemblyTreeProvider: DisassemblyTreeProvider;
|
||||
private memoryContentProvider: MemoryContentProvider;
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this.context = context;
|
||||
this.registerProvider = new RegisterTreeProvider();
|
||||
this.peripheralProvider = new PeripheralTreeProvider();
|
||||
this.memoryTreeProvider = new MemoryTreeProvider();
|
||||
this.disassemblyTreeProvider = new DisassemblyTreeProvider();
|
||||
this.memoryContentProvider = new MemoryContentProvider();
|
||||
|
||||
const peripheralTreeView = vscode.window.createTreeView('platformio-debug.peripherals', {
|
||||
treeDataProvider: this.peripheralProvider,
|
||||
});
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.debug.registerDebugConfigurationProvider(
|
||||
'platformio-debug',
|
||||
new PlatformIODebugConfigurationProvider()
|
||||
),
|
||||
peripheralTreeView,
|
||||
peripheralTreeView.onDidExpandElement(
|
||||
this.peripheralProvider.onDidExpandElement.bind(this.peripheralProvider)
|
||||
),
|
||||
peripheralTreeView.onDidCollapseElement(
|
||||
this.peripheralProvider.onDidCollapseElement.bind(this.peripheralProvider)
|
||||
),
|
||||
vscode.window.registerTreeDataProvider('platformio-debug.registers', this.registerProvider),
|
||||
vscode.window.registerTreeDataProvider('platformio-debug.memory', this.memoryTreeProvider),
|
||||
vscode.window.registerTreeDataProvider('platformio-debug.disassembly', this.disassemblyTreeProvider),
|
||||
vscode.workspace.registerTextDocumentContentProvider('examinememory', this.memoryContentProvider),
|
||||
vscode.workspace.registerTextDocumentContentProvider('disassembly', new DisassemblyContentProvider()),
|
||||
|
||||
vscode.commands.registerCommand('platformio-debug.peripherals.updateNode', this.peripheralsUpdateNode.bind(this)),
|
||||
vscode.commands.registerCommand('platformio-debug.peripherals.selectedNode', this.peripheralsSelectedNode.bind(this)),
|
||||
vscode.commands.registerCommand('platformio-debug.peripherals.copyValue', this.peripheralsCopyValue.bind(this)),
|
||||
vscode.commands.registerCommand('platformio-debug.peripherals.setFormat', this.peripheralsSetFormat.bind(this)),
|
||||
vscode.commands.registerCommand('platformio-debug.registers.selectedNode', this.registersSelectedNode.bind(this)),
|
||||
vscode.commands.registerCommand('platformio-debug.registers.copyValue', this.registersCopyValue.bind(this)),
|
||||
vscode.commands.registerCommand('platformio-debug.registers.setFormat', this.registersSetFormat.bind(this)),
|
||||
vscode.commands.registerCommand('platformio-debug.memory.deleteHistoryItem', this.memoryDeleteHistoryItem.bind(this)),
|
||||
vscode.commands.registerCommand('platformio-debug.memory.clearHistory', this.memoryClearHistory.bind(this)),
|
||||
vscode.commands.registerCommand('platformio-debug.examineMemory', this.examineMemory.bind(this)),
|
||||
vscode.commands.registerCommand('platformio-debug.viewDisassembly', this.showDisassembly.bind(this)),
|
||||
vscode.commands.registerCommand('platformio-debug.setForceDisassembly', this.setForceDisassembly.bind(this)),
|
||||
|
||||
vscode.debug.onDidReceiveDebugSessionCustomEvent(this.receivedCustomEvent.bind(this)),
|
||||
vscode.debug.onDidStartDebugSession(this.debugSessionStarted.bind(this)),
|
||||
vscode.debug.onDidTerminateDebugSession(this.debugSessionTerminated.bind(this)),
|
||||
vscode.window.onDidChangeActiveTextEditor(this.activeEditorChanged.bind(this)),
|
||||
vscode.window.onDidChangeTextEditorSelection((e) => {
|
||||
if (e && e.textEditor.document.fileName.endsWith('.dbgmem')) {
|
||||
this.memoryContentProvider.handleSelection(e);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private isPIODebugSession(): boolean {
|
||||
return vscode.debug.activeDebugSession && vscode.debug.activeDebugSession.type === 'platformio-debug';
|
||||
}
|
||||
|
||||
private activeEditorChanged(editor: vscode.TextEditor): void {
|
||||
if (!editor || !this.isPIODebugSession()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uri = editor.document.uri;
|
||||
if (uri.scheme === 'file') {
|
||||
vscode.debug.activeDebugSession.customRequest('set-active-editor', { path: uri.path });
|
||||
} else if (uri.scheme === 'disassembly') {
|
||||
vscode.debug.activeDebugSession.customRequest('set-active-editor', {
|
||||
path: `${uri.scheme}://${uri.authority}${uri.path}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async showDisassembly(): Promise<void> {
|
||||
if (!this.isPIODebugSession()) {
|
||||
vscode.window.showErrorMessage('No debugging session available');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.functionSymbols) {
|
||||
try {
|
||||
const result = await vscode.debug.activeDebugSession.customRequest('load-function-symbols');
|
||||
this.functionSymbols = result.functionSymbols;
|
||||
} catch (e) {
|
||||
vscode.window.showErrorMessage('Unable to load symbol table. Disassembly view unavailable.');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const funcName = await vscode.window.showInputBox({
|
||||
placeHolder: 'main',
|
||||
ignoreFocusOut: true,
|
||||
prompt: 'Function Name to Disassemble',
|
||||
});
|
||||
|
||||
const matches = this.functionSymbols.filter((s) => s.name === funcName);
|
||||
let uri: string;
|
||||
|
||||
if (matches.length === 1) {
|
||||
uri = encodeDisassembly(matches[0].name, matches[0].file);
|
||||
} else if (matches.length > 1) {
|
||||
const selected = await vscode.window.showQuickPick(
|
||||
matches.map((m) => ({
|
||||
label: m.name,
|
||||
name: m.name,
|
||||
file: m.file,
|
||||
scope: m.scope,
|
||||
description:
|
||||
m.scope === SymbolScope.Global ? 'Global Scope' : `Static in ${m.file}`,
|
||||
})),
|
||||
{ ignoreFocusOut: true }
|
||||
);
|
||||
uri = encodeDisassembly(selected.name, selected.file);
|
||||
} else {
|
||||
vscode.window.showErrorMessage(`No function with name ${funcName} found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (uri) {
|
||||
vscode.window.showTextDocument(vscode.Uri.parse(uri));
|
||||
}
|
||||
} catch (e) {
|
||||
vscode.window.showErrorMessage('Unable to show disassembly.');
|
||||
}
|
||||
}
|
||||
|
||||
private setForceDisassembly(force?: string): void {
|
||||
const doSet = (value: string) => {
|
||||
const forced = value === 'Forced';
|
||||
this.disassemblyTreeProvider.updateForcedState(forced);
|
||||
return vscode.debug.activeDebugSession.customRequest('set-force-disassembly', { force: forced });
|
||||
};
|
||||
|
||||
if (force) {
|
||||
return doSet(force) as any;
|
||||
}
|
||||
|
||||
vscode.window
|
||||
.showQuickPick(
|
||||
[
|
||||
{
|
||||
label: 'Auto',
|
||||
description: 'Show disassembly for functions when source cannot be located.',
|
||||
},
|
||||
{
|
||||
label: 'Forced',
|
||||
description: 'Always show disassembly for functions.',
|
||||
},
|
||||
],
|
||||
{ matchOnDescription: true, ignoreFocusOut: true }
|
||||
)
|
||||
.then(
|
||||
(selected) => {
|
||||
doSet(selected.label);
|
||||
},
|
||||
(err) => {}
|
||||
);
|
||||
}
|
||||
|
||||
private memoryDeleteHistoryItem(item: any): void {
|
||||
const [address, length] = item.label.split('+');
|
||||
this.memoryTreeProvider.deleteHistory(address, length);
|
||||
}
|
||||
|
||||
private memoryClearHistory(): void {
|
||||
this.memoryTreeProvider.clearHistory();
|
||||
}
|
||||
|
||||
private examineMemory(address?: string, length?: string): any {
|
||||
function validateInput(input: string): string | null {
|
||||
if (/^0x[0-9a-f]{1,8}$/i.test(input) || /^[0-9]+$/i.test(input)) {
|
||||
return input;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.isPIODebugSession()) {
|
||||
vscode.window.showErrorMessage('No debugging session available');
|
||||
return;
|
||||
}
|
||||
|
||||
if (address && length) {
|
||||
return this.showMemoryContent(address, length);
|
||||
}
|
||||
|
||||
vscode.window
|
||||
.showInputBox({
|
||||
placeHolder: 'Prefix with 0x for hexidecimal format',
|
||||
ignoreFocusOut: true,
|
||||
prompt: 'A start memory address',
|
||||
})
|
||||
.then(
|
||||
(addressInput) => {
|
||||
if (validateInput(addressInput)) {
|
||||
vscode.window
|
||||
.showInputBox({
|
||||
placeHolder: 'Prefix with 0x for hexidecimal format',
|
||||
ignoreFocusOut: true,
|
||||
prompt: 'How many bytes to read?',
|
||||
})
|
||||
.then(
|
||||
(lengthInput) => {
|
||||
if (validateInput(lengthInput)) {
|
||||
this.memoryTreeProvider.pushHistory(addressInput, lengthInput);
|
||||
this.showMemoryContent(addressInput, lengthInput);
|
||||
} else {
|
||||
vscode.window.showErrorMessage('Invalid length entered');
|
||||
}
|
||||
},
|
||||
(err) => {}
|
||||
);
|
||||
} else {
|
||||
vscode.window.showErrorMessage('Invalid memory address entered');
|
||||
}
|
||||
},
|
||||
(err) => {}
|
||||
);
|
||||
}
|
||||
|
||||
private showMemoryContent(address: string, length: string): void {
|
||||
vscode.workspace
|
||||
.openTextDocument(
|
||||
vscode.Uri.parse(
|
||||
`examinememory:///Memory%20[${address}+${length}].dbgmem?address=${address}&length=${length}×tamp=${new Date().getTime()}`
|
||||
)
|
||||
)
|
||||
.then(
|
||||
(doc) => {
|
||||
vscode.window.showTextDocument(doc, { viewColumn: 2, preview: false });
|
||||
},
|
||||
(error) => {
|
||||
vscode.window.showErrorMessage(`Failed to examine memory: ${error}`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private peripheralsUpdateNode(node: any): void {
|
||||
node.node.performUpdate().then(
|
||||
(result: boolean) => {
|
||||
if (result) {
|
||||
this.peripheralProvider.refresh();
|
||||
}
|
||||
},
|
||||
(error: any) => {
|
||||
vscode.window.showErrorMessage(`Unable to update value: ${error.toString()}`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private peripheralsSelectedNode(node: any): void {
|
||||
if (node.recordType !== PeripheralRecordType.Field) {
|
||||
node.expanded = !node.expanded;
|
||||
}
|
||||
node.selected().then(
|
||||
(result: boolean) => {
|
||||
if (result) {
|
||||
this.peripheralProvider.refresh();
|
||||
}
|
||||
},
|
||||
(error: any) => {}
|
||||
);
|
||||
}
|
||||
|
||||
private peripheralsCopyValue(node: any): void {
|
||||
const value = node.node.getCopyValue();
|
||||
if (value) {
|
||||
copyPaste.copy(value);
|
||||
}
|
||||
}
|
||||
|
||||
private async peripheralsSetFormat(node: any): Promise<void> {
|
||||
const selected = await vscode.window.showQuickPick([
|
||||
{ label: 'Auto', description: 'Automatically choose format (Inherits from parent)', value: NumberFormat.Auto },
|
||||
{ label: 'Hex', description: 'Format value in hexidecimal', value: NumberFormat.Hexidecimal },
|
||||
{ label: 'Decimal', description: 'Format value in decimal', value: NumberFormat.Decimal },
|
||||
{ label: 'Binary', description: 'Format value in binary', value: NumberFormat.Binary },
|
||||
]);
|
||||
node.node.setFormat(selected.value);
|
||||
this.peripheralProvider.refresh();
|
||||
}
|
||||
|
||||
private registersSelectedNode(node: any): void {
|
||||
if (node.recordType !== RegisterRecordType.Field) {
|
||||
node.expanded = !node.expanded;
|
||||
}
|
||||
}
|
||||
|
||||
private registersCopyValue(node: any): void {
|
||||
const value = node.node.getCopyValue();
|
||||
if (value) {
|
||||
copyPaste.copy(value);
|
||||
}
|
||||
}
|
||||
|
||||
private async registersSetFormat(node: any): Promise<void> {
|
||||
const selected = await vscode.window.showQuickPick([
|
||||
{ label: 'Auto', description: 'Automatically choose format (Inherits from parent)', value: NumberFormat.Auto },
|
||||
{ label: 'Hex', description: 'Format value in hexidecimal', value: NumberFormat.Hexidecimal },
|
||||
{ label: 'Decimal', description: 'Format value in decimal', value: NumberFormat.Decimal },
|
||||
{ label: 'Binary', description: 'Format value in binary', value: NumberFormat.Binary },
|
||||
]);
|
||||
node.node.setFormat(selected.value);
|
||||
this.registerProvider.refresh();
|
||||
}
|
||||
|
||||
private debugSessionStarted(session: vscode.DebugSession): void {
|
||||
if (session.type === 'platformio-debug') {
|
||||
this.functionSymbols = null;
|
||||
session.customRequest('get-arguments').then(
|
||||
(args: any) => {
|
||||
this.registerProvider.debugSessionStarted(
|
||||
this.context.workspaceState.get('debugRegistersTreeState')
|
||||
);
|
||||
this.peripheralProvider.debugSessionStarted(
|
||||
args.svdPath,
|
||||
this.context.workspaceState.get('debugPeripheralsTreeState')
|
||||
);
|
||||
this.memoryTreeProvider.debugSessionStarted(
|
||||
this.context.workspaceState.get('debugMemoryTreeState')
|
||||
);
|
||||
this.disassemblyTreeProvider.debugSessionStarted();
|
||||
},
|
||||
(error: any) => {
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private debugSessionTerminated(session: vscode.DebugSession): void {
|
||||
if (session.type === 'platformio-debug') {
|
||||
this.context.workspaceState.update(
|
||||
'debugRegistersTreeState',
|
||||
this.registerProvider.dumpSettings()
|
||||
);
|
||||
this.context.workspaceState.update(
|
||||
'debugPeripheralsTreeState',
|
||||
this.peripheralProvider.dumpSettings()
|
||||
);
|
||||
this.context.workspaceState.update(
|
||||
'debugMemoryTreeState',
|
||||
this.memoryTreeProvider.dumpSettings()
|
||||
);
|
||||
|
||||
this.registerProvider.debugSessionTerminated();
|
||||
this.peripheralProvider.debugSessionTerminated();
|
||||
this.memoryTreeProvider.debugSessionTerminated();
|
||||
this.disassemblyTreeProvider.debugSessionTerminated();
|
||||
}
|
||||
}
|
||||
|
||||
private receivedCustomEvent(e: vscode.DebugSessionCustomEvent): void {
|
||||
if (!this.isPIODebugSession()) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.event) {
|
||||
case 'custom-stop':
|
||||
this.receivedStopEvent(e);
|
||||
break;
|
||||
case 'custom-continued':
|
||||
this.receivedContinuedEvent(e);
|
||||
break;
|
||||
case 'adapter-output':
|
||||
this.receivedAdapterOutput(e);
|
||||
break;
|
||||
case 'record-event':
|
||||
this.receivedEvent(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private receivedStopEvent(e: vscode.DebugSessionCustomEvent): void {
|
||||
this.peripheralProvider.debugStopped();
|
||||
this.registerProvider.debugStopped();
|
||||
|
||||
vscode.workspace.textDocuments
|
||||
.filter((doc) => doc.fileName.endsWith('.dbgmem'))
|
||||
.forEach((doc) => {
|
||||
this.memoryContentProvider.update(doc);
|
||||
});
|
||||
}
|
||||
|
||||
private receivedContinuedEvent(e: vscode.DebugSessionCustomEvent): void {
|
||||
this.peripheralProvider.debugContinued();
|
||||
this.registerProvider.debugContinued();
|
||||
}
|
||||
|
||||
private receivedEvent(e: vscode.DebugSessionCustomEvent): void {}
|
||||
|
||||
private receivedAdapterOutput(e: vscode.DebugSessionCustomEvent): void {
|
||||
if (!this.adapterOutputChannel) {
|
||||
this.adapterOutputChannel = vscode.window.createOutputChannel('Adapter Output');
|
||||
}
|
||||
|
||||
let content: string = e.body.content;
|
||||
if (!content.endsWith('\n')) {
|
||||
content += '\n';
|
||||
}
|
||||
this.adapterOutputChannel.append(content);
|
||||
}
|
||||
}
|
||||
|
||||
export function activate(context: vscode.ExtensionContext): PlatformIODebugExtension {
|
||||
return new PlatformIODebugExtension(context);
|
||||
}
|
||||
|
||||
export function deactivate(): void {}
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class PlatformIODebugConfigurationProvider implements vscode.DebugConfigurationProvider {
|
||||
constructor() {}
|
||||
|
||||
async resolveDebugConfiguration(
|
||||
folder: vscode.WorkspaceFolder | undefined,
|
||||
config: vscode.DebugConfiguration,
|
||||
token?: vscode.CancellationToken
|
||||
): Promise<vscode.DebugConfiguration> {
|
||||
(config as any).cwd = folder ? folder.uri.fsPath : vscode.workspace.rootPath;
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { parseQuery } from '../utils';
|
||||
|
||||
export class DisassemblyContentProvider implements vscode.TextDocumentContentProvider {
|
||||
provideTextDocumentContent(uri: vscode.Uri, token: vscode.CancellationToken): Thenable<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const params = parseQuery(uri.query);
|
||||
vscode.debug.activeDebugSession
|
||||
.customRequest('disassemble', { function: params.func, file: params.file })
|
||||
.then(
|
||||
(result: any) => {
|
||||
const instructions = result.instructions;
|
||||
let output = '';
|
||||
instructions.forEach((instruction: any) => {
|
||||
output += `${instruction.address}: ${this.padEnd(15, instruction.opcodes)} \t${instruction.instruction}\n`;
|
||||
});
|
||||
resolve(output);
|
||||
},
|
||||
(error: any) => {
|
||||
vscode.window.showErrorMessage(error.message);
|
||||
reject(error.message);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private padEnd(targetLength: number, str: string): string {
|
||||
for (let i = str.length; i < targetLength; i++) {
|
||||
str += ' ';
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class DisassemblyTreeProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
|
||||
private _onDidChangeTreeData = new vscode.EventEmitter<vscode.TreeItem>();
|
||||
public onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
private forced: boolean = false;
|
||||
|
||||
refresh(): void {
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
getChildren(element?: vscode.TreeItem): vscode.TreeItem[] {
|
||||
if (!vscode.debug.activeDebugSession) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const disassembleItem = new vscode.TreeItem('Disassemble function');
|
||||
disassembleItem.command = {
|
||||
title: 'Disassemble function',
|
||||
command: 'platformio-debug.viewDisassembly',
|
||||
};
|
||||
|
||||
const switchLabel = 'Switch to ' + (this.forced ? 'code' : 'assembly');
|
||||
const switchItem = new vscode.TreeItem(switchLabel);
|
||||
switchItem.command = {
|
||||
title: switchLabel,
|
||||
command: 'platformio-debug.setForceDisassembly',
|
||||
arguments: [this.forced ? 'Auto' : 'Forced'],
|
||||
};
|
||||
|
||||
return [disassembleItem, switchItem];
|
||||
}
|
||||
|
||||
getTreeItem(element: vscode.TreeItem): vscode.TreeItem {
|
||||
return element;
|
||||
}
|
||||
|
||||
updateForcedState(forced: boolean): void {
|
||||
this.forced = forced;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
debugSessionStarted(): void {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
debugSessionTerminated(): void {
|
||||
this.updateForcedState(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { hexFormat, parseQuery } from '../utils';
|
||||
|
||||
export class MemoryContentProvider implements vscode.TextDocumentContentProvider {
|
||||
private _onDidChange = new vscode.EventEmitter<vscode.Uri>();
|
||||
public onDidChange = this._onDidChange.event;
|
||||
|
||||
private firstBytePos = 10;
|
||||
private lastBytePos = this.firstBytePos + 48 - 1;
|
||||
private firstAsciiPos = this.lastBytePos + 3;
|
||||
private lastAsciiPos = this.firstAsciiPos + 16;
|
||||
|
||||
private smallDecorationType = vscode.window.createTextEditorDecorationType({
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
overviewRulerColor: 'blue',
|
||||
overviewRulerLane: vscode.OverviewRulerLane.Right,
|
||||
light: { borderColor: 'darkblue' },
|
||||
dark: { borderColor: 'lightblue' },
|
||||
});
|
||||
|
||||
provideTextDocumentContent(uri: vscode.Uri): Thenable<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const params = parseQuery(uri.query);
|
||||
const address = params.address.startsWith('0x')
|
||||
? parseInt(params.address.substring(2), 16)
|
||||
: parseInt(params.address, 10);
|
||||
const length = params.length.startsWith('0x')
|
||||
? parseInt(params.length.substring(2), 16)
|
||||
: parseInt(params.length, 10);
|
||||
|
||||
vscode.debug.activeDebugSession
|
||||
.customRequest('read-memory', { address, length: length || 32 })
|
||||
.then(
|
||||
(result: any) => {
|
||||
const bytes: number[] = result.bytes;
|
||||
let rowAddress = address - (address % 16);
|
||||
const offset = address - rowAddress;
|
||||
let output = '';
|
||||
|
||||
output += ' Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F \t\n';
|
||||
output += hexFormat(rowAddress, 8, false) + ': ';
|
||||
|
||||
let asciiStr = '';
|
||||
for (let i = 0; i < offset; i++) {
|
||||
output += ' ';
|
||||
asciiStr += ' ';
|
||||
}
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const byte = bytes[i];
|
||||
output += hexFormat(byte, 2, false).toUpperCase() + ' ';
|
||||
asciiStr +=
|
||||
byte <= 32 || (byte >= 127 && byte <= 159)
|
||||
? '.'
|
||||
: String.fromCharCode(bytes[i]);
|
||||
|
||||
if ((address + i) % 16 === 15 && i < length - 1) {
|
||||
output += ' ' + asciiStr;
|
||||
asciiStr = '';
|
||||
output += '\n';
|
||||
rowAddress += 16;
|
||||
output += hexFormat(rowAddress, 8, false) + ': ';
|
||||
}
|
||||
}
|
||||
|
||||
const remaining = (16 - ((address + length) % 16)) % 16;
|
||||
for (let i = 0; i < remaining; i++) {
|
||||
output += ' ';
|
||||
}
|
||||
output += ' ' + asciiStr;
|
||||
output += '\n';
|
||||
|
||||
resolve(output);
|
||||
},
|
||||
(error: any) => {
|
||||
vscode.window.showErrorMessage(
|
||||
`Unable to read memory from ${hexFormat(address, 8)} to ${hexFormat(address + length, 8)}`
|
||||
);
|
||||
reject(error.toString());
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
update(document: vscode.TextDocument): void {
|
||||
this._onDidChange.fire(document.uri);
|
||||
}
|
||||
|
||||
getOffset(position: vscode.Position): number | undefined {
|
||||
if (position.line < 1 || position.character < this.firstBytePos) {
|
||||
return;
|
||||
}
|
||||
|
||||
let offset = 16 * (position.line - 1);
|
||||
const charOffset = position.character - this.firstBytePos;
|
||||
|
||||
if (position.character >= this.firstBytePos && position.character <= this.lastBytePos) {
|
||||
offset += Math.floor(charOffset / 3);
|
||||
} else if (position.character >= this.firstAsciiPos) {
|
||||
offset += position.character - this.firstAsciiPos;
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
getPosition(offset: number, isAscii: boolean = false): vscode.Position {
|
||||
const line = 1 + Math.floor(offset / 16);
|
||||
let character = offset % 16;
|
||||
if (isAscii) {
|
||||
character += this.firstAsciiPos;
|
||||
} else {
|
||||
character = this.firstBytePos + 3 * character;
|
||||
}
|
||||
return new vscode.Position(line, character);
|
||||
}
|
||||
|
||||
getRanges(startOffset: number, endOffset: number, isAscii: boolean): vscode.Range[] {
|
||||
const startPos = this.getPosition(startOffset, isAscii);
|
||||
let endPos = this.getPosition(endOffset, isAscii);
|
||||
endPos = new vscode.Position(endPos.line, endPos.character + (isAscii ? 1 : 2));
|
||||
|
||||
const ranges: vscode.Range[] = [];
|
||||
const startChar = isAscii ? this.firstAsciiPos : this.firstBytePos;
|
||||
const endChar = isAscii ? this.lastAsciiPos : this.lastBytePos;
|
||||
|
||||
for (let line = startPos.line; line <= endPos.line; ++line) {
|
||||
const lineStart = new vscode.Position(line, line === startPos.line ? startPos.character : startChar);
|
||||
const lineEnd = new vscode.Position(line, line === endPos.line ? endPos.character : endChar);
|
||||
ranges.push(new vscode.Range(lineStart, lineEnd));
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
handleSelection(event: vscode.TextEditorSelectionChangeEvent): void {
|
||||
const lineCount = event.textEditor.document.lineCount;
|
||||
if (
|
||||
event.selections[0].start.line + 1 === lineCount ||
|
||||
event.selections[0].end.line + 1 === lineCount
|
||||
) {
|
||||
event.textEditor.setDecorations(this.smallDecorationType, []);
|
||||
return;
|
||||
}
|
||||
|
||||
const startOffset = this.getOffset(event.selections[0].start);
|
||||
const endOffset = this.getOffset(event.selections[0].end);
|
||||
|
||||
if (startOffset === undefined || endOffset === undefined) {
|
||||
event.textEditor.setDecorations(this.smallDecorationType, []);
|
||||
return;
|
||||
}
|
||||
|
||||
let ranges = this.getRanges(startOffset, endOffset, false);
|
||||
ranges = ranges.concat(this.getRanges(startOffset, endOffset, true));
|
||||
event.textEditor.setDecorations(this.smallDecorationType, ranges);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class MemoryTreeProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
|
||||
private _onDidChangeTreeData = new vscode.EventEmitter<vscode.TreeItem>();
|
||||
public onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
private history: string[] = [];
|
||||
|
||||
refresh(): void {
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
dumpSettings(): string[] {
|
||||
return this.history;
|
||||
}
|
||||
|
||||
getChildren(element?: vscode.TreeItem): vscode.TreeItem[] {
|
||||
if (!vscode.debug.activeDebugSession) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this.history.length) {
|
||||
return this.getHistoryNodes();
|
||||
}
|
||||
|
||||
const item = new vscode.TreeItem('Enter address...');
|
||||
item.command = {
|
||||
title: 'Enter memory address...',
|
||||
command: 'platformio-debug.examineMemory',
|
||||
};
|
||||
return [item];
|
||||
}
|
||||
|
||||
private getHistoryNodes(): vscode.TreeItem[] {
|
||||
return this.history.map((entry) => {
|
||||
const item = new vscode.TreeItem(entry);
|
||||
item.command = {
|
||||
title: `Examine memory at ${entry}`,
|
||||
command: 'platformio-debug.examineMemory',
|
||||
arguments: entry.split('+'),
|
||||
};
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
getTreeItem(element: vscode.TreeItem): vscode.TreeItem {
|
||||
return element;
|
||||
}
|
||||
|
||||
pushHistory(address: string, length: string): void {
|
||||
const entry = `${address}+${length}`;
|
||||
if (!this.history.includes(entry)) {
|
||||
this.history.push(entry);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
deleteHistory(address: string, length: string): void {
|
||||
const entry = `${address}+${length}`;
|
||||
if (this.history.includes(entry)) {
|
||||
this.history = this.history.filter((e) => e !== entry);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
clearHistory(): void {
|
||||
this.history = [];
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
debugSessionStarted(savedState: string[]): void {
|
||||
this.history = savedState || [];
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
debugSessionTerminated(): void {
|
||||
this.history = [];
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,360 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { NumberFormat } from '../common';
|
||||
import { hexFormat, binaryFormat, extractBits } from '../utils';
|
||||
|
||||
export enum RecordType {
|
||||
Register = 0,
|
||||
Field = 1,
|
||||
}
|
||||
|
||||
export class TreeNode extends vscode.TreeItem {
|
||||
constructor(
|
||||
public label: string,
|
||||
public collapsibleState: vscode.TreeItemCollapsibleState,
|
||||
public contextValue: string,
|
||||
public node: BaseNode
|
||||
) {
|
||||
super(label, collapsibleState);
|
||||
this.command = {
|
||||
command: 'platformio-debug.registers.selectedNode',
|
||||
arguments: [node],
|
||||
title: 'Selected Node',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class BaseNode {
|
||||
public format: NumberFormat = NumberFormat.Auto;
|
||||
public expanded: boolean = false;
|
||||
|
||||
constructor(public recordType: RecordType) {}
|
||||
|
||||
getChildren(): BaseNode[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
getTreeNode(): TreeNode {
|
||||
return null;
|
||||
}
|
||||
|
||||
getCopyValue(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
setFormat(format: NumberFormat): void {
|
||||
this.format = format;
|
||||
}
|
||||
}
|
||||
|
||||
export class RegisterNode extends BaseNode {
|
||||
public name: string;
|
||||
public index: number;
|
||||
public fields: FieldNode[];
|
||||
public currentValue: number;
|
||||
|
||||
constructor(name: string, index: number) {
|
||||
super(RecordType.Register);
|
||||
this.name = name;
|
||||
this.index = index;
|
||||
|
||||
if (name.toUpperCase() === 'XPSR' || name.toUpperCase() === 'CPSR') {
|
||||
this.fields = [
|
||||
new FieldNode('Negative Flag (N)', 31, 1, this),
|
||||
new FieldNode('Zero Flag (Z)', 30, 1, this),
|
||||
new FieldNode('Carry or borrow flag (C)', 29, 1, this),
|
||||
new FieldNode('Overflow Flag (V)', 28, 1, this),
|
||||
new FieldNode('Saturation Flag (Q)', 27, 1, this),
|
||||
new FieldNode('GE', 16, 4, this),
|
||||
new FieldNode('Interrupt Number', 0, 8, this),
|
||||
new FieldNode('ICI/IT', 25, 2, this),
|
||||
new FieldNode('ICI/IT', 10, 6, this),
|
||||
new FieldNode('Thumb State (T)', 24, 1, this),
|
||||
];
|
||||
} else if (name.toUpperCase() === 'CONTROL') {
|
||||
this.fields = [
|
||||
new FieldNode('FPCA', 2, 1, this),
|
||||
new FieldNode('SPSEL', 1, 1, this),
|
||||
new FieldNode('nPRIV', 0, 1, this),
|
||||
];
|
||||
}
|
||||
|
||||
this.currentValue = 0;
|
||||
}
|
||||
|
||||
extractBits(offset: number, width: number): number {
|
||||
return extractBits(this.currentValue, offset, width);
|
||||
}
|
||||
|
||||
getTreeNode(): TreeNode {
|
||||
let label = `${this.name} = `;
|
||||
switch (this.getFormat()) {
|
||||
case NumberFormat.Decimal:
|
||||
label += this.currentValue.toString();
|
||||
break;
|
||||
case NumberFormat.Binary:
|
||||
label += binaryFormat(this.currentValue, 32, false, true);
|
||||
break;
|
||||
default:
|
||||
label += hexFormat(this.currentValue, 8);
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.fields && this.fields.length > 0) {
|
||||
return new TreeNode(
|
||||
label,
|
||||
this.expanded ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.Collapsed,
|
||||
'register',
|
||||
this
|
||||
);
|
||||
}
|
||||
return new TreeNode(label, vscode.TreeItemCollapsibleState.None, 'register', this);
|
||||
}
|
||||
|
||||
getChildren(): BaseNode[] {
|
||||
return this.fields;
|
||||
}
|
||||
|
||||
setValue(value: number): void {
|
||||
this.currentValue = value;
|
||||
}
|
||||
|
||||
getCopyValue(): string {
|
||||
switch (this.getFormat()) {
|
||||
case NumberFormat.Decimal:
|
||||
return this.currentValue.toString();
|
||||
case NumberFormat.Binary:
|
||||
return binaryFormat(this.currentValue, 32);
|
||||
default:
|
||||
return hexFormat(this.currentValue, 8);
|
||||
}
|
||||
}
|
||||
|
||||
getFormat(): NumberFormat {
|
||||
return this.format;
|
||||
}
|
||||
|
||||
dumpSettings(): any[] {
|
||||
const settings: any[] = [];
|
||||
if (this.expanded || this.format !== NumberFormat.Auto) {
|
||||
settings.push({
|
||||
node: this.name,
|
||||
format: this.format,
|
||||
expanded: this.expanded,
|
||||
});
|
||||
}
|
||||
if (this.fields) {
|
||||
settings.push(
|
||||
...this.fields.map((field) => field.dumpSettings()).filter((s) => s !== null)
|
||||
);
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
export class FieldNode extends BaseNode {
|
||||
constructor(
|
||||
public name: string,
|
||||
public offset: number,
|
||||
public size: number,
|
||||
public register: RegisterNode
|
||||
) {
|
||||
super(RecordType.Field);
|
||||
}
|
||||
|
||||
getTreeNode(): TreeNode {
|
||||
const value = this.register.extractBits(this.offset, this.size);
|
||||
let label = `${this.name} = `;
|
||||
|
||||
switch (this.getFormat()) {
|
||||
case NumberFormat.Decimal:
|
||||
label += value.toString();
|
||||
break;
|
||||
case NumberFormat.Binary:
|
||||
label += binaryFormat(value, this.size, false, true);
|
||||
break;
|
||||
case NumberFormat.Hexidecimal:
|
||||
label += hexFormat(value, Math.ceil(this.size / 4), true);
|
||||
break;
|
||||
default:
|
||||
label +=
|
||||
this.size >= 4
|
||||
? hexFormat(value, Math.ceil(this.size / 4), true)
|
||||
: binaryFormat(value, this.size, false, true);
|
||||
break;
|
||||
}
|
||||
|
||||
return new TreeNode(label, vscode.TreeItemCollapsibleState.None, 'field', this);
|
||||
}
|
||||
|
||||
getCopyValue(): string {
|
||||
const value = this.register.extractBits(this.offset, this.size);
|
||||
switch (this.getFormat()) {
|
||||
case NumberFormat.Decimal:
|
||||
return value.toString();
|
||||
case NumberFormat.Binary:
|
||||
return binaryFormat(value, this.size);
|
||||
case NumberFormat.Hexidecimal:
|
||||
return hexFormat(value, Math.ceil(this.size / 4), true);
|
||||
default:
|
||||
return this.size >= 4
|
||||
? hexFormat(value, Math.ceil(this.size / 4), true)
|
||||
: binaryFormat(value, this.size);
|
||||
}
|
||||
}
|
||||
|
||||
getFormat(): NumberFormat {
|
||||
return this.format === NumberFormat.Auto ? this.register.getFormat() : this.format;
|
||||
}
|
||||
|
||||
dumpSettings(): any {
|
||||
if (this.format !== NumberFormat.Auto) {
|
||||
return { node: `${this.register.name}.${this.name}`, format: this.format };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class RegisterTreeProvider implements vscode.TreeDataProvider<TreeNode> {
|
||||
private _onDidChangeTreeData = new vscode.EventEmitter<TreeNode>();
|
||||
public onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
private loaded: boolean = false;
|
||||
private viewExpanded: boolean = false;
|
||||
private registers: RegisterNode[] = [];
|
||||
private registerMap: { [index: number]: RegisterNode } = {};
|
||||
private initialSettings: any[];
|
||||
|
||||
refresh(): void {
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
dumpSettings(): any[] {
|
||||
const settings: any[] = [];
|
||||
this.registers.forEach((reg) => {
|
||||
settings.push(...reg.dumpSettings());
|
||||
});
|
||||
return settings;
|
||||
}
|
||||
|
||||
fetchRegisterList(): void {
|
||||
if (!vscode.debug.activeDebugSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.loaded) {
|
||||
this._fetchRegisterValues();
|
||||
} else {
|
||||
vscode.debug.activeDebugSession.customRequest('read-register-list').then((names: any) => {
|
||||
this.loaded = true;
|
||||
this.createRegisters(names);
|
||||
this._fetchRegisterValues();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _fetchRegisterValues(): void {
|
||||
vscode.debug.activeDebugSession.customRequest('read-registers').then((registers: any) => {
|
||||
registers.forEach((reg: any) => {
|
||||
const index = parseInt(reg.number, 10);
|
||||
const value = parseInt(reg.value, 16);
|
||||
const registerNode = this.registerMap[index];
|
||||
if (registerNode) {
|
||||
registerNode.setValue(value);
|
||||
}
|
||||
});
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
getTreeItem(element: TreeNode): TreeNode {
|
||||
return element;
|
||||
}
|
||||
|
||||
private createRegisters(names: string[]): void {
|
||||
this.registerMap = {};
|
||||
this.registers = [];
|
||||
|
||||
names.forEach((name, index) => {
|
||||
if (name) {
|
||||
const reg = new RegisterNode(name, index);
|
||||
this.registers.push(reg);
|
||||
this.registerMap[index] = reg;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.initialSettings) {
|
||||
this.initialSettings.forEach((setting) => {
|
||||
if (setting.node.indexOf('.') === -1) {
|
||||
const reg = this.registers.find((r) => r.name === setting.node);
|
||||
if (reg) {
|
||||
if (setting.expanded) {
|
||||
reg.expanded = setting.expanded;
|
||||
}
|
||||
if (setting.format) {
|
||||
reg.setFormat(setting.format);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const [regName, fieldName] = setting.node.split('.');
|
||||
const reg = this.registers.find((r) => r.name === regName);
|
||||
if (reg) {
|
||||
const field = reg.getChildren().find((f: any) => f.name === fieldName);
|
||||
if (field && setting.format) {
|
||||
field.setFormat(setting.format);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
updateRegisterValues(values: any[]): void {
|
||||
values.forEach((val) => {
|
||||
this.registerMap[val.number].setValue(val.value);
|
||||
});
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
getChildren(element?: TreeNode): any[] {
|
||||
this.viewExpanded = true;
|
||||
if (!vscode.debug.activeDebugSession) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this.registers.length > 0) {
|
||||
if (element) {
|
||||
return element.node.getChildren().map((child) => child.getTreeNode());
|
||||
}
|
||||
return this.registers.map((reg) => reg.getTreeNode());
|
||||
}
|
||||
|
||||
if (!this.loaded) {
|
||||
setTimeout(() => this.fetchRegisterList(), 1000);
|
||||
}
|
||||
|
||||
return [new TreeNode('Loading...', vscode.TreeItemCollapsibleState.None, 'message', null)];
|
||||
}
|
||||
|
||||
debugSessionTerminated(): void {
|
||||
this.loaded = false;
|
||||
this.registers = [];
|
||||
this.registerMap = {};
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
debugSessionStarted(savedState: any[]): void {
|
||||
this.loaded = false;
|
||||
this.registers = [];
|
||||
this.registerMap = {};
|
||||
this.initialSettings = savedState;
|
||||
}
|
||||
|
||||
debugStopped(): void {
|
||||
if (this.viewExpanded) {
|
||||
this.fetchRegisterList();
|
||||
}
|
||||
}
|
||||
|
||||
debugContinued(): void {}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
export function hexFormat(value: number, padding: number = 8, includePrefix: boolean = true): string {
|
||||
let result = value.toString(16);
|
||||
while (result.length < padding) {
|
||||
result = '0' + result;
|
||||
}
|
||||
return includePrefix ? '0x' + result : result;
|
||||
}
|
||||
|
||||
export function binaryFormat(
|
||||
value: number,
|
||||
padding: number = 0,
|
||||
includePrefix: boolean = true,
|
||||
groupByNibble: boolean = false
|
||||
): string {
|
||||
let result = (value >>> 0).toString(2);
|
||||
while (result.length < padding) {
|
||||
result = '0' + result;
|
||||
}
|
||||
|
||||
if (groupByNibble) {
|
||||
const extraZeros = 4 - (result.length % 4);
|
||||
for (let i = 0; i < extraZeros; i++) {
|
||||
result = '0' + result;
|
||||
}
|
||||
const groups = result.match(/[01]{4}/g);
|
||||
result = groups.join(' ');
|
||||
result = result.substring(extraZeros);
|
||||
}
|
||||
|
||||
return includePrefix ? '0b' + result : result;
|
||||
}
|
||||
|
||||
export function createMask(offset: number, width: number): number {
|
||||
let mask = 0;
|
||||
const end = offset + width - 1;
|
||||
for (let i = offset; i <= end; i++) {
|
||||
mask = (mask | (1 << i)) >>> 0;
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
export function extractBits(value: number, offset: number, width: number): number {
|
||||
return ((value & createMask(offset, width)) >>> offset) >>> 0;
|
||||
}
|
||||
|
||||
export function parseQuery(queryString: string): { [key: string]: string } {
|
||||
const params: { [key: string]: string } = {};
|
||||
const pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
|
||||
for (const pair of pairs) {
|
||||
const parts = pair.split('=');
|
||||
params[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1] || '');
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export function encodeDisassembly(name: string, file: string): string {
|
||||
let uri = 'disassembly:///';
|
||||
if (file) {
|
||||
uri += `${file}:`;
|
||||
}
|
||||
uri += `${name}.dbgasm?func=${name}&file=${file || ''}`;
|
||||
return uri;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"outDir": "out",
|
||||
"lib": ["es6"],
|
||||
"sourceMap": true,
|
||||
"strict": false,
|
||||
"rootDir": "src",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist", ".vscode-test"]
|
||||
}
|
||||
Reference in New Issue
Block a user