diff --git a/CHANGELOG.md b/CHANGELOG.md index 82fb89ed..2984df72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,35 @@ Versioning]. [keep a changelog]: https://keepachangelog.com/en/1.0.0 [semantic versioning]: https://semver.org/spec/v2.0.0.html +## Unreleased + +### Added + +- fix missing output of variable type for structure ([@henryriley0]) +- add static variable support ([@henryriley0]) +- fix gdb check error when debug beginning ([@henryriley0]) +- fix implicitly type error in log message when build vsix ([@henryriley0]) +- check for configured debugger before start to provide a nicer error message + ([@GitMensch]) +- New `frameFilters` option for GDB that allows using custom frame filters, + enabled by default ([@JacquesLucke]) +- Suppress error for hover as the user may just play with the mouse ([@oltolm]). +- solve the problem of failed parsing of containers ([@henryriley0]) +- Fixes #421 - Added `registerLimit` option to specify the registers to + display - PR #444 ([@chenzhiy2001]) +- resolve the issue of not being able to set the GDB binary with a path on + Windows - PR #448 ([@henryriley0]) +- add qt unit test ([@henryriley0]) + +## [0.27.0] - 2024-02-07 + +### Added + +- Added registers view ([@nomtats]) #242 +- Enabled breakpoints inside `riscv` files ([@William-An]) #404 + +[0.27.0]: https://github.com/WebFreak001/code-debug/compare/v0.26.1...v0.27.0 + ## [0.26.1] - 2022-12-31 ### Fixed @@ -219,13 +248,15 @@ Versioning]. [@abussy-aldebaran]: https://github.com/abussy-aldebaran [@anshulrouthu]: https://github.com/anshulrouthu [@brownts]: https://github.com/brownts -[@coldencullen]: https://github.com/ColdenCullen +[@chenzhiy2001]: https://github.com/chenzhiy2001 [@eamousing]: https://github.com/eamousing [@evangrayk]: https://github.com/evangrayk [@faustinoaq]: https://github.com/faustinoaq [@gentoo90]: https://github.com/gentoo90 [@gitmensch]: https://github.com/GitMensch [@haronk]: https://github.com/HaronK +[@henryriley0]: https://github.com/HenryRiley0 +[@jacqueslucke]: https://github.com/JacquesLucke [@jelleroets]: https://github.com/JelleRoets [@karljs]: https://github.com/karljs [@kvinwang]: https://github.com/kvinwang @@ -238,6 +269,7 @@ Versioning]. [@reznikmm]: https://github.com/reznikmm [@simark]: https://github.com/simark [@webfreak001]: https://github.com/WebFreak001 +[@william-an]: https://github.com/William-An [@yanpas]: https://github.com/Yanpas diff --git a/README.md b/README.md index b9bda930..603bdf8c 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,20 @@ that location prior to continuing execution. Note that stopping at the entry po attach configuration assumes that the entry point has not yet been entered at the time of attach, otherwise this will have no affect. +There is a Registers view in the VARIABLES view. As we fetch all registers at once, there can +be cases where a register that cannot be fetched causes the entire register request to fail, +corrupting the entire Registers output. If this happens, you might need to set the +`registerLimit` option to specify which registers you want the debugger to fetch +automatically. + +For example, to display only registers `rax` and `rip` in an x64 debug session, send the +command `-data-list-register-names` in the debug console of an active x64 debug session. +You will then receive a response containing an array starting with `["rax","rbx" ...]`. +In this array, the index of `rax` is 0 and `rip` is 16, so set the option as +`"registerLimit": "0 16"`. If you find the response text hard to navigate, you can paste +it into a browser's developer tools console and press enter to get an expandable response +object with array elements' indices explicitly displayed. + ### Attaching to existing processes Attaching to existing processes currently only works by specifying the PID in the @@ -213,4 +227,10 @@ differently based on whether the remote system is a POSIX or a Windows system. You may need to experiment to find the correct escaping necessary for the command to be sent to the debugger as you intended. +### LogMessage + +LogMessage will print a message in the debug console when breakpoint is hit. Expressions within {} are interpolated. + +![LogMessage](images/logMessage.gif) + ## [Issues](https://github.com/WebFreak001/code-debug) diff --git a/images/logMessage.gif b/images/logMessage.gif new file mode 100644 index 00000000..4bdb9ac3 Binary files /dev/null and b/images/logMessage.gif differ diff --git a/package-lock.json b/package-lock.json index 8577d448..aa8b7454 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "debug", - "version": "0.26.1", + "version": "0.27.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "debug", - "version": "0.26.1", + "version": "0.27.1", "license": "public domain", "dependencies": { "ssh2": "^1.6.0", @@ -17,6 +17,7 @@ "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/mocha": "^5.2.6", "@types/node": "^11.11.3", + "@types/ssh2": "^1.15.0", "@types/vscode": "^1.55.0", "@typescript-eslint/eslint-plugin": "^5.22.0", "@typescript-eslint/parser": "^5.22.0", @@ -30,7 +31,7 @@ "nyc": "^15.1.0", "prettier": "^2.6.2", "ts-node": "^10.8.0", - "typescript": "^3.9.3" + "typescript": "^4.3.2" }, "engines": { "vscode": "^1.55.0" @@ -972,6 +973,24 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "node_modules/@types/ssh2": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.0.tgz", + "integrity": "sha512-YcT8jP5F8NzWeevWvcyrrLB3zcneVjzYY9ZDSMAMboI+2zR1qYWFhwsyOFVzT7Jorn67vqxC0FRiw8YyG9P1ww==", + "dev": true, + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.24.tgz", + "integrity": "sha512-eghAz3gnbQbvnHqB+mgB2ZR3aH6RhdEmHGS48BnV75KceQPHqabkxKI0BbUSsqhqy2Ddhc2xD/VAR9ySZd57Lw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@types/vscode": { "version": "1.62.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.62.0.tgz", @@ -5343,9 +5362,9 @@ } }, "node_modules/typescript": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", - "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.2.tgz", + "integrity": "sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -5361,6 +5380,12 @@ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "dev": true }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -6445,6 +6470,26 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "@types/ssh2": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.0.tgz", + "integrity": "sha512-YcT8jP5F8NzWeevWvcyrrLB3zcneVjzYY9ZDSMAMboI+2zR1qYWFhwsyOFVzT7Jorn67vqxC0FRiw8YyG9P1ww==", + "dev": true, + "requires": { + "@types/node": "^18.11.18" + }, + "dependencies": { + "@types/node": { + "version": "18.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.24.tgz", + "integrity": "sha512-eghAz3gnbQbvnHqB+mgB2ZR3aH6RhdEmHGS48BnV75KceQPHqabkxKI0BbUSsqhqy2Ddhc2xD/VAR9ySZd57Lw==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + } + } + }, "@types/vscode": { "version": "1.62.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.62.0.tgz", @@ -9643,9 +9688,9 @@ } }, "typescript": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", - "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.2.tgz", + "integrity": "sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw==", "dev": true }, "uc.micro": { @@ -9654,6 +9699,12 @@ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "dev": true }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", diff --git a/package.json b/package.json index 38fb36f5..bea5c72e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "debug" ], "license": "public domain", - "version": "0.26.1", + "version": "0.27.1", "publisher": "webfreak", "icon": "images/icon.png", "engines": { @@ -101,6 +101,9 @@ }, { "language": "zig" + }, + { + "language": "riscv" } ], "debuggers": [ @@ -129,7 +132,8 @@ "vala", "crystal", "kotlin", - "zig" + "zig", + "riscv" ], "variables": { "FileBasenameNoExt": "code-debug.getFileBasenameNoExt", @@ -183,13 +187,18 @@ "valuesFormatting": { "type": "string", "description": "Set the way of showing variable values. 'disabled' - show value as is, 'parseText' - parse debuggers output text into structure, 'prettyPrinters' - enable debuggers custom pretty-printers if there are any", - "default": "parseText", + "default": "prettyPrinters", "enum": [ "disabled", "parseText", "prettyPrinters" ] }, + "frameFilters": { + "type": "boolean", + "description": "Use frame filters registered in GDB", + "default": true + }, "printCalls": { "type": "boolean", "description": "Prints all GDB calls to the console", @@ -272,12 +281,12 @@ "number", "string" ], - "description": "Port to redirect X11 data to (by default port = display + 6000)", + "description": "TCP port to redirect X11 data to (by default port = display + 6000); raw display number for unix domain connection", "default": 6000 }, "x11host": { "type": "string", - "description": "Hostname/ip to redirect X11 data to", + "description": "Hostname/ip to redirect X11 data to; empty string for a unix domain connection", "default": "localhost" }, "remotex11screen": { @@ -290,6 +299,11 @@ "description": "Content will be executed on the SSH host before the debugger call." } } + }, + "registerLimit": { + "type": "string", + "description": "List of numbers specifying the registers to display. An empty string indicates that the contents of all the registers must be returned.", + "default": "" } } }, @@ -318,6 +332,11 @@ "prettyPrinters" ] }, + "frameFilters": { + "type": "boolean", + "description": "Use frame filters registered in GDB", + "default": true + }, "printCalls": { "type": "boolean", "description": "Prints all GDB calls to the console", @@ -436,12 +455,12 @@ "number", "string" ], - "description": "Port to redirect X11 data to (by default port = display + 6000)", + "description": "TCP port to redirect X11 data to (by default port = display + 6000); raw display number for unix domain connection", "default": 6000 }, "x11host": { "type": "string", - "description": "Hostname/ip to redirect X11 data to", + "description": "Hostname/ip to redirect X11 data to; empty string for a unix domain connection", "default": "localhost" }, "remotex11screen": { @@ -454,6 +473,11 @@ "description": "Content will be executed on the SSH host before the debugger call." } } + }, + "registerLimit": { + "type": "string", + "description": "List of numbers specifying the registers to display. An empty string indicates that the contents of all the registers must be returned.", + "default": "" } } } @@ -595,7 +619,8 @@ "vala", "crystal", "kotlin", - "zig" + "zig", + "riscv" ], "variables": { "FileBasenameNoExt": "code-debug.getFileBasenameNoExt", @@ -730,7 +755,7 @@ }, "x11host": { "type": "string", - "description": "Hostname/ip to redirect X11 data to", + "description": "Hostname/ip to redirect X11 data to; empty string for a unix domain connection", "default": "localhost" }, "x11port": { @@ -738,7 +763,7 @@ "number", "string" ], - "description": "Port to redirect X11 data to (by default port = display + 6000)", + "description": "TCP port to redirect X11 data to (by default port = display + 6000); raw display number for unix domain connection", "default": 6000 }, "remotex11screen": { @@ -751,6 +776,11 @@ "description": "Content will be executed on the SSH host before the debugger call." } } + }, + "registerLimit": { + "type": "string", + "description": "List of numbers specifying the registers to display. An empty string indicates that the contents of all the registers must be returned.", + "default": "" } } }, @@ -831,6 +861,11 @@ ], "description": "Whether debugger should stop at application entry point", "default": false + }, + "registerLimit": { + "type": "string", + "description": "List of numbers specifying the registers to display. An empty string indicates that the contents of all the registers must be returned.", + "default": "" } } } @@ -977,6 +1012,11 @@ "type": "array", "description": "mago commands to run when starting to debug", "default": [] + }, + "registerLimit": { + "type": "string", + "description": "List of numbers specifying the registers to display. An empty string indicates that the contents of all the registers must be returned.", + "default": "" } } }, @@ -1042,6 +1082,11 @@ "type": "boolean", "description": "Whether debugger should stop after connecting to target", "default": false + }, + "registerLimit": { + "type": "string", + "description": "List of numbers specifying the registers to display. An empty string indicates that the contents of all the registers must be returned.", + "default": "" } } } @@ -1107,6 +1152,7 @@ "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/mocha": "^5.2.6", "@types/node": "^11.11.3", + "@types/ssh2": "^1.15.0", "@types/vscode": "^1.55.0", "@typescript-eslint/eslint-plugin": "^5.22.0", "@typescript-eslint/parser": "^5.22.0", @@ -1120,7 +1166,7 @@ "nyc": "^15.1.0", "prettier": "^2.6.2", "ts-node": "^10.8.0", - "typescript": "^3.9.3" + "typescript": "^4.3.2" }, "__metadata": { "id": "2fd22b8e-b3b8-4e7f-9a28-a5e2d1bdd0d4", diff --git a/src/backend/backend.ts b/src/backend/backend.ts index f532f0ed..e26bd369 100644 --- a/src/backend/backend.ts +++ b/src/backend/backend.ts @@ -4,11 +4,13 @@ import { DebugProtocol } from "vscode-debugprotocol/lib/debugProtocol"; export type ValuesFormattingMode = "disabled" | "parseText" | "prettyPrinters"; export interface Breakpoint { + id?:number; file?: string; line?: number; raw?: string; condition: string; countCondition?: string; + logMessage?: string; } export interface Thread { @@ -33,6 +35,11 @@ export interface Variable { raw?: any; } +export interface RegisterValue { + index: number; + value: string; +} + export interface SSHArguments { forwardX11: boolean; host: string; @@ -55,8 +62,8 @@ export interface IBackend { attach(cwd: string, executable: string, target: string, autorun: string[]): Thenable; connect(cwd: string, executable: string, target: string, autorun: string[]): Thenable; start(runToStart: boolean): Thenable; - stop(); - detach(); + stop(): void; + detach(): void; interrupt(): Thenable; continue(): Thenable; next(): Thenable; @@ -136,29 +143,25 @@ export interface MIError extends Error { readonly source: string; } export interface MIErrorConstructor { - new (message: string, source: string): MIError; + new(message: string, source: string): MIError; readonly prototype: MIError; } -export const MIError: MIErrorConstructor = class MIError { - readonly name: string; - readonly message: string; - readonly source: string; +export const MIError: MIErrorConstructor = class MIError { + private readonly _message: string; + private readonly _source: string; public constructor(message: string, source: string) { - Object.defineProperty(this, 'name', { - get: () => (this.constructor as any).name, - }); - Object.defineProperty(this, 'message', { - get: () => message, - }); - Object.defineProperty(this, 'source', { - get: () => source, - }); + this._message = message; + this._source = source; Error.captureStackTrace(this, this.constructor); } + get name() { return this.constructor.name; } + get message() { return this._message; } + get source() { return this._source; } + public toString() { - return `${this.message} (from ${this.source})`; + return `${this.message} (from ${this._source})`; } }; Object.setPrototypeOf(MIError as any, Object.create(Error.prototype)); diff --git a/src/backend/gdb_expansion.ts b/src/backend/gdb_expansion.ts index d2c83e41..32b248ec 100644 --- a/src/backend/gdb_expansion.ts +++ b/src/backend/gdb_expansion.ts @@ -1,7 +1,8 @@ +import { VariableObject } from "./backend"; import { MINode } from "./mi_parse"; -const resultRegex = /^([a-zA-Z_\-][a-zA-Z0-9_\-]*|\[\d+\])\s*=\s*/; -const variableRegex = /^[a-zA-Z_\-][a-zA-Z0-9_\-]*/; +const resultRegex = /^([a-zA-Z_\-][a-zA-Z0-9_\-<> :(),]*|\[\d+\])\s*=\s*/; +const variableRegex = /^[a-zA-Z_\-\'\(][a-zA-Z0-9_\-\|\>\ \\\'\)\:]*/; const errorRegex = /^\<.+?\>/; const referenceStringRegex = /^(0x[0-9a-fA-F]+\s*)"/; const referenceRegex = /^0x[0-9a-fA-F]+/; @@ -14,9 +15,9 @@ const pointerCombineChar = "."; export function isExpandable(value: string): number { let match; value = value.trim(); - if (value.length == 0) return 0; + if (value.length === 0) return 0; else if (value.startsWith("{...}")) return 2; // lldb string/array - else if (value[0] == '{') return 1; // object + else if (value[0] === '{') return 1; // object else if (value.startsWith("true")) return 0; else if (value.startsWith("false")) return 0; else if (match = nullpointerRegex.exec(value)) return 0; @@ -29,10 +30,10 @@ export function isExpandable(value: string): number { else return 0; } -export function expandValue(variableCreate: Function, value: string, root: string = "", extra: any = undefined): any { +export function expandValue(variableCreate: (arg: VariableObject | string, options?: any) => any, value: string, root: string = "", extra: any = undefined): any { const parseCString = () => { value = value.trim(); - if (value[0] != '"' && value[0] != '\'') + if (value[0] !== '"' && value[0] !== '\'') return ""; let stringEnd = 1; let inString = true; @@ -42,9 +43,9 @@ export function expandValue(variableCreate: Function, value: string, root: strin while (inString) { if (escaped) escaped = false; - else if (remaining[0] == '\\') + else if (remaining[0] === '\\') escaped = true; - else if (remaining[0] == charStr) + else if (remaining[0] === charStr) inString = false; remaining = remaining.substring(1); @@ -56,10 +57,10 @@ export function expandValue(variableCreate: Function, value: string, root: strin }; const stack = [root]; - let parseValue, parseCommaResult, parseCommaValue, parseResult, createValue; + let parseValue: () => any, parseCommaResult: (pushToStack: boolean) => any, parseCommaValue: () => any, parseResult: (pushToStack: boolean) => any, createValue: (name: string, val: any) => any; let variable = ""; - const getNamespace = (variable) => { + const getNamespace = (variable: string) => { let namespace = ""; let prefix = ""; stack.push(variable); @@ -86,17 +87,17 @@ export function expandValue(variableCreate: Function, value: string, root: strin const parseTupleOrList = () => { value = value.trim(); - if (value[0] != '{') + if (value[0] !== '{') return undefined; const oldContent = value; value = value.substring(1).trim(); - if (value[0] == '}') { + if (value[0] === '}') { value = value.substring(1).trim(); return []; } if (value.startsWith("...")) { value = value.substring(3).trim(); - if (value[0] == '}') { + if (value[0] === '}') { value = value.substring(1).trim(); return "<...>"; } @@ -104,14 +105,18 @@ export function expandValue(variableCreate: Function, value: string, root: strin const eqPos = value.indexOf("="); const newValPos1 = value.indexOf("{"); const newValPos2 = value.indexOf(","); + const newValPos3 = value.indexOf("}"); let newValPos = newValPos1; - if (newValPos2 != -1 && newValPos2 < newValPos1) + if (newValPos2 !== -1 && newValPos2 < newValPos1) newValPos = newValPos2; - if (newValPos != -1 && eqPos > newValPos || eqPos == -1) { // is value list + if (newValPos !== -1 && eqPos > newValPos || eqPos === -1 || eqPos > newValPos3 && newValPos2 !== -1 || value.startsWith("std::")) { // is value list const values = []; stack.push("[0]"); let val = parseValue(); stack.pop(); + if(typeof val === "string" && val.endsWith('>')){ + val = val.substring(0, val.length - 2); + } values.push(createValue("[0]", val)); const remaining = value; let i = 0; @@ -145,7 +150,7 @@ export function expandValue(variableCreate: Function, value: string, root: strin let primitive: any; let match; value = value.trim(); - if (value.length == 0) + if (value.length === 0) primitive = undefined; else if (value.startsWith("true")) { primitive = "true"; @@ -186,21 +191,30 @@ export function expandValue(variableCreate: Function, value: string, root: strin parseValue = () => { value = value.trim(); - if (value[0] == '"') + if (value[0] === '"') return parseCString(); - else if (value[0] == '{') + else if (value[0] === '{') return parseTupleOrList(); + else if(value.startsWith("std::")){ + const eqPos = value.indexOf("="); + value = value.substring(eqPos + 2); + return parseValue(); + } else return parsePrimitive(); }; parseResult = (pushToStack: boolean = false) => { - value = value.trim(); + value = value.replace(/^ 1 && !name.includes("anonymous union") && !name.includes(',')){ + name = tmpName[tmpName.length - 1]; + } if (pushToStack) stack.push(variable); const val = parseValue(); @@ -209,12 +223,12 @@ export function expandValue(variableCreate: Function, value: string, root: strin return createValue(name, val); }; - createValue = (name, val) => { + createValue = (name: string, val: any) => { let ref = 0; - if (typeof val == "object") { + if (typeof val === "object") { ref = variableCreate(val); val = "Object"; - } else if (typeof val == "string" && val.startsWith("*0x")) { + } else if (typeof val === "string" && val.startsWith("*0x")) { if (extra && MINode.valueOf(extra, "arg") == "1") { ref = variableCreate(getNamespace("*(" + name), { arg: true }); val = ""; @@ -222,13 +236,16 @@ export function expandValue(variableCreate: Function, value: string, root: strin ref = variableCreate(getNamespace("*" + name)); val = "Object@" + val; } - } else if (typeof val == "string" && val.startsWith("@0x")) { - ref = variableCreate(getNamespace("*&" + name.substr)); + } else if (typeof val === "string" && val.startsWith("@0x")) { + ref = variableCreate(getNamespace("*&" + name.substring(1))); val = "Ref" + val; - } else if (typeof val == "string" && val.startsWith("<...>")) { + } else if (typeof val === "string" && val.startsWith("<...>")) { ref = variableCreate(getNamespace(name)); val = "..."; } + value = value.trim(); + value = value.replace(/^, /, ""); + return { name: name, value: val, @@ -238,7 +255,7 @@ export function expandValue(variableCreate: Function, value: string, root: strin parseCommaValue = () => { value = value.trim(); - if (value[0] != ',') + if (value[0] !== ',') return undefined; value = value.substring(1).trim(); return parseValue(); @@ -246,7 +263,7 @@ export function expandValue(variableCreate: Function, value: string, root: strin parseCommaResult = (pushToStack: boolean = false) => { value = value.trim(); - if (value[0] != ',') + if (value[0] !== ',') return undefined; value = value.substring(1).trim(); return parseResult(pushToStack); diff --git a/src/backend/mi2/mi2.ts b/src/backend/mi2/mi2.ts index 007fd209..1a5c2b7c 100644 --- a/src/backend/mi2/mi2.ts +++ b/src/backend/mi2/mi2.ts @@ -1,4 +1,4 @@ -import { Breakpoint, IBackend, Thread, Stack, SSHArguments, Variable, VariableObject, MIError } from "../backend"; +import { Breakpoint, IBackend, Thread, Stack, SSHArguments, Variable, RegisterValue, VariableObject, MIError } from "../backend"; import * as ChildProcess from "child_process"; import { EventEmitter } from "events"; import { parseMI, MINode } from '../mi_parse'; @@ -6,7 +6,7 @@ import * as linuxTerm from '../linux/console'; import * as net from "net"; import * as fs from "fs"; import * as path from "path"; -import { Client } from "ssh2"; +import { Client, ClientChannel, ExecOptions } from "ssh2"; export function escape(str: string) { return str.replace(/\\/g, "\\\\").replace(/"/g, "\\\""); @@ -24,12 +24,56 @@ function couldBeOutput(line: string) { const trace = false; +class LogMessage { + protected logMsgVar = ""; + protected logMsgVarProcess = ""; + protected logMsgRplNum = 0; + protected logMsgRplItem: string[] = []; + protected logMsgMatch = /(^\$[0-9]*[\ ]*=[\ ]*)(.*)/; + protected logReplaceTest = /{([^}]*)}/g; + public logMsgBrkList: Breakpoint[] = []; + + logMsgOutput(record:any){ + if ((record.type === 'console')) { + if(record.content.startsWith("$")){ + const content = record.content; + const variableMatch = this.logMsgMatch.exec(content); + if (variableMatch) { + const value = content.substr(variableMatch[1].length).trim(); + this.logMsgRplItem.push(value); + this.logMsgRplNum--; + if(this.logMsgRplNum === 0){ + for(let i = 0; i < this.logMsgRplItem.length; i++){ + this.logMsgVarProcess = this.logMsgVarProcess.replace("placeHolderForVariable", this.logMsgRplItem[i]); + } + return "Log Message:" + this.logMsgVarProcess; + } + } + } + return undefined; + } + } + + logMsgProcess(parsed:MINode){ + this.logMsgBrkList.forEach((brk)=>{ + if(parsed.outOfBandRecord[0].output[0][1] === "breakpoint-hit" && parsed.outOfBandRecord[0].output[2][1] === brk.id){ + this.logMsgVar = brk?.logMessage; + const matches = this.logMsgVar.match(this.logReplaceTest); + const count = matches ? matches.length : 0; + this.logMsgRplNum = count; + this.logMsgVarProcess = this.logMsgVar.replace(this.logReplaceTest, "placeHolderForVariable"); + this.logMsgRplItem = []; + } + }); + } +} + export class MI2 extends EventEmitter implements IBackend { constructor(public application: string, public preargs: string[], public extraargs: string[], procEnv: any, public extraCommands: string[] = []) { super(); if (procEnv) { - const env = {}; + const env: { [key: string]: string } = {}; // Duplicate process.env so we don't override it for (const key in process.env) if (process.env.hasOwnProperty(key)) @@ -47,6 +91,7 @@ export class MI2 extends EventEmitter implements IBackend { this.procEnv = env; } } + protected logMessage:LogMessage = new LogMessage; load(cwd: string, target: string, procArgs: string, separateConsole: string, autorun: string[]): Thenable { if (!path.isAbsolute(target)) @@ -57,12 +102,12 @@ export class MI2 extends EventEmitter implements IBackend { this.process = ChildProcess.spawn(this.application, args, { cwd: cwd, env: this.procEnv }); 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) => { this.emit("launcherror", err); }).bind(this)); + this.process.on("exit", () => this.emit("quit")); + this.process.on("error", err => this.emit("launcherror", err)); const promises = this.initCommands(target, cwd); if (procArgs && procArgs.length) promises.push(this.sendCommand("exec-arguments " + procArgs)); - if (process.platform == "win32") { + if (process.platform === "win32") { if (separateConsole !== undefined) promises.push(this.sendCommand("gdb-set new-console on")); promises.push(...autorun.map(value => { return this.sendUserInput(value); })); @@ -110,7 +155,10 @@ export class MI2 extends EventEmitter implements IBackend { const xclientsock = accept(); xclientsock.pipe(xserversock).pipe(xclientsock); }); - xserversock.connect(args.x11port, args.x11host); + if (args.x11host !== "") + xserversock.connect(args.x11port, args.x11host); + else + xserversock.connect(`/tmp/.X11-unix/X${args.x11port}`); }); } @@ -137,7 +185,7 @@ export class MI2 extends EventEmitter implements IBackend { this.sshConn.on("ready", () => { this.log("stdout", "Running " + this.application + " over ssh..."); - const execArgs: any = {}; + const execArgs: ExecOptions = {}; if (args.forwardX11) { execArgs.x11 = { single: false, @@ -150,7 +198,7 @@ export class MI2 extends EventEmitter implements IBackend { if (err) { this.log("stderr", "Could not run " + this.application + "(" + sshCMD + ") over ssh!"); if (err === undefined) { - err = ""; + err = new Error(""); } this.log("stderr", err.toString()); this.emit("quit"); @@ -161,10 +209,10 @@ export class MI2 extends EventEmitter implements IBackend { this.stream = stream; stream.on("data", this.stdout.bind(this)); stream.stderr.on("data", this.stderr.bind(this)); - stream.on("exit", (() => { + stream.on("exit", () => { this.emit("quit"); this.sshConn.end(); - }).bind(this)); + }); const promises = this.initCommands(target, cwd, attach); promises.push(this.sendCommand("environment-cd \"" + escape(cwd) + "\"")); if (attach) { @@ -181,7 +229,7 @@ export class MI2 extends EventEmitter implements IBackend { }).on("error", (err) => { this.log("stderr", "Error running " + this.application + " over ssh!"); if (err === undefined) { - err = ""; + err = new Error(""); } this.log("stderr", err.toString()); this.emit("quit"); @@ -219,6 +267,8 @@ export class MI2 extends EventEmitter implements IBackend { cmds.push(this.sendCommand("file-exec-and-symbols \"" + escape(target) + "\"")); if (this.prettyPrint) cmds.push(this.sendCommand("enable-pretty-printing")); + if (this.frameFilters) + cmds.push(this.sendCommand("enable-frame-filters")); for (const cmd of this.extraCommands) { cmds.push(this.sendCommand(cmd)); } @@ -235,8 +285,8 @@ export class MI2 extends EventEmitter implements IBackend { this.process = ChildProcess.spawn(this.application, args, { cwd: cwd, env: this.procEnv }); 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) => { this.emit("launcherror", err); }).bind(this)); + this.process.on("exit", () => this.emit("quit")); + this.process.on("error", err => this.emit("launcherror", err)); const promises = this.initCommands(target, cwd, true); if (target.startsWith("extended-remote")) { promises.push(this.sendCommand("target-select " + target)); @@ -267,8 +317,8 @@ export class MI2 extends EventEmitter implements IBackend { this.process = ChildProcess.spawn(this.application, args, { cwd: cwd, env: this.procEnv }); 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) => { this.emit("launcherror", err); }).bind(this)); + this.process.on("exit", () => this.emit("quit")); + this.process.on("error", err => this.emit("launcherror", err)); const promises = this.initCommands(target, cwd, true); promises.push(this.sendCommand("target-select remote " + target)); promises.push(...autorun.map(value => { return this.sendUserInput(value); })); @@ -279,15 +329,15 @@ export class MI2 extends EventEmitter implements IBackend { }); } - stdout(data) { + stdout(data: any) { if (trace) this.log("stderr", "stdout: " + data); - if (typeof data == "string") + if (typeof data === "string") this.buffer += data; else this.buffer += data.toString("utf8"); const end = this.buffer.lastIndexOf('\n'); - if (end != -1) { + if (end !== -1) { this.onOutput(this.buffer.substring(0, end)); this.buffer = this.buffer.substring(end + 1); } @@ -298,13 +348,13 @@ export class MI2 extends EventEmitter implements IBackend { } } - stderr(data) { - if (typeof data == "string") + stderr(data: any) { + if (typeof data === "string") this.errbuf += data; else this.errbuf += data.toString("utf8"); const end = this.errbuf.lastIndexOf('\n'); - if (end != -1) { + if (end !== -1) { this.onOutputStderr(this.errbuf.substring(0, end)); this.errbuf = this.errbuf.substring(end + 1); } @@ -314,14 +364,14 @@ export class MI2 extends EventEmitter implements IBackend { } } - onOutputStderr(lines) { - lines = lines.split('\n'); + onOutputStderr(str: string) { + const lines = str.split('\n'); lines.forEach(line => { this.log("stderr", line); }); } - onOutputPartial(line) { + onOutputPartial(line: string) { if (couldBeOutput(line)) { this.logNoNewLine("stdout", line); return true; @@ -329,8 +379,8 @@ export class MI2 extends EventEmitter implements IBackend { return false; } - onOutput(lines) { - lines = lines.split('\n'); + onOutput(str: string) { + const lines = str.split('\n'); lines.forEach(line => { if (couldBeOutput(line)) { if (!gdbMatch.exec(line)) @@ -347,19 +397,23 @@ export class MI2 extends EventEmitter implements IBackend { handled = true; } } - if (!handled && parsed.resultRecords && parsed.resultRecords.resultClass == "error") { + if (!handled && parsed.resultRecords && parsed.resultRecords.resultClass === "error") { this.log("stderr", parsed.result("msg") || line); } if (parsed.outOfBandRecord) { parsed.outOfBandRecord.forEach(record => { if (record.isStream) { this.log(record.type, record.content); + const logOutput = this.logMessage.logMsgOutput(record); + if(logOutput){ + this.log("console", logOutput); + } } else { - if (record.type == "exec") { + if (record.type === "exec") { this.emit("exec-async-output", parsed); - if (record.asyncClass == "running") + if (record.asyncClass === "running") this.emit("running", parsed); - else if (record.asyncClass == "stopped") { + else if (record.asyncClass === "stopped") { const reason = parsed.record("reason"); if (reason === undefined) { if (trace) @@ -373,6 +427,7 @@ export class MI2 extends EventEmitter implements IBackend { switch (reason) { case "breakpoint-hit": this.emit("breakpoint", parsed); + this.logMessage.logMsgProcess(parsed); break; case "watchpoint-trigger": case "read-watchpoint-trigger": @@ -391,13 +446,13 @@ export class MI2 extends EventEmitter implements IBackend { case "solib-event": case "syscall-entry": case "syscall-return": - // TODO: inform the user + // TODO: inform the user this.emit("step-end", parsed); break; case "fork": case "vfork": case "exec": - // TODO: inform the user, possibly add second inferior + // TODO: inform the user, possibly add second inferior this.emit("step-end", parsed); break; case "signal-received": @@ -423,10 +478,10 @@ export class MI2 extends EventEmitter implements IBackend { } } else this.log("log", JSON.stringify(parsed)); - } else if (record.type == "notify") { - if (record.asyncClass == "thread-created") { + } else if (record.type === "notify") { + if (record.asyncClass === "thread-created") { this.emit("thread-created", parsed); - } else if (record.asyncClass == "thread-exited") { + } else if (record.asyncClass === "thread-exited") { this.emit("thread-exited", parsed); } } @@ -434,7 +489,7 @@ export class MI2 extends EventEmitter implements IBackend { }); handled = true; } - if (parsed.token == undefined && parsed.resultRecords == undefined && parsed.outOfBandRecord.length == 0) + if (parsed.token == undefined && parsed.resultRecords == undefined && parsed.outOfBandRecord.length === 0) handled = true; if (!handled) this.log("log", "Unhandled: " + JSON.stringify(parsed)); @@ -450,7 +505,7 @@ export class MI2 extends EventEmitter implements IBackend { return new Promise((resolve, reject) => { this.log("console", "Running executable"); this.sendCommand(startCommand).then((info) => { - if (info.resultRecords.resultClass == "running") + if (info.resultRecords.resultClass === "running") resolve(undefined); else reject(); @@ -496,7 +551,7 @@ export class MI2 extends EventEmitter implements IBackend { this.log("stderr", "interrupt"); return new Promise((resolve, reject) => { this.sendCommand("exec-interrupt").then((info) => { - resolve(info.resultRecords.resultClass == "done"); + resolve(info.resultRecords.resultClass === "done"); }, reject); }); } @@ -506,7 +561,7 @@ export class MI2 extends EventEmitter implements IBackend { this.log("stderr", "continue"); return new Promise((resolve, reject) => { this.sendCommand("exec-continue" + (reverse ? " --reverse" : "")).then((info) => { - resolve(info.resultRecords.resultClass == "running"); + resolve(info.resultRecords.resultClass === "running"); }, reject); }); } @@ -516,7 +571,7 @@ export class MI2 extends EventEmitter implements IBackend { this.log("stderr", "next"); return new Promise((resolve, reject) => { this.sendCommand("exec-next" + (reverse ? " --reverse" : "")).then((info) => { - resolve(info.resultRecords.resultClass == "running"); + resolve(info.resultRecords.resultClass === "running"); }, reject); }); } @@ -526,7 +581,7 @@ export class MI2 extends EventEmitter implements IBackend { this.log("stderr", "step"); return new Promise((resolve, reject) => { this.sendCommand("exec-step" + (reverse ? " --reverse" : "")).then((info) => { - resolve(info.resultRecords.resultClass == "running"); + resolve(info.resultRecords.resultClass === "running"); }, reject); }); } @@ -536,7 +591,7 @@ export class MI2 extends EventEmitter implements IBackend { this.log("stderr", "stepOut"); return new Promise((resolve, reject) => { this.sendCommand("exec-finish" + (reverse ? " --reverse" : "")).then((info) => { - resolve(info.resultRecords.resultClass == "running"); + resolve(info.resultRecords.resultClass === "running"); }, reject); }); } @@ -548,7 +603,7 @@ export class MI2 extends EventEmitter implements IBackend { const target: string = '"' + (filename ? escape(filename) + ":" : "") + line + '"'; this.sendCommand("break-insert -t " + target).then(() => { this.sendCommand("exec-jump " + target).then((info) => { - resolve(info.resultRecords.resultClass == "running"); + resolve(info.resultRecords.resultClass === "running"); }, reject); }, reject); }); @@ -563,19 +618,35 @@ export class MI2 extends EventEmitter implements IBackend { loadBreakPoints(breakpoints: Breakpoint[]): Thenable<[boolean, Breakpoint][]> { if (trace) this.log("stderr", "loadBreakPoints"); - const promisses = []; + const promisses: Thenable<[boolean, Breakpoint]>[] = []; breakpoints.forEach(breakpoint => { promisses.push(this.addBreakPoint(breakpoint)); }); return Promise.all(promisses); } - setBreakPointCondition(bkptNum, condition): Thenable { + setBreakPointCondition(bkptNum: number, condition: string): Thenable { if (trace) this.log("stderr", "setBreakPointCondition"); return this.sendCommand("break-condition " + bkptNum + " " + condition); } + setLogPoint(bkptNum:number, command:string): Thenable { + const regex = /{([a-z0-9A-Z-_\.\>\&\*\[\]]*)}/gm; + let m:RegExpExecArray; + let commands:string = ""; + + while ((m = regex.exec(command))) { + if (m.index === regex.lastIndex) { + regex.lastIndex++; + } + if (m[1]) { + commands += `\"print ${m[1]}\" `; + } + } + return this.sendCommand("break-commands " + bkptNum + " " + commands); + } + setEntryBreakPoint(entryPoint: string): Thenable { return this.sendCommand("break-insert -t -f " + entryPoint); } @@ -588,14 +659,14 @@ export class MI2 extends EventEmitter implements IBackend { return resolve([false, undefined]); let location = ""; if (breakpoint.countCondition) { - if (breakpoint.countCondition[0] == ">") + if (breakpoint.countCondition[0] === ">") location += "-i " + numRegex.exec(breakpoint.countCondition.substring(1))[0] + " "; else { const match = numRegex.exec(breakpoint.countCondition)[0]; - if (match.length != breakpoint.countCondition.length) { + if (match.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"); location += "-t "; - } else if (parseInt(match) != 0) + } else if (parseInt(match) !== 0) location += "-t -i " + parseInt(match) + " "; } } @@ -604,18 +675,31 @@ export class MI2 extends EventEmitter implements IBackend { else location += '"' + escape(breakpoint.file) + ":" + breakpoint.line + '"'; this.sendCommand("break-insert -f " + location).then((result) => { - if (result.resultRecords.resultClass == "done") { + if (result.resultRecords.resultClass === "done") { const bkptNum = parseInt(result.result("bkpt.number")); const newBrk = { + id: bkptNum, file: breakpoint.file ? breakpoint.file : result.result("bkpt.file"), raw: breakpoint.raw, line: parseInt(result.result("bkpt.line")), - condition: breakpoint.condition + condition: breakpoint.condition, + logMessage: breakpoint?.logMessage, }; if (breakpoint.condition) { this.setBreakPointCondition(bkptNum, breakpoint.condition).then((result) => { - if (result.resultRecords.resultClass == "done") { + if (result.resultRecords.resultClass === "done") { + this.breakpoints.set(newBrk, bkptNum); + resolve([true, newBrk]); + } else { + resolve([false, undefined]); + } + }, reject); + } else if (breakpoint.logMessage) { + this.setLogPoint(bkptNum, breakpoint.logMessage).then((result) => { + if (result.resultRecords.resultClass === "done") { + breakpoint.id = newBrk.id; this.breakpoints.set(newBrk, bkptNum); + this.logMessage.logMsgBrkList.push(breakpoint); resolve([true, newBrk]); } else { resolve([false, undefined]); @@ -639,7 +723,7 @@ export class MI2 extends EventEmitter implements IBackend { if (!this.breakpoints.has(breakpoint)) return resolve(false); this.sendCommand("break-delete " + this.breakpoints.get(breakpoint)).then((result) => { - if (result.resultRecords.resultClass == "done") { + if (result.resultRecords.resultClass === "done") { this.breakpoints.delete(breakpoint); resolve(true); } else resolve(false); @@ -651,13 +735,13 @@ export class MI2 extends EventEmitter implements IBackend { if (trace) this.log("stderr", "clearBreakPoints"); return new Promise((resolve, reject) => { - const promises = []; + const promises: Thenable[] = []; const breakpoints = this.breakpoints; this.breakpoints = new Map(); breakpoints.forEach((k, index) => { if (index.file === source) { promises.push(this.sendCommand("break-delete " + k).then((result) => { - if (result.resultRecords.resultClass == "done") resolve(true); + if (result.resultRecords.resultClass === "done") resolve(true); else resolve(false); })); } else { @@ -675,15 +759,16 @@ export class MI2 extends EventEmitter implements IBackend { const result = await this.sendCommand(command); const threads = result.result("threads"); const ret: Thread[] = []; + if (!Array.isArray(threads)) { // workaround for lldb-mi bug: `'^done,threads="[]"'` + return ret; + } return threads.map(element => { const ret: Thread = { id: parseInt(MINode.valueOf(element, "id")), - targetId: MINode.valueOf(element, "target-id") + targetId: MINode.valueOf(element, "target-id"), + name: MINode.valueOf(element, "name") || MINode.valueOf(element, "details") }; - ret.name = MINode.valueOf(element, "details") - || undefined; - return ret; }); } @@ -693,7 +778,7 @@ export class MI2 extends EventEmitter implements IBackend { const options: string[] = []; - if (thread != 0) + if (thread !== 0) options.push("--thread " + thread); const depth: number = (await this.sendCommand(["stack-info-depth"].concat(options).join(" "))).result("depth").valueOf(); @@ -708,12 +793,17 @@ export class MI2 extends EventEmitter implements IBackend { const result = await this.sendCommand(["stack-list-frames"].concat(options).join(" ")); const stack = result.result("stack"); - return stack.map(element => { + return stack.map((element: any) => { const level = MINode.valueOf(element, "@frame.level"); const addr = MINode.valueOf(element, "@frame.addr"); const func = MINode.valueOf(element, "@frame.func"); const filename = MINode.valueOf(element, "@frame.file"); let file: string = MINode.valueOf(element, "@frame.fullname"); + if (!file) { + // Fallback to using `file` if `fullname` is not provided. + // GDB does this for some reason when frame filters are used. + file = MINode.valueOf(element, "@frame.file"); + } if (file) { if (this.isSSH) file = path.posix.normalize(file); @@ -758,6 +848,57 @@ export class MI2 extends EventEmitter implements IBackend { return ret; } + async getRegisters(): Promise { + if (trace) + this.log("stderr", "getRegisters"); + + // Getting register names and values are separate GDB commands. + // We first retrieve the register names and then the values. + // The register names should never change, so we could cache and reuse them, + // but for now we just retrieve them every time to keep it simple. + const names = await this.getRegisterNames(); + const values = await this.getRegisterValues(); + const ret: Variable[] = []; + for (const val of values) { + const key = names[val.index]; + const value = val.value; + const type = "string"; + ret.push({ + name: key, + valueStr: value, + type: type + }); + } + return ret; + } + + async getRegisterNames(): Promise { + if (trace) + this.log("stderr", "getRegisterNames"); + const result = await this.sendCommand("data-list-register-names"); + const names = result.result('register-names'); + if (!Array.isArray(names)) { + throw new Error('Failed to retrieve register names.'); + } + return names.map(name => name.toString()); + } + + async getRegisterValues(): Promise { + if (trace) + this.log("stderr", "getRegisterValues"); + const result = await this.sendCommand("data-list-register-values --skip-unavailable N " + this.registerLimit); + const nodes = result.result('register-values'); + if (!Array.isArray(nodes)) { + throw new Error('Failed to retrieve register values.'); + } + const ret: RegisterValue[] = nodes.map(node => { + const index = parseInt(MINode.valueOf(node, "number")); + const value = MINode.valueOf(node, "value"); + return { index: index, value: value }; + }); + return ret; + } + examineMemory(from: number, length: number): Thenable { if (trace) this.log("stderr", "examineMemory"); @@ -773,7 +914,7 @@ export class MI2 extends EventEmitter implements IBackend { this.log("stderr", "evalExpression"); let command = "data-evaluate-expression "; - if (thread != 0) { + if (thread !== 0) { command += `--thread ${thread} --frame ${frame} `; } command += name; @@ -781,10 +922,14 @@ export class MI2 extends EventEmitter implements IBackend { return await this.sendCommand(command); } - async varCreate(expression: string, name: string = "-", frame: string = "@"): Promise { + async varCreate(threadId: number, frameLevel: number, expression: string, name: string = "-", frame: string = "@"): Promise { if (trace) this.log("stderr", "varCreate"); - const res = await this.sendCommand(`var-create ${this.quote(name)} ${frame} "${expression}"`); + let miCommand = "var-create "; + if (threadId !== 0) { + miCommand += `--thread ${threadId} --frame ${frameLevel}`; + } + const res = await this.sendCommand(`${miCommand} ${this.quote(name)} ${frame} "${expression}"`); return new VariableObject(res.result("")); } @@ -800,7 +945,7 @@ export class MI2 extends EventEmitter implements IBackend { //TODO: add `from` and `to` arguments const res = await this.sendCommand(`var-list-children --all-values ${this.quote(name)}`); const children = res.result("children") || []; - const omg: VariableObject[] = children.map(child => new VariableObject(child[1])); + const omg: VariableObject[] = children.map((child: any) => new VariableObject(child[1])); return omg; } @@ -821,7 +966,7 @@ export class MI2 extends EventEmitter implements IBackend { } log(type: string, msg: string) { - this.emit("msg", type, msg[msg.length - 1] == '\n' ? msg : (msg + "\n")); + this.emit("msg", type, msg[msg.length - 1] === '\n' ? msg : (msg + "\n")); } sendUserInput(command: string, threadId: number = 0, frameLevel: number = 0): Thenable { @@ -843,7 +988,7 @@ export class MI2 extends EventEmitter implements IBackend { sendCliCommand(command: string, threadId: number = 0, frameLevel: number = 0): Thenable { let miCommand = "interpreter-exec "; - if (threadId != 0) { + if (threadId !== 0) { miCommand += `--thread ${threadId} --frame ${frameLevel} `; } miCommand += `console "${command.replace(/[\\"']/g, '\\$&')}"`; @@ -877,10 +1022,12 @@ export class MI2 extends EventEmitter implements IBackend { } prettyPrint: boolean = true; + frameFilters: boolean = true; printCalls: boolean; debugOutput: boolean; features: string[]; public procEnv: any; + public registerLimit: string; protected isSSH: boolean; protected sshReady: boolean; protected currentToken: number = 1; @@ -889,6 +1036,6 @@ export class MI2 extends EventEmitter implements IBackend { protected buffer: string; protected errbuf: string; protected process: ChildProcess.ChildProcess; - protected stream; - protected sshConn; + protected stream: ClientChannel; + protected sshConn: Client; } diff --git a/src/backend/mi2/mi2lldb.ts b/src/backend/mi2/mi2lldb.ts index 44999144..600c7931 100644 --- a/src/backend/mi2/mi2lldb.ts +++ b/src/backend/mi2/mi2lldb.ts @@ -4,7 +4,7 @@ import * as ChildProcess from "child_process"; import * as path from "path"; export class MI2_LLDB extends MI2 { - protected initCommands(target: string, cwd: string, attach: boolean = false) { + protected override initCommands(target: string, cwd: string, attach: boolean = false) { // We need to account for the possibility of the path type used by the debugger being different // than the path type where the extension is running (e.g., SSH from Linux to Windows machine). // Since the CWD is expected to be an absolute path in the debugger's environment, we can test @@ -35,14 +35,14 @@ export class MI2_LLDB extends MI2 { return cmds; } - attach(cwd: string, executable: string, target: string, autorun: string[]): Thenable { + override attach(cwd: string, executable: string, target: string, autorun: string[]): Thenable { return new Promise((resolve, reject) => { const args = this.preargs.concat(this.extraargs || []); this.process = ChildProcess.spawn(this.application, args, { cwd: cwd, env: this.procEnv }); 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) => { this.emit("launcherror", err); }).bind(this)); + this.process.on("exit", () => this.emit("quit")); + this.process.on("error", err => this.emit("launcherror", err)); const promises = this.initCommands(target, cwd, true); promises.push(this.sendCommand("file-exec-and-symbols \"" + escape(executable) + "\"")); promises.push(this.sendCommand("target-attach " + target)); @@ -54,11 +54,11 @@ export class MI2_LLDB extends MI2 { }); } - setBreakPointCondition(bkptNum, condition): Thenable { + override setBreakPointCondition(bkptNum: number, condition: string): Thenable { return this.sendCommand("break-condition " + bkptNum + " \"" + escape(condition) + "\" 1"); } - goto(filename: string, line: number): Thenable { + override goto(filename: string, line: number): Thenable { return new Promise((resolve, reject) => { // LLDB parses the file differently than GDB... // GDB doesn't allow quoting only the file but only the whole argument diff --git a/src/backend/mi2/mi2mago.ts b/src/backend/mi2/mi2mago.ts index 78e9f254..848ae854 100644 --- a/src/backend/mi2/mi2mago.ts +++ b/src/backend/mi2/mi2mago.ts @@ -3,14 +3,14 @@ import { Stack } from "../backend"; import { MINode } from "../mi_parse"; export class MI2_Mago extends MI2_LLDB { - getStack(startFrame: number, maxLevels: number, thread: number): Promise { + override getStack(startFrame: number, maxLevels: number, thread: number): Promise { return new Promise((resolve, reject) => { const command = "stack-list-frames"; this.sendCommand(command).then((result) => { const stack = result.resultRecords.results; const ret: Stack[] = []; - const remaining = []; - const addToStack = (element) => { + const remaining: any = []; + const addToStack = (element: any) => { const level = MINode.valueOf(element, "frame.level"); const addr = MINode.valueOf(element, "frame.addr"); const func = MINode.valueOf(element, "frame.func"); @@ -32,7 +32,7 @@ export class MI2_Mago extends MI2_LLDB { }; stack.forEach(element => { if (element) - if (element[0] == "stack") { + if (element[0] === "stack") { addToStack(element[1]); } else remaining.push(element); }); diff --git a/src/backend/mi_parse.ts b/src/backend/mi_parse.ts index 75a64ae3..9b761c9d 100644 --- a/src/backend/mi_parse.ts +++ b/src/backend/mi_parse.ts @@ -9,32 +9,32 @@ function parseString(str: string): string { const ret = Buffer.alloc(str.length * 4); let bufIndex = 0; - if (str[0] != '"' || str[str.length - 1] != '"') + 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 m; - if (str[i] == '\\') + if (str[i] === '\\') bufIndex += ret.write('\\', bufIndex); - else if (str[i] == '"') + else if (str[i] === '"') bufIndex += ret.write('"', bufIndex); - else if (str[i] == '\'') + else if (str[i] === '\'') bufIndex += ret.write('\'', bufIndex); - else if (str[i] == 'n') + else if (str[i] === 'n') bufIndex += ret.write('\n', bufIndex); - else if (str[i] == 'r') + else if (str[i] === 'r') bufIndex += ret.write('\r', bufIndex); - else if (str[i] == 't') + else if (str[i] === 't') bufIndex += ret.write('\t', bufIndex); - else if (str[i] == 'b') + else if (str[i] === 'b') bufIndex += ret.write('\b', bufIndex); - else if (str[i] == 'f') + else if (str[i] === 'f') bufIndex += ret.write('\f', bufIndex); - else if (str[i] == 'v') + else if (str[i] === 'v') bufIndex += ret.write('\v', bufIndex); - else if (str[i] == '0') + else if (str[i] === '0') bufIndex += ret.write('\0', bufIndex); else if (m = octalMatch.exec(str.substring(i))) { ret.writeUInt8(parseInt(m[0], 8), bufIndex++); @@ -43,9 +43,9 @@ function parseString(str: string): string { bufIndex += ret.write(str[i], bufIndex); escaped = false; } else { - if (str[i] == '\\') + if (str[i] === '\\') escaped = true; - else if (str[i] == '"') + else if (str[i] === '"') throw new Error("Not a valid string"); else bufIndex += ret.write(str[i], bufIndex); @@ -90,20 +90,20 @@ export class MINode implements MIInfo { let target = pathRegex.exec(path); if (target) { path = path.substring(target[0].length); - if (current.length && typeof current != "string") { + if (current.length && typeof current !== "string") { const found = []; for (const element of current) { - if (element[0] == target[1]) { + if (element[0] === target[1]) { found.push(element[1]); } } if (found.length > 1) { current = found; - } else if (found.length == 1) { + } else if (found.length === 1) { current = found[0]; } else return undefined; } else return undefined; - } else if (path[0] == '@') { + } else if (path[0] === '@') { current = [current]; path = path.substring(1); } else { @@ -111,9 +111,9 @@ export class MINode implements MIInfo { if (target) { path = path.substring(target[0].length); const i = parseInt(target[1]); - if (current.length && typeof current != "string" && i >= 0 && i < current.length) { + if (current.length && typeof current !== "string" && i >= 0 && i < current.length) { current = current[i]; - } else if (i == 0) { + } else if (i === 0) { // empty } else return undefined; } else return undefined; @@ -150,22 +150,22 @@ export function parseMI(output: string): MINode { */ let token = undefined; - const outOfBandRecord = []; + const outOfBandRecord: { isStream: boolean, type: string, asyncClass: string, output: [string, any][], content: string }[] = []; let resultRecords = undefined; const asyncRecordType = { "*": "exec", "+": "status", "=": "notify" - }; + } as const; const streamRecordType = { "~": "console", "@": "target", "&": "log" - }; + } as const; const parseCString = () => { - if (output[0] != '"') + if (output[0] !== '"') return ""; let stringEnd = 1; let inString = true; @@ -174,9 +174,9 @@ export function parseMI(output: string): MINode { while (inString) { if (escaped) escaped = false; - else if (remaining[0] == '\\') + else if (remaining[0] === '\\') escaped = true; - else if (remaining[0] == '"') + else if (remaining[0] === '"') inString = false; remaining = remaining.substring(1); @@ -192,15 +192,15 @@ export function parseMI(output: string): MINode { return str; }; - let parseValue, parseCommaResult, parseCommaValue, parseResult; + let parseValue: () => any, parseCommaResult: () => any, parseCommaValue: () => any, parseResult: () => any; const parseTupleOrList = () => { - if (output[0] != '{' && output[0] != '[') + if (output[0] !== '{' && output[0] !== '[') return undefined; const oldContent = output; - const canBeValueList = output[0] == '['; + const canBeValueList = output[0] === '['; output = output.substring(1); - if (output[0] == '}' || output[0] == ']') { + if (output[0] === '}' || output[0] === ']') { output = output.substring(1); // ] or } return []; } @@ -230,9 +230,9 @@ export function parseMI(output: string): MINode { }; parseValue = () => { - if (output[0] == '"') + if (output[0] === '"') return parseCString(); - else if (output[0] == '{' || output[0] == '[') + else if (output[0] === '{' || output[0] === '[') return parseTupleOrList(); else return undefined; @@ -248,14 +248,14 @@ export function parseMI(output: string): MINode { }; parseCommaValue = () => { - if (output[0] != ',') + if (output[0] !== ',') return undefined; output = output.substring(1); return parseValue(); }; parseCommaResult = () => { - if (output[0] != ',') + if (output[0] !== ',') return undefined; output = output.substring(1); return parseResult(); @@ -274,9 +274,10 @@ export function parseMI(output: string): MINode { output = output.substring(classMatch[0].length); const asyncRecord = { isStream: false, - type: asyncRecordType[match[2]], + type: asyncRecordType[match[2] as keyof typeof asyncRecordType], asyncClass: classMatch[0], - output: [] + output: [] as any, + content: "" }; let result; while (result = parseCommaResult()) @@ -285,8 +286,10 @@ export function parseMI(output: string): MINode { } else if (match[3]) { const streamRecord = { isStream: true, - type: streamRecordType[match[3]], - content: parseCString() + type: streamRecordType[match[3] as keyof typeof streamRecordType], + content: parseCString(), + output: [] as [string, any][], + asyncClass: "" }; outOfBandRecord.push(streamRecord); } @@ -310,5 +313,5 @@ export function parseMI(output: string): MINode { output = output.replace(newlineRegex, ""); } - return new MINode(token, outOfBandRecord || [], resultRecords); + return new MINode(token, outOfBandRecord || [], resultRecords); } diff --git a/src/frontend/extension.ts b/src/frontend/extension.ts index 222be7b3..46ad022b 100644 --- a/src/frontend/extension.ts +++ b/src/frontend/extension.ts @@ -34,7 +34,7 @@ function getMemoryRange(range: string) { return undefined; range = range.replace(/\s+/g, "").toLowerCase(); let index; - if ((index = range.indexOf("+")) != -1) { + if ((index = range.indexOf("+")) !== -1) { const from = range.substring(0, index); let length = range.substring(index + 1); if (!memoryLocationRegex.exec(from)) @@ -42,7 +42,7 @@ function getMemoryRange(range: string) { if (memoryLocationRegex.exec(length)) length = parseInt(length.substring(2), 16).toString(); return "from=" + encodeURIComponent(from) + "&length=" + encodeURIComponent(length); - } else if ((index = range.indexOf("-")) != -1) { + } else if ((index = range.indexOf("-")) !== -1) { const from = range.substring(0, index); const to = range.substring(index + 1); if (!memoryLocationRegex.exec(from)) @@ -58,28 +58,28 @@ function getMemoryRange(range: string) { function examineMemory() { const socketlists = path.join(os.tmpdir(), "code-debug-sockets"); if (!fs.existsSync(socketlists)) { - if (process.platform == "win32") + if (process.platform === "win32") return vscode.window.showErrorMessage("This command is not available on windows"); else return vscode.window.showErrorMessage("No debugging sessions available"); } fs.readdir(socketlists, (err, files) => { if (err) { - if (process.platform == "win32") + if (process.platform === "win32") return vscode.window.showErrorMessage("This command is not available on windows"); else return vscode.window.showErrorMessage("No debugging sessions available"); } - const pickedFile = (file) => { + const pickedFile = (file: string) => { vscode.window.showInputBox({ placeHolder: "Memory Location or Range", validateInput: range => getMemoryRange(range) === undefined ? "Range must either be in format 0xF00-0xF01, 0xF100+32 or 0xABC154" : "" }).then(range => { vscode.window.showTextDocument(vscode.Uri.parse("debugmemory://" + file + "?" + getMemoryRange(range))); }); }; - if (files.length == 1) + if (files.length === 1) pickedFile(files[0]); else if (files.length > 0) vscode.window.showQuickPick(files, { placeHolder: "Running debugging instance" }).then(file => pickedFile(file)); - else if (process.platform == "win32") + else if (process.platform === "win32") return vscode.window.showErrorMessage("This command is not available on windows"); else vscode.window.showErrorMessage("No debugging sessions available"); @@ -90,19 +90,19 @@ class MemoryContentProvider implements vscode.TextDocumentContentProvider { provideTextDocumentContent(uri: vscode.Uri, token: vscode.CancellationToken): Thenable { return new Promise((resolve, reject) => { const conn = net.connect(path.join(os.tmpdir(), "code-debug-sockets", uri.authority.toLowerCase())); - let from, to; + let from: number, to: number; let highlightAt = -1; const splits = uri.query.split("&"); - if (splits[0].split("=")[0] == "at") { + if (splits[0].split("=")[0] === "at") { const loc = parseInt(splits[0].split("=")[1].substring(2), 16); highlightAt = 64; from = Math.max(loc - 64, 0); to = Math.max(loc + 768, 0); - } else if (splits[0].split("=")[0] == "from") { + } else if (splits[0].split("=")[0] === "from") { from = parseInt(splits[0].split("=")[1].substring(2), 16); - if (splits[1].split("=")[0] == "to") { + if (splits[1].split("=")[0] === "to") { to = parseInt(splits[1].split("=")[1].substring(2), 16); - } else if (splits[1].split("=")[0] == "length") { + } else if (splits[1].split("=")[0] === "length") { to = from + parseInt(splits[1].split("=")[1]); } else return reject("Invalid Range"); } else return reject("Invalid Range"); @@ -117,7 +117,7 @@ class MemoryContentProvider implements vscode.TextDocumentContentProvider { let asciiLine = ""; let byteNo = 0; for (let i = 0; i < hexString.length; i += 2) { - if (x == 0) { + if (x === 0) { let addr = index.toString(16); while (addr.length < 16) addr = '0' + addr; formattedCode += addr + " "; @@ -131,13 +131,13 @@ class MemoryContentProvider implements vscode.TextDocumentContentProvider { else asciiLine += "."; - if (highlightAt == byteNo) { + if (highlightAt === byteNo) { formattedCode = formattedCode.slice(0, -1) + "[" + digit + "]"; } else { formattedCode += digit + " "; } - if (x == 7) + if (x === 7) formattedCode += " "; if (++x >= 16) { diff --git a/src/gdb.ts b/src/gdb.ts index 3c0357bf..c1e178c0 100644 --- a/src/gdb.ts +++ b/src/gdb.ts @@ -17,8 +17,10 @@ export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArgum stopAtEntry: boolean | string; ssh: SSHArguments; valuesFormatting: ValuesFormattingMode; + frameFilters: boolean; printCalls: boolean; showDevDebugOutput: boolean; + registerLimit: string; } export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { @@ -35,12 +37,14 @@ export interface AttachRequestArguments extends DebugProtocol.AttachRequestArgum stopAtEntry: boolean | string; ssh: SSHArguments; valuesFormatting: ValuesFormattingMode; + frameFilters: boolean; printCalls: boolean; showDevDebugOutput: boolean; + registerLimit: string; } class GDBDebugSession extends MI2DebugSession { - protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { + protected override initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { response.body.supportsGotoTargetsRequest = true; response.body.supportsHitConditionalBreakpoints = true; response.body.supportsConfigurationDoneRequest = true; @@ -49,11 +53,17 @@ class GDBDebugSession extends MI2DebugSession { response.body.supportsEvaluateForHovers = true; response.body.supportsSetVariable = true; response.body.supportsStepBack = true; + response.body.supportsLogPoints = true; this.sendResponse(response); } - protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { - this.miDebugger = new MI2(args.gdbpath || "gdb", ["-q", "--interpreter=mi2"], args.debugger_args, args.env); + protected override launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { + const dbgCommand = args.gdbpath || "gdb"; + if (!this.checkCommand(dbgCommand)) { + this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`); + return; + } + this.miDebugger = new MI2(dbgCommand, ["-q", "--interpreter=mi2"], args.debugger_args, args.env); this.setPathSubstitutions(args.pathSubstitutions); this.initDebugger(); this.quit = false; @@ -63,9 +73,11 @@ class GDBDebugSession extends MI2DebugSession { this.started = false; this.crashed = false; this.setValuesFormattingMode(args.valuesFormatting); + this.miDebugger.frameFilters = !!args.frameFilters; this.miDebugger.printCalls = !!args.printCalls; this.miDebugger.debugOutput = !!args.showDevDebugOutput; this.stopAtEntry = args.stopAtEntry; + this.miDebugger.registerLimit = args.registerLimit ?? ""; if (args.ssh !== undefined) { if (args.ssh.forwardX11 === undefined) args.ssh.forwardX11 = true; @@ -93,8 +105,13 @@ class GDBDebugSession extends MI2DebugSession { } } - protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { - this.miDebugger = new MI2(args.gdbpath || "gdb", ["-q", "--interpreter=mi2"], args.debugger_args, args.env); + protected override attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { + const dbgCommand = args.gdbpath || "gdb"; + if (!this.checkCommand(dbgCommand)) { + this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`); + return; + } + this.miDebugger = new MI2(dbgCommand, ["-q", "--interpreter=mi2"], args.debugger_args, args.env); this.setPathSubstitutions(args.pathSubstitutions); this.initDebugger(); this.quit = false; @@ -102,9 +119,11 @@ class GDBDebugSession extends MI2DebugSession { this.initialRunCommand = args.stopAtConnect ? RunCommand.NONE : RunCommand.CONTINUE; this.isSSH = false; this.setValuesFormattingMode(args.valuesFormatting); + this.miDebugger.frameFilters = !!args.frameFilters; this.miDebugger.printCalls = !!args.printCalls; this.miDebugger.debugOutput = !!args.showDevDebugOutput; this.stopAtEntry = args.stopAtEntry; + this.miDebugger.registerLimit = args.registerLimit ?? ""; if (args.ssh !== undefined) { if (args.ssh.forwardX11 === undefined) args.ssh.forwardX11 = true; diff --git a/src/lldb.ts b/src/lldb.ts index 79b7e709..0d7bbd46 100644 --- a/src/lldb.ts +++ b/src/lldb.ts @@ -18,6 +18,7 @@ export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArgum valuesFormatting: ValuesFormattingMode; printCalls: boolean; showDevDebugOutput: boolean; + registerLimit: string; } export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { @@ -34,10 +35,11 @@ export interface AttachRequestArguments extends DebugProtocol.AttachRequestArgum valuesFormatting: ValuesFormattingMode; printCalls: boolean; showDevDebugOutput: boolean; + registerLimit: string; } class LLDBDebugSession extends MI2DebugSession { - protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { + protected override initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { response.body.supportsGotoTargetsRequest = true; response.body.supportsHitConditionalBreakpoints = true; response.body.supportsConfigurationDoneRequest = true; @@ -47,8 +49,13 @@ class LLDBDebugSession extends MI2DebugSession { this.sendResponse(response); } - protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { - this.miDebugger = new MI2_LLDB(args.lldbmipath || "lldb-mi", [], args.debugger_args, args.env); + protected override launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { + const dbgCommand = args.lldbmipath || "lldb-mi"; + if (!this.checkCommand(dbgCommand)) { + this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`); + return; + } + this.miDebugger = new MI2_LLDB(dbgCommand, [], args.debugger_args, args.env); this.setPathSubstitutions(args.pathSubstitutions); this.initDebugger(); this.quit = false; @@ -61,6 +68,7 @@ class LLDBDebugSession extends MI2DebugSession { this.miDebugger.printCalls = !!args.printCalls; this.miDebugger.debugOutput = !!args.showDevDebugOutput; this.stopAtEntry = args.stopAtEntry; + this.miDebugger.registerLimit = args.registerLimit ?? ""; if (args.ssh !== undefined) { if (args.ssh.forwardX11 === undefined) args.ssh.forwardX11 = true; @@ -88,8 +96,13 @@ class LLDBDebugSession extends MI2DebugSession { } } - protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { - this.miDebugger = new MI2_LLDB(args.lldbmipath || "lldb-mi", [], args.debugger_args, args.env); + protected override attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { + const dbgCommand = args.lldbmipath || "lldb-mi"; + if (!this.checkCommand(dbgCommand)) { + this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`); + return; + } + this.miDebugger = new MI2_LLDB(dbgCommand, [], args.debugger_args, args.env); this.setPathSubstitutions(args.pathSubstitutions); this.initDebugger(); this.quit = false; @@ -100,6 +113,7 @@ class LLDBDebugSession extends MI2DebugSession { this.miDebugger.printCalls = !!args.printCalls; this.miDebugger.debugOutput = !!args.showDevDebugOutput; this.stopAtEntry = args.stopAtEntry; + this.miDebugger.registerLimit = args.registerLimit ?? ""; this.miDebugger.attach(args.cwd, args.executable, args.target, args.autorun || []).then(() => { this.sendResponse(response); }, err => { diff --git a/src/mago.ts b/src/mago.ts index d9d8fd0d..20f7f2fe 100644 --- a/src/mago.ts +++ b/src/mago.ts @@ -15,6 +15,7 @@ export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArgum valuesFormatting: ValuesFormattingMode; printCalls: boolean; showDevDebugOutput: boolean; + registerLimit: string; } export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { @@ -29,6 +30,7 @@ export interface AttachRequestArguments extends DebugProtocol.AttachRequestArgum valuesFormatting: ValuesFormattingMode; printCalls: boolean; showDevDebugOutput: boolean; + registerLimit: string; } class MagoDebugSession extends MI2DebugSession { @@ -36,7 +38,7 @@ class MagoDebugSession extends MI2DebugSession { super(debuggerLinesStartAt1, isServer); } - protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { + protected override initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { response.body.supportsHitConditionalBreakpoints = true; response.body.supportsConfigurationDoneRequest = true; response.body.supportsConditionalBreakpoints = true; @@ -49,8 +51,13 @@ class MagoDebugSession extends MI2DebugSession { return 0; } - protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { - this.miDebugger = new MI2_Mago(args.magomipath || "mago-mi", ["-q"], args.debugger_args, args.env); + protected override launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { + const dbgCommand = args.magomipath || "mago-mi"; + if (!this.checkCommand(dbgCommand)) { + this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`); + return; + } + this.miDebugger = new MI2_Mago(dbgCommand, ["-q"], args.debugger_args, args.env); this.initDebugger(); this.quit = false; this.attached = false; @@ -61,6 +68,7 @@ class MagoDebugSession extends MI2DebugSession { this.setValuesFormattingMode(args.valuesFormatting); this.miDebugger.printCalls = !!args.printCalls; this.miDebugger.debugOutput = !!args.showDevDebugOutput; + this.miDebugger.registerLimit = args.registerLimit ?? ""; this.miDebugger.load(args.cwd, args.target, args.arguments, undefined, args.autorun || []).then(() => { this.sendResponse(response); }, err => { @@ -68,8 +76,13 @@ class MagoDebugSession extends MI2DebugSession { }); } - protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { - this.miDebugger = new MI2_Mago(args.magomipath || "mago-mi", [], args.debugger_args, args.env); + protected override attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { + const dbgCommand = args.magomipath || "mago-mi"; + if (!this.checkCommand(dbgCommand)) { + this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`); + return; + } + this.miDebugger = new MI2_Mago(dbgCommand, ["-q"], args.debugger_args, args.env); this.initDebugger(); this.quit = false; this.attached = true; @@ -78,6 +91,7 @@ class MagoDebugSession extends MI2DebugSession { this.setValuesFormattingMode(args.valuesFormatting); this.miDebugger.printCalls = !!args.printCalls; this.miDebugger.debugOutput = !!args.showDevDebugOutput; + this.miDebugger.registerLimit = args.registerLimit ?? ""; this.miDebugger.attach(args.cwd, args.executable, args.target, args.autorun || []).then(() => { this.sendResponse(response); }, err => { diff --git a/src/mibase.ts b/src/mibase.ts index f0a19c6b..eb22088e 100644 --- a/src/mibase.ts +++ b/src/mibase.ts @@ -5,6 +5,7 @@ import { Breakpoint, IBackend, Variable, VariableObject, ValuesFormattingMode, M import { MINode } from './backend/mi_parse'; import { expandValue, isExpandable } from './backend/gdb_expansion'; import { MI2 } from './backend/mi2/mi2'; +import { execSync } from 'child_process'; import * as systemPath from "path"; import * as net from "net"; import * as os from "os"; @@ -12,15 +13,15 @@ import * as fs from "fs"; import { SourceFileMap } from "./source_file_map"; class ExtendedVariable { - constructor(public name, public options) { + constructor(public name: string, public options: { "arg": any }) { } } class VariableScope { - constructor (public readonly name: string, public readonly threadId: number, public readonly level: number) { + constructor(public readonly name: string, public readonly threadId: number, public readonly level: number) { } - public static variableName (handle: number, name: string): string { + public static variableName(handle: number, name: string): string { return `var_${handle}_${name}`; } } @@ -70,28 +71,45 @@ export class MI2DebugSession extends DebugSession { const spaceIndex = rawCmd.indexOf(" "); let func = rawCmd; let args = []; - if (spaceIndex != -1) { + if (spaceIndex !== -1) { func = rawCmd.substring(0, spaceIndex); args = JSON.parse(rawCmd.substring(spaceIndex + 1)); } - Promise.resolve(this.miDebugger[func].apply(this.miDebugger, args)).then(data => { + Promise.resolve((this.miDebugger as any)[func].apply(this.miDebugger, args)).then(data => { c.write(data.toString()); }); }); }); this.commandServer.on("error", err => { - if (process.platform != "win32") + if (process.platform !== "win32") this.handleMsg("stderr", "Code-Debug WARNING: Utility Command Server: Error in command socket " + err.toString() + "\nCode-Debug WARNING: The examine memory location command won't work"); }); if (!fs.existsSync(systemPath.join(os.tmpdir(), "code-debug-sockets"))) fs.mkdirSync(systemPath.join(os.tmpdir(), "code-debug-sockets")); this.commandServer.listen(this.serverPath = systemPath.join(os.tmpdir(), "code-debug-sockets", ("Debug-Instance-" + Math.floor(Math.random() * 36 * 36 * 36 * 36).toString(36)).toLowerCase())); } catch (e) { - if (process.platform != "win32") + if (process.platform !== "win32") this.handleMsg("stderr", "Code-Debug WARNING: Utility Command Server: Failed to start " + e.toString() + "\nCode-Debug WARNING: The examine memory location command won't work"); } } + // verifies that the specified command can be executed + protected checkCommand(debuggerName: string): boolean { + try { + if (process.platform === 'win32' && debuggerName.includes("\\")) { + // For Windows paths containing backslashes, check if the file exists directly + return fs.existsSync(debuggerName); + } + else { + const command = process.platform === 'win32' ? 'where' : 'command -v'; + execSync(`${command} ${debuggerName}`, { stdio: 'ignore' }); + return true; + } + } catch (error) { + return false; + } + } + protected setValuesFormattingMode(mode: ValuesFormattingMode) { switch (mode) { case "disabled": @@ -110,28 +128,28 @@ export class MI2DebugSession extends DebugSession { } protected handleMsg(type: string, msg: string) { - if (type == "target") + if (type === "target") type = "stdout"; - if (type == "log") + if (type === "log") type = "stderr"; this.sendEvent(new OutputEvent(msg, type)); } protected handleBreakpoint(info: MINode) { const event = new StoppedEvent("breakpoint", parseInt(info.record("thread-id"))); - (event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") == "all"; + (event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") === "all"; this.sendEvent(event); } protected handleBreak(info?: MINode) { const event = new StoppedEvent("step", info ? parseInt(info.record("thread-id")) : 1); - (event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info ? info.record("stopped-threads") == "all" : true; + (event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info ? info.record("stopped-threads") === "all" : true; this.sendEvent(event); } protected handlePause(info: MINode) { const event = new StoppedEvent("user request", parseInt(info.record("thread-id"))); - (event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") == "all"; + (event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") === "all"; this.sendEvent(event); } @@ -140,7 +158,7 @@ export class MI2DebugSession extends DebugSession { this.crashed = true; if (!this.quit) { const event = new StoppedEvent("exception", parseInt(info.record("thread-id"))); - (event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") == "all"; + (event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") === "all"; this.sendEvent(event); } } @@ -170,7 +188,7 @@ export class MI2DebugSession extends DebugSession { this.quitEvent(); } - protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): void { + protected override disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): void { if (this.attached) this.miDebugger.detach(); else @@ -180,7 +198,7 @@ export class MI2DebugSession extends DebugSession { this.sendResponse(response); } - protected async setVariableRequest(response: DebugProtocol.SetVariableResponse, args: DebugProtocol.SetVariableArguments): Promise { + protected override async setVariableRequest(response: DebugProtocol.SetVariableResponse, args: DebugProtocol.SetVariableArguments): Promise { try { if (this.useVarObjects) { let name = args.name; @@ -207,16 +225,16 @@ export class MI2DebugSession extends DebugSession { } } - protected setFunctionBreakPointsRequest(response: DebugProtocol.SetFunctionBreakpointsResponse, args: DebugProtocol.SetFunctionBreakpointsArguments): void { - const all = []; + protected override setFunctionBreakPointsRequest(response: DebugProtocol.SetFunctionBreakpointsResponse, args: DebugProtocol.SetFunctionBreakpointsArguments): void { + const all: Thenable<[boolean, Breakpoint]>[] = []; args.breakpoints.forEach(brk => { all.push(this.miDebugger.addBreakPoint({ raw: brk.name, condition: brk.condition, countCondition: brk.hitCondition })); }); Promise.all(all).then(brkpoints => { - const finalBrks = []; + const finalBrks: DebugProtocol.Breakpoint[] = []; brkpoints.forEach(brkp => { if (brkp[0]) - finalBrks.push({ line: brkp[1].line }); + finalBrks.push({ line: brkp[1].line, verified: true }); }); response.body = { breakpoints: finalBrks @@ -227,7 +245,7 @@ export class MI2DebugSession extends DebugSession { }); } - protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { + protected override setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { let path = args.source.path; if (this.isSSH) { // convert local path to ssh path @@ -235,10 +253,10 @@ export class MI2DebugSession extends DebugSession { } this.miDebugger.clearBreakPoints(path).then(() => { const all = args.breakpoints.map(brk => { - return this.miDebugger.addBreakPoint({ file: path, line: brk.line, condition: brk.condition, countCondition: brk.hitCondition }); + return this.miDebugger.addBreakPoint({ file: path, line: brk.line, condition: brk.condition, countCondition: brk.hitCondition, logMessage: brk.logMessage }); }); Promise.all(all).then(brkpoints => { - const finalBrks = []; + const finalBrks: DebugProtocol.Breakpoint[] = []; brkpoints.forEach(brkp => { // TODO: Currently all breakpoints returned are marked as verified, // which leads to verified breakpoints on a broken lldb. @@ -257,7 +275,7 @@ export class MI2DebugSession extends DebugSession { }); } - protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { + protected override threadsRequest(response: DebugProtocol.ThreadsResponse): void { if (!this.miDebugger) { this.sendResponse(response); return; @@ -271,7 +289,11 @@ export class MI2DebugSession extends DebugSession { response.body.threads.push(new Thread(thread.id, thread.id + ":" + threadName)); } this.sendResponse(response); - }).catch(error => { + }).catch((error: MIError) => { + if (error.message === 'Selected thread is running.') { + this.sendResponse(response); + return; + } this.sendErrorResponse(response, 17, `Could not get threads: ${error}`); }); } @@ -284,7 +306,7 @@ export class MI2DebugSession extends DebugSession { return [frameId & 0xffff, frameId >> 16]; } - protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { + protected override stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { this.miDebugger.getStack(args.startFrame, args.levels, args.threadId).then(stack => { const ret: StackFrame[] = []; stack.forEach(element => { @@ -304,7 +326,7 @@ export class MI2DebugSession extends DebugSession { ret.push(new StackFrame( this.threadAndLevelToFrameId(args.threadId, element.level), - element.function + "@" + element.address, + element.function + (element.address ? "@" + element.address : ""), source, element.line, 0)); @@ -318,7 +340,7 @@ export class MI2DebugSession extends DebugSession { }); } - protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void { + protected override configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void { const promises: Thenable[] = []; let entryPoint: string | undefined = undefined; let runToStart: boolean = false; @@ -326,18 +348,18 @@ export class MI2DebugSession extends DebugSession { switch (this.initialRunCommand) { case RunCommand.CONTINUE: case RunCommand.NONE: - if (typeof this.stopAtEntry == 'boolean' && this.stopAtEntry) + if (typeof this.stopAtEntry === 'boolean' && this.stopAtEntry) entryPoint = "main"; // sensible default - else if (typeof this.stopAtEntry == 'string') + else if (typeof this.stopAtEntry === 'string') entryPoint = this.stopAtEntry; break; case RunCommand.RUN: - if (typeof this.stopAtEntry == 'boolean' && this.stopAtEntry) { + if (typeof this.stopAtEntry === 'boolean' && this.stopAtEntry) { if (this.miDebugger.features.includes("exec-run-start-option")) runToStart = true; else entryPoint = "main"; // sensible fallback - } else if (typeof this.stopAtEntry == 'string') + } else if (typeof this.stopAtEntry === 'string') entryPoint = this.stopAtEntry; break; default: @@ -387,7 +409,7 @@ export class MI2DebugSession extends DebugSession { }); } - protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { + protected override scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { const scopes = new Array(); const [threadId, level] = this.frameIdToThreadAndLevel(args.frameId); @@ -405,7 +427,8 @@ export class MI2DebugSession extends DebugSession { return new Scope(scopeName, handle, expensive); }; - scopes.push(createScope("Local", false)); + scopes.push(createScope("Locals", false)); + scopes.push(createScope("Registers", false)); response.body = { scopes: scopes @@ -413,13 +436,13 @@ export class MI2DebugSession extends DebugSession { this.sendResponse(response); } - protected async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): Promise { + protected override async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): Promise { const variables: DebugProtocol.Variable[] = []; const id: VariableScope | string | VariableObject | ExtendedVariable = this.variableHandles.get(args.variablesReference); - const createVariable = (arg, options?) => { + const createVariable = (arg: string | VariableObject, options?: any) => { if (options) - return this.variableHandles.create(new ExtendedVariable(arg, options)); + return this.variableHandles.create(new ExtendedVariable(typeof arg === 'string' ? arg : arg.name, options)); else return this.variableHandles.create(arg); }; @@ -436,64 +459,74 @@ export class MI2DebugSession extends DebugSession { }; if (id instanceof VariableScope) { - let stack: Variable[]; try { - stack = await this.miDebugger.getStackVariables(id.threadId, id.level); - for (const variable of stack) { - if (this.useVarObjects) { - try { - const varObjName = VariableScope.variableName(args.variablesReference, variable.name); - let varObj: VariableObject; + if (id.name === "Registers") { + const registers = await this.miDebugger.getRegisters(); + for (const reg of registers) { + variables.push({ + name: reg.name, + value: reg.valueStr, + variablesReference: 0 + }); + } + } else { + const stack: Variable[] = await this.miDebugger.getStackVariables(id.threadId, id.level); + for (const variable of stack) { + if (this.useVarObjects) { try { - const changes = await this.miDebugger.varUpdate(varObjName); - const changelist = changes.result("changelist"); - changelist.forEach((change) => { - const name = MINode.valueOf(change, "name"); - const vId = this.variableHandlesReverse[name]; - const v = this.variableHandles.get(vId) as any; - v.applyChanges(change); - }); - const varId = this.variableHandlesReverse[varObjName]; - varObj = this.variableHandles.get(varId) as any; - } catch (err) { - if (err instanceof MIError && err.message == "Variable object not found") { - varObj = await this.miDebugger.varCreate(variable.name, varObjName); - const varId = findOrCreateVariable(varObj); - varObj.exp = variable.name; - varObj.id = varId; - } else { - throw err; + const varObjName = VariableScope.variableName(args.variablesReference, variable.name); + let varObj: VariableObject; + try { + const changes = await this.miDebugger.varUpdate(varObjName); + const changelist = changes.result("changelist"); + changelist.forEach((change: any) => { + const name = MINode.valueOf(change, "name"); + const vId = this.variableHandlesReverse[name]; + const v = this.variableHandles.get(vId) as any; + v.applyChanges(change); + }); + const varId = this.variableHandlesReverse[varObjName]; + varObj = this.variableHandles.get(varId) as any; + } catch (err) { + if (err instanceof MIError && (err.message === "Variable object not found" || err.message.endsWith("does not exist"))) { + varObj = await this.miDebugger.varCreate(id.threadId, id.level, variable.name, varObjName); + const varId = findOrCreateVariable(varObj); + varObj.exp = variable.name; + varObj.id = varId; + } else { + throw err; + } } + variables.push(varObj.toProtocolVariable()); + } catch (err) { + variables.push({ + name: variable.name, + value: `<${err}>`, + variablesReference: 0 + }); } - variables.push(varObj.toProtocolVariable()); - } catch (err) { - variables.push({ - name: variable.name, - value: `<${err}>`, - variablesReference: 0 - }); + } else { + if (variable.valueStr !== undefined) { + let expanded = expandValue(createVariable, `{${variable.name}=${variable.valueStr})`, "", variable.raw); + if (expanded) { + if (typeof expanded[0] === "string") + expanded = [ + { + name: "", + value: prettyStringArray(expanded), + variablesReference: 0 + } + ]; + variables.push(expanded[0]); + } + } else + variables.push({ + name: variable.name, + type: variable.type, + value: variable.type, + variablesReference: createVariable(variable.name) + }); } - } else { - if (variable.valueStr !== undefined) { - let expanded = expandValue(createVariable, `{${variable.name}=${variable.valueStr})`, "", variable.raw); - if (expanded) { - if (typeof expanded[0] == "string") - expanded = [ - { - name: "", - value: prettyStringArray(expanded), - variablesReference: 0 - } - ]; - variables.push(expanded[0]); - } - } else - variables.push({ - name: variable.name, - type: variable.type, - value: "", - variablesReference: createVariable(variable.name) - }); } } response.body = { @@ -503,18 +536,25 @@ export class MI2DebugSession extends DebugSession { } catch (err) { this.sendErrorResponse(response, 1, `Could not expand variable: ${err}`); } - } else if (typeof id == "string") { + } else if (typeof id === "string") { // Variable members let variable; try { // TODO: this evaluates on an (effectively) unknown thread for multithreaded programs. variable = await this.miDebugger.evalExpression(JSON.stringify(id), 0, 0); try { - let expanded = expandValue(createVariable, variable.result("value"), id, variable); + let variableValue = variable.result("value"); + const pattern = /'([^']*)' /g; + variableValue = variableValue.replace(pattern, (_: any, char: string, count: string) => { + const repeatCount = parseInt(count, 10) + 1; + const repeatedArray = Array(repeatCount).fill(char); + return `{${repeatedArray.map(item => `'${item}'`).join(', ')}}`; + }); + let expanded = expandValue(createVariable, variableValue, id, variable); if (!expanded) { this.sendErrorResponse(response, 2, `Could not expand variable`); } else { - if (typeof expanded[0] == "string") + if (typeof expanded[0] === "string") expanded = [ { name: "", @@ -533,7 +573,7 @@ export class MI2DebugSession extends DebugSession { } catch (err) { this.sendErrorResponse(response, 1, `Could not expand variable: ${err}`); } - } else if (typeof id == "object") { + } else if (typeof id === "object") { if (id instanceof VariableObject) { // Variable members let children: VariableObject[]; @@ -555,7 +595,7 @@ export class MI2DebugSession extends DebugSession { } else if (id instanceof ExtendedVariable) { const varReq = id; if (varReq.options.arg) { - const strArr = []; + const strArr: DebugProtocol.Variable[] = []; let argsPart = true; let arrIndex = 0; const submit = () => { @@ -572,13 +612,13 @@ export class MI2DebugSession extends DebugSession { if (!expanded) { this.sendErrorResponse(response, 15, `Could not expand variable`); } else { - if (typeof expanded == "string") { - if (expanded == "") { + if (typeof expanded === "string") { + if (expanded === "") { if (argsPart) argsPart = false; else return submit(); - } else if (expanded[0] != '"') { + } else if (expanded[0] !== '"') { strArr.push({ name: "[err]", value: expanded, @@ -622,7 +662,7 @@ export class MI2DebugSession extends DebugSession { } } - protected pauseRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { + protected override pauseRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { this.miDebugger.interrupt().then(done => { this.sendResponse(response); }, msg => { @@ -630,7 +670,7 @@ export class MI2DebugSession extends DebugSession { }); } - protected reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse, args: DebugProtocol.ReverseContinueArguments): void { + protected override reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse, args: DebugProtocol.ReverseContinueArguments): void { this.miDebugger.continue(true).then(done => { this.sendResponse(response); }, msg => { @@ -638,7 +678,7 @@ export class MI2DebugSession extends DebugSession { }); } - protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { + protected override continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { this.miDebugger.continue().then(done => { this.sendResponse(response); }, msg => { @@ -646,7 +686,7 @@ export class MI2DebugSession extends DebugSession { }); } - protected stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments): void { + protected override stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments): void { this.miDebugger.step(true).then(done => { this.sendResponse(response); }, msg => { @@ -654,7 +694,7 @@ export class MI2DebugSession extends DebugSession { }); } - protected stepInRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { + protected override stepInRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { this.miDebugger.step().then(done => { this.sendResponse(response); }, msg => { @@ -662,7 +702,7 @@ export class MI2DebugSession extends DebugSession { }); } - protected stepOutRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { + protected override stepOutRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { this.miDebugger.stepOut().then(done => { this.sendResponse(response); }, msg => { @@ -670,7 +710,7 @@ export class MI2DebugSession extends DebugSession { }); } - protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { + protected override nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { this.miDebugger.next().then(done => { this.sendResponse(response); }, msg => { @@ -678,9 +718,9 @@ export class MI2DebugSession extends DebugSession { }); } - protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { + protected override evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { const [threadId, level] = this.frameIdToThreadAndLevel(args.frameId); - if (args.context == "watch" || args.context == "hover") { + if (args.context === "watch" || args.context === "hover") { this.miDebugger.evalExpression(args.expression, threadId, level).then((res) => { response.body = { variablesReference: 0, @@ -688,11 +728,16 @@ export class MI2DebugSession extends DebugSession { }; this.sendResponse(response); }, msg => { - this.sendErrorResponse(response, 7, msg.toString()); + if (args.context === "hover") { + // suppress error for hover as the user may just play with the mouse + this.sendResponse(response); + } else { + this.sendErrorResponse(response, 7, msg.toString()); + } }); } else { this.miDebugger.sendUserInput(args.expression, threadId, level).then(output => { - if (typeof output == "undefined") + if (typeof output === "undefined") response.body = { result: "", variablesReference: 0 @@ -709,7 +754,7 @@ export class MI2DebugSession extends DebugSession { } } - protected gotoTargetsRequest(response: DebugProtocol.GotoTargetsResponse, args: DebugProtocol.GotoTargetsArguments): void { + protected override gotoTargetsRequest(response: DebugProtocol.GotoTargetsResponse, args: DebugProtocol.GotoTargetsArguments): void { const path: string = this.isSSH ? this.sourceFileMap.toRemotePath(args.source.path) : args.source.path; this.miDebugger.goto(path, args.line).then(done => { response.body = { @@ -717,7 +762,7 @@ export class MI2DebugSession extends DebugSession { id: 1, label: args.source.name, column: args.column, - line : args.line + line: args.line }] }; this.sendResponse(response); @@ -726,13 +771,13 @@ export class MI2DebugSession extends DebugSession { }); } - protected gotoRequest(response: DebugProtocol.GotoResponse, args: DebugProtocol.GotoArguments): void { + protected override gotoRequest(response: DebugProtocol.GotoResponse, args: DebugProtocol.GotoArguments): void { this.sendResponse(response); } protected setSourceFileMap(configMap: { [index: string]: string }, fallbackGDB: string, fallbackIDE: string): void { if (configMap === undefined) { - this.sourceFileMap = new SourceFileMap({[fallbackGDB]: fallbackIDE}); + this.sourceFileMap = new SourceFileMap({ [fallbackGDB]: fallbackIDE }); } else { this.sourceFileMap = new SourceFileMap(configMap, fallbackGDB); } @@ -740,8 +785,8 @@ export class MI2DebugSession extends DebugSession { } -function prettyStringArray(strings) { - if (typeof strings == "object") { +function prettyStringArray(strings: any) { + if (typeof strings === "object") { if (strings.length !== undefined) return strings.join(", "); else diff --git a/src/source_file_map.ts b/src/source_file_map.ts index b33424be..49ddcf39 100755 --- a/src/source_file_map.ts +++ b/src/source_file_map.ts @@ -38,7 +38,7 @@ export class SourceFileMap { // allowing non-native path types to be tested by overriding this method in // a subclass in the test harness. protected getNativePath(): PathKind { - if (process.platform == "win32") + if (process.platform === "win32") return PathWin32.getInstance(); else return PathPosix.getInstance(); diff --git a/src/test/unit/gdb_expansion.test.ts b/src/test/unit/gdb_expansion.test.ts index 9f00a8cc..dc4b8ff9 100644 --- a/src/test/unit/gdb_expansion.test.ts +++ b/src/test/unit/gdb_expansion.test.ts @@ -1,8 +1,9 @@ import * as assert from 'assert'; import { expandValue, isExpandable } from '../../backend/gdb_expansion'; +import { VariableObject } from '../../backend/backend'; suite("GDB Value Expansion", () => { - const variableCreate = (variable) => { return { expanded: variable }; }; + const variableCreate = (variable: string) => ({ expanded: variable }); test("Various values", () => { assert.strictEqual(isExpandable(`false`), 0); assert.strictEqual(expandValue(variableCreate, `false`), "false"); @@ -223,6 +224,133 @@ suite("GDB Value Expansion", () => { } ]); }); + test("QT Complex node", () => { + const node = `{{ref = {atomic = {_q_value = {> = {static _S_alignment = 4, _M_i = -1}, }}}, size = 0, static shared_null = }, {ref = {atomic = {_q_value = {> = {static _S_alignment = 4, _M_i = 0}, }}}, size = 0, static shared_null = }}`; + assert.strictEqual(isExpandable(node), 1); + const variables = expandValue(variableCreate, node); + assert.deepStrictEqual(variables, [ + { + "name": "[0]", + "value": "Object", + "variablesReference": { + "expanded": [ + { + "name": "ref", + "value": "Object", + "variablesReference": { + "expanded": [ + { + "name": "atomic", + "value": "Object", + "variablesReference": { + "expanded": [ + { + "name": "_q_value", + "value": "Object", + "variablesReference": { + "expanded": [ + { + "name": "std::__atomic_base>", + "value": "Object", + "variablesReference": { + "expanded": [ + { + "name": "_S_alignment", + "value": "4", + "variablesReference": 0 + }, + { + "name": "_M_i", + "value": "-1", + "variablesReference": 0 + } + ] + } + } + ] + } + } + ] + } + } + ] + } + }, + { + "name": "size", + "value": "0", + "variablesReference": 0 + }, + { + "name": "shared_null", + "value": "", + "variablesReference": 0 + } + ] + } + }, + { + "name": "[1]", + "value": "Object", + "variablesReference": { + "expanded": [ + { + "name": "ref", + "value": "Object", + "variablesReference": { + "expanded": [ + { + "name": "atomic", + "value": "Object", + "variablesReference": { + "expanded": [ + { + "name": "_q_value", + "value": "Object", + "variablesReference": { + "expanded": [ + { + "name": "std::__atomic_base>", + "value": "Object", + "variablesReference": { + "expanded": [ + { + "name": "_S_alignment", + "value": "4", + "variablesReference": 0 + }, + { + "name": "_M_i", + "value": "0", + "variablesReference": 0 + } + ] + } + } + ] + } + } + ] + } + } + ] + } + }, + { + "name": "size", + "value": "0", + "variablesReference": 0 + }, + { + "name": "shared_null", + "value": "", + "variablesReference": 0 + } + ] + } + } + ]); + }); test("Simple node with errors", () => { const node = `{_enableMipMaps = false, _minFilter = , _magFilter = , _wrapX = , _wrapY = , _inMode = 6408, _mode = 6408, _id = 1, _width = 1024, _height = 1024}`; assert.strictEqual(isExpandable(node), 1); @@ -303,4 +431,92 @@ suite("GDB Value Expansion", () => { { name: "floatval2", value: "234.45", variablesReference: 0 } ]); }); + test("std container values", () => { + let node = `std::vector of length 4, capacity 6 = {1, 2, 3, 4}`; + let variables = expandValue(variableCreate, node); + assert.strictEqual(variables[3].value, `4`); + node = `std::deque with 7 elements = {0, 1, 2, 3, 4, 5, 6}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[6].value, `6`); + node = `std::__cxx11::list = {[0] = 6, [1] = 7, [2] = 8, [3] = 9, [4] = 10}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[4].value, `10`); + node = `std::vector of length 4, capacity 6 = {std::vector of length 3, capacity 3 = {1, 2, 3}, std::vector of length 3, capacity 3 = {4, 5, 6}, std::vector of length 3, capacity 3 = {7, 8, 9}, std::vector of length 4, capacity 4 = {1, 2, 3, 4}}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[3].variablesReference.expanded[3].value, `4`); + node = `std::deque with 3 elements = {std::deque with 2 elements = {10, 20}, std::deque with 2 elements = {30, 40}, std::deque with 7 elements = {0, 1, 2, 3, 4, 5, 6}}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[2].variablesReference.expanded[6].value, `6`); + node = `std::__cxx11::list = {[0] = std::__cxx11::list = {[0] = 11, [1] = 12}, [1] = std::__cxx11::list = {[0] = 13, [1] = 14}, [2] = std::__cxx11::list = {[0] = 6, [1] = 7, [2] = 8, [3] = 9, [4] = 10}}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[2].variablesReference.expanded[4].value, `10`); + node = `std::forward_list = {[0] = 0, [1] = 1, [2] = 2, [3] = 3}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[3].value, `3`); + node = `std::set with 5 elements = {[0] = 1, [1] = 2, [2] = 3, [3] = 4, [4] = 5}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[4].value, `5`); + node = `std::multiset with 6 elements = {[0] = 1, [1] = 2, [2] = 2, [3] = 2, [4] = 3, [5] = 4}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[5].value, `4`); + node = `std::map with 3 elements = {[1] = \"one\", [2] = \"two\", [3] = \"three\"}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[2].value, `"three"`); + node = `std::multimap with 3 elements = {[1] = \"one\", [2] = \"two\", [2] = \"another two\"}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[2].value, `"another two"`); + node = `std::unordered_set with 5 elements = {[0] = 5, [1] = 2, [2] = 3, [3] = 1, [4] = 4}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[4].value, `4`); + node = `std::unordered_multiset with 6 elements = {[0] = 1, [1] = 2, [2] = 2, [3] = 2, [4] = 3, [5] = 4}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[5].value, `4`); + node = `std::unordered_map with 3 elements = {[3] = \"three\", [1] = \"one\", [2] = \"two\"}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[2].value, `"two"`); + node = `std::unordered_multimap with 3 elements = {[2] = \"another two\", [2] = \"two\", [1] = \"one\"}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[2].value, `"one"`); + node = `std::stack wrapping: std::deque with 2 elements = {1, 2}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[1].value, `2`); + node = `std::queue wrapping: std::deque with 2 elements = {1, 2}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[1].value, `2`); + node = `std::priority_queue wrapping: std::vector of length 3, capacity 4 = {4, 1, 3}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[2].value, `3`); + node = `std::bitset = {[0] = 1, [2] = 1, [4] = 1, [6] = 1, [7] = 1}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[3].value, `1`); + node = `{_M_elems = {1, 2, 3}}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[0].variablesReference.expanded[2].value, `3`); + node = `std::unique_ptr = {get() = 0x5555555d7d20}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[0].name, `get()`); + assert.strictEqual(variables[0].value, `Object@*0x5555555d7d20`); + node = `std::unique_ptr = {get() = 0x5555555d7d40}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[0].name, `get()`); + assert.strictEqual(variables[0].value, `Object@*0x5555555d7d40`); + node = `std::shared_ptr (use count 1, weak count 1) = {get() = 0x5555555d7d70}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[0].name, `get()`); + assert.strictEqual(variables[0].value, `Object@*0x5555555d7d70`); + node = `{> = {static _S_alignment = 4, _M_i = 0}, }`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[0].name, `std::__atomic_base>`); + assert.strictEqual(variables[0].variablesReference.expanded[1].name, `_M_i`); + assert.strictEqual(variables[0].variablesReference.expanded[1].value, `0`); + node = `{ = {_M_mutex = {__data = {__lock = 0, __count = 0, __owner = 0, __nusers = 0, __kind = 0, __spins = 0, __elision = 0, __list = {__prev = 0x0, __next = 0x0}}, __size = '\\000' , __align = 0}}, }`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[0].variablesReference.expanded[0].variablesReference.expanded[0].variablesReference.expanded[7].variablesReference.expanded[1].name, `__next`); + assert.strictEqual(variables[0].variablesReference.expanded[0].variablesReference.expanded[0].variablesReference.expanded[7].variablesReference.expanded[1].value, ``); + node = `{static icase = , static nosubs = , static optimize = , static collate = , static ECMAScript = (unknown: 16), static basic = , static extended = , static awk = , static grep = , static egrep = , _M_flags = (unknown: 16), _M_loc = {static none = 0, static ctype = 1, static numeric = 2, static collate = 4, static time = 8, static monetary = 16, static messages = 32, static all = 63, _M_impl = 0x7ffff7fa8ce0, static _S_classic = , static _S_global = , static _S_categories = , static _S_once = , static _S_twinned_facets = }, _M_automaton = std::shared_ptr >> (use count 1, weak count 0) = {get() = 0x5555555d81b0}}`; + variables = expandValue(variableCreate, node); + assert.strictEqual(variables[12].name, `_M_automaton`); + assert.strictEqual(variables[12].variablesReference.expanded[0].value, `Object@*0x5555555d81b0`); + + }); }); diff --git a/src/test/unit/source_file_map.test.ts b/src/test/unit/source_file_map.test.ts index 450c2b00..162a984f 100755 --- a/src/test/unit/source_file_map.test.ts +++ b/src/test/unit/source_file_map.test.ts @@ -25,7 +25,7 @@ suite("Source File Map", () => { } } const fileMap: NativeSourceFileMap = new NativeSourceFileMap({}); - if (process.platform == "win32") + if (process.platform === "win32") assert.ok(fileMap.getNativePathTest() instanceof PathWin32); else assert.ok(fileMap.getNativePathTest() instanceof PathPosix); diff --git a/tsconfig.json b/tsconfig.json index 915ff8fc..97824ed7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,8 @@ { "name": "tslint-language-service" } - ] + ], + "noImplicitAny": true }, "exclude": [ "node_modules",