diff --git a/arduino-debugger-extension/src/browser/arduino-variable-resolver.ts b/arduino-debugger-extension/src/browser/arduino-variable-resolver.ts index ab527eb3..b094f1e1 100644 --- a/arduino-debugger-extension/src/browser/arduino-variable-resolver.ts +++ b/arduino-debugger-extension/src/browser/arduino-variable-resolver.ts @@ -2,9 +2,12 @@ import { VariableContribution, VariableRegistry, Variable } from '@theia/variable-resolver/lib/browser'; import { injectable, inject } from 'inversify'; import URI from '@theia/core/lib/common/uri'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import { ApplicationShell, Navigatable } from '@theia/core/lib/browser'; +import { FileStat, FileSystem } from '@theia/filesystem/lib/common'; +import { WorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution'; import { BoardsServiceClientImpl } from 'arduino-ide-extension/lib/browser/boards/boards-service-client-impl'; import { BoardsService, ToolLocations } from 'arduino-ide-extension/lib/common/protocol/boards-service'; -import { WorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution'; @injectable() export class ArduinoVariableResolver implements VariableContribution { @@ -18,64 +21,134 @@ export class ArduinoVariableResolver implements VariableContribution { @inject(WorkspaceVariableContribution) protected readonly workspaceVars: WorkspaceVariableContribution; + @inject(ApplicationShell) + protected readonly applicationShell: ApplicationShell; + + @inject(FileSystem) + protected readonly fileSystem: FileSystem; + + @inject(MessageService) + protected readonly messageService: MessageService + registerVariables(variables: VariableRegistry): void { variables.registerVariable({ - name: `boardTools`, - description: "Provides paths and access to board specific tooling", + name: 'boardTools', + description: 'Provides paths and access to board specific tooling', resolve: this.resolveBoardTools.bind(this), }); variables.registerVariable({ - name: "board", - description: "Provides details about the currently selected board", + name: 'board', + description: 'Provides details about the currently selected board', resolve: this.resolveBoard.bind(this), }); - variables.registerVariable({ - name: "sketchBinary", - description: "Path to the sketch's binary file", + name: 'sketchBinary', + description: 'Path to the sketch\'s binary file', resolve: this.resolveSketchBinary.bind(this) }); } - // TODO: this function is a total hack. Instead of botching around with URI's it should ask something on the backend - // that properly udnerstands the filesystem. - protected async resolveSketchBinary(context?: URI, argument?: string, configurationSection?: string): Promise { - let sketchPath = argument || this.workspaceVars.getResourceUri()!.path.toString(); - return sketchPath.substring(0, sketchPath.length - 3) + "arduino.samd.arduino_zero_edbg.elf"; + protected resolveSketchBinary(context?: URI, argument?: string, configurationSection?: string): Promise { + if (argument) { + return this.resolveBinaryWithHint(argument); + } + const resourceUri = this.workspaceVars.getResourceUri(); + if (resourceUri) { + return this.resolveBinaryWithHint(resourceUri.toString()); + } + for (const tabBar of this.applicationShell.mainAreaTabBars) { + if (tabBar.currentTitle && Navigatable.is(tabBar.currentTitle.owner)) { + const uri = tabBar.currentTitle.owner.getResourceUri(); + if (uri) { + return this.resolveBinaryWithHint(uri.toString()); + } + } + } + this.messageService.error('No sketch available. Please open a sketch to start debugging.'); + return Promise.resolve(undefined); } - protected async resolveBoard(context?: URI, argument?: string, configurationSection?: string): Promise { - const { boardsConfig } = this.boardsServiceClient; - if (!boardsConfig || !boardsConfig.selectedBoard) { - throw new Error('No boards selected. Please select a board.'); + private async resolveBinaryWithHint(hint: string): Promise { + const fileStat = await this.fileSystem.getFileStat(hint); + if (!fileStat) { + this.messageService.error('Cannot find sketch binary: ' + hint); + return undefined; + } + if (!fileStat.isDirectory && fileStat.uri.endsWith('.elf')) { + return fileStat.uri; } - if (!argument || argument === "fqbn") { + let parent: FileStat | undefined; + let prefix: string | undefined; + let suffix: string; + if (fileStat.isDirectory) { + parent = fileStat; + } else { + const uri = new URI(fileStat.uri); + parent = await this.fileSystem.getFileStat(uri.parent.toString()); + prefix = uri.path.name; + } + const { boardsConfig } = this.boardsServiceClient; + if (boardsConfig && boardsConfig.selectedBoard && boardsConfig.selectedBoard.fqbn) { + suffix = boardsConfig.selectedBoard.fqbn.replace(/:/g, '.') + '.elf'; + } else { + suffix = '.elf'; + } + if (parent && parent.children) { + let bin: FileStat | undefined; + if (prefix) { + bin = parent.children.find(c => c.uri.startsWith(prefix!) && c.uri.endsWith(suffix)); + } + if (!bin) { + bin = parent.children.find(c => c.uri.endsWith(suffix)); + } + if (!bin && suffix.length > 4) { + bin = parent.children.find(c => c.uri.endsWith('.elf')); + } + if (bin) { + return bin.uri; + } + } + this.messageService.error('Cannot find sketch binary: ' + hint); + return undefined; + } + + protected async resolveBoard(context?: URI, argument?: string, configurationSection?: string): Promise { + const { boardsConfig } = this.boardsServiceClient; + if (!boardsConfig || !boardsConfig.selectedBoard) { + this.messageService.error('No boards selected. Please select a board.'); + return undefined; + } + + if (!argument || argument === 'fqbn') { return boardsConfig.selectedBoard.fqbn!; } - if (argument === "name") { + if (argument === 'name') { return boardsConfig.selectedBoard.name; } - const details = await this.boardsService.detail({id: boardsConfig.selectedBoard.fqbn!}); + const details = await this.boardsService.detail({ id: boardsConfig.selectedBoard.fqbn! }); if (!details.item) { - throw new Error("Cannot get board details"); + this.messageService.error('Details of the selected boards are not available.'); + return undefined; } - if (argument === "openocd-debug-file") { + if (argument === 'openocd-debug-file') { return details.item.locations!.debugScript; } return boardsConfig.selectedBoard.fqbn!; } - protected async resolveBoardTools(context?: URI, argument?: string, configurationSection?: string): Promise { + protected async resolveBoardTools(context?: URI, argument?: string, configurationSection?: string): Promise { const { boardsConfig } = this.boardsServiceClient; if (!boardsConfig || !boardsConfig.selectedBoard) { - throw new Error('No boards selected. Please select a board.'); + this.messageService.error('No boards selected. Please select a board.'); + return undefined; } - const details = await this.boardsService.detail({id: boardsConfig.selectedBoard.fqbn!}); + const details = await this.boardsService.detail({ id: boardsConfig.selectedBoard.fqbn! }); if (!details.item) { - throw new Error("Cannot get board details") + this.messageService.error('Details of the selected boards are not available.'); + return undefined; } let toolLocations: { [name: string]: ToolLocations } = {}; @@ -84,17 +157,37 @@ export class ArduinoVariableResolver implements VariableContribution { }) switch (argument) { - case "openocd": - return toolLocations["openocd"].main; - case "openocd-scripts": - return toolLocations["openocd"].scripts; - case "objdump": - return toolLocations["arm-none-eabi-gcc"].objdump; - case "gdb": - return toolLocations["arm-none-eabi-gcc"].gdb; + case 'openocd': { + const openocd = toolLocations['openocd']; + if (openocd) { + return openocd.main; + } + this.messageService.error('Unable to find debugging tool: openocd'); + return undefined; + } + case 'openocd-scripts': { + const openocd = toolLocations['openocd']; + return openocd ? openocd.scripts : undefined; + } + case 'objdump': { + const gcc = Object.keys(toolLocations).find(key => key.endsWith('gcc')); + if (gcc) { + return toolLocations[gcc].objdump; + } + this.messageService.error('Unable to find debugging tool: objdump'); + return undefined; + } + case 'gdb': { + const gcc = Object.keys(toolLocations).find(key => key.endsWith('gcc')); + if (gcc) { + return toolLocations[gcc].gdb; + } + this.messageService.error('Unable to find debugging tool: gdb'); + return undefined; + } } return boardsConfig.selectedBoard.name; } -} \ No newline at end of file +} diff --git a/arduino-debugger-extension/src/node/arduino-debug-adapter-contribution.ts b/arduino-debugger-extension/src/node/arduino-debug-adapter-contribution.ts index ab383bf8..5b1f62ad 100644 --- a/arduino-debugger-extension/src/node/arduino-debug-adapter-contribution.ts +++ b/arduino-debugger-extension/src/node/arduino-debug-adapter-contribution.ts @@ -1,59 +1,59 @@ import { injectable } from 'inversify'; import { DebugAdapterContribution, DebugAdapterExecutable, DebugAdapterSessionFactory } from '@theia/debug/lib/common/debug-model'; -import { DebugConfiguration } from "@theia/debug/lib/common/debug-configuration"; -import { MaybePromise } from "@theia/core/lib/common/types"; -import { IJSONSchema, IJSONSchemaSnippet } from "@theia/core/lib/common/json-schema"; +import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration'; +import { MaybePromise } from '@theia/core/lib/common/types'; +import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema'; import * as path from 'path'; @injectable() export class ArduinoDebugAdapterContribution implements DebugAdapterContribution { - type = "arduino"; + type = 'arduino'; - label = "Arduino"; + label = 'Arduino'; - languages = ["c", "cpp", "ino"]; + languages = ['c', 'cpp', 'ino']; debugAdapterSessionFactory?: DebugAdapterSessionFactory; - getSchemaAttributes?(): MaybePromise { + getSchemaAttributes(): MaybePromise { return [ { - "required": [ - "program" + 'required': [ + 'program' ], - "properties": { - "sketch": { - "type": "string", - "description": "path to the sketch root ino file", - "default": "${file}", + 'properties': { + 'sketch': { + 'type': 'string', + 'description': 'path to the sketch root ino file', + 'default': '${file}', }, - "fqbn": { - "type": "string", - "description": "Fully-qualified board name to debug on", - "default": "" + 'fqbn': { + 'type': 'string', + 'description': 'Fully-qualified board name to debug on', + 'default': '' }, - "runToMain": { - "description": "If enabled the debugger will run until the start of the main function.", - "type": "boolean", - "default": false + 'runToMain': { + 'description': 'If enabled the debugger will run until the start of the main function.', + 'type': 'boolean', + 'default': false }, - "verbose": { - "type": "boolean", - "description": "Produce verbose log output", - "default": false + 'verbose': { + 'type': 'boolean', + 'description': 'Produce verbose log output', + 'default': false }, - "debugDebugAdapter": { - "type": "boolean", - "description": "Start the debug adapter in debug mode (with --inspect-brk)", - "default": false + 'debugDebugAdapter': { + 'type': 'boolean', + 'description': 'Start the debug adapter in debug mode (with --inspect-brk)', + 'default': false }, } } ] } - getConfigurationSnippets?(): MaybePromise { + getConfigurationSnippets(): MaybePromise { return [] } @@ -65,29 +65,29 @@ export class ArduinoDebugAdapterContribution implements DebugAdapterContribution args = args.concat([path.join(__dirname, 'debug-adapter', 'main')]); return { - command: "node", + command: 'node', args: args, } } - provideDebugConfigurations?(workspaceFolderUri?: string): MaybePromise { + provideDebugConfigurations(workspaceFolderUri?: string): MaybePromise { return [ { name: this.label, type: this.type, - request: "launch", - sketch: "${file}", + request: 'launch', + sketch: '${file}', }, { - name: this.label + " (explicit)", + name: this.label + ' (explicit)', type: this.type, - request: "launch", + request: 'launch', - program: "${sketchBinary}", - objdump: "${boardTools:objdump}", - gdb: "${boardTools:gdb}", - gdbServer: "${boardTools:openocd}", - gdbServerArguments: ["-s", "${boardTools:openocd-scripts}", "--file", "${board:openocd-debug-file}"], + program: '${sketchBinary}', + objdump: '${boardTools:objdump}', + gdb: '${boardTools:gdb}', + gdbServer: '${boardTools:openocd}', + gdbServerArguments: ['-s', '${boardTools:openocd-scripts}', '--file', '${board:openocd-debug-file}'], runToMain: false, verbose: false, @@ -95,23 +95,23 @@ export class ArduinoDebugAdapterContribution implements DebugAdapterContribution ]; } - async resolveDebugConfiguration?(config: DebugConfiguration, workspaceFolderUri?: string): Promise { + async resolveDebugConfiguration(config: DebugConfiguration, workspaceFolderUri?: string): Promise { // if program is present we expect to have an explicit config here if (!!config.program) { return config; } - let sketchBinary = "${sketchBinary}" - if (config.sketch !== "${file}") { - sketchBinary = "${sketchBinary:" + config.sketch + "}"; + let sketchBinary = '${sketchBinary}' + if (typeof config.sketch === 'string' && config.sketch.indexOf('${') < 0) { + sketchBinary = '${sketchBinary:' + config.sketch + '}'; } const res: ActualDebugConfig = { ...config, - objdump: "${boardTools:objdump}", - gdb: "${boardTools:gdb}", - gdbServer: "${boardTools:openocd}", - gdbServerArguments: ["-s", "${boardTools:openocd-scripts}", "--file", "${board:openocd-debug-file}"], + objdump: '${boardTools:objdump}', + gdb: '${boardTools:gdb}', + gdbServer: '${boardTools:openocd}', + gdbServerArguments: ['-s', '${boardTools:openocd-scripts}', '--file', '${board:openocd-debug-file}'], program: sketchBinary } return res; diff --git a/arduino-debugger-extension/src/node/debug-adapter/abstract-server.ts b/arduino-debugger-extension/src/node/debug-adapter/abstract-server.ts index baf053d7..0a839038 100644 --- a/arduino-debugger-extension/src/node/debug-adapter/abstract-server.ts +++ b/arduino-debugger-extension/src/node/debug-adapter/abstract-server.ts @@ -48,7 +48,14 @@ export abstract class AbstractServer extends EventEmitter { try { this.timer = setTimeout(() => this.onSpawnError(new Error('Timeout waiting for gdb server to start')), TIMEOUT); - const command = args.gdbServer || 'gdb-server'; + const command = args.gdbServer; + if (!command) { + throw new Error('Missing parameter: gdbServer'); + } + const varRegexp = /\$\{.*\}/; + if (varRegexp.test(command)) { + throw new Error(`Unresolved variable: ${command}`) + } const serverArguments = await this.resolveServerArguments(args); this.process = spawn(command, serverArguments, { cwd: dirname(command), @@ -110,7 +117,7 @@ export abstract class AbstractServer extends EventEmitter { protected onData(chunk: string | Buffer, buffer: string, event: string) { buffer += typeof chunk === 'string' ? chunk - : chunk.toString('utf8'); + : chunk.toString('utf8'); const end = buffer.lastIndexOf('\n'); if (end !== -1) { diff --git a/arduino-debugger-extension/src/node/debug-adapter/cmsis-debug-session.ts b/arduino-debugger-extension/src/node/debug-adapter/cmsis-debug-session.ts index 0584a154..a22e8d0d 100644 --- a/arduino-debugger-extension/src/node/debug-adapter/cmsis-debug-session.ts +++ b/arduino-debugger-extension/src/node/debug-adapter/cmsis-debug-session.ts @@ -71,7 +71,8 @@ export class CmsisDebugSession extends GDBDebugSession { await this.runSession(args); this.sendResponse(response); } catch (err) { - this.sendErrorResponse(response, 1, err.message); + const message = `Failed to launch the debugger:\n${err.message}`; + this.sendErrorResponse(response, 1, message); } } @@ -80,7 +81,8 @@ export class CmsisDebugSession extends GDBDebugSession { await this.runSession(args); this.sendResponse(response); } catch (err) { - this.sendErrorResponse(response, 1, err.message); + const message = `Failed to attach the debugger:\n${err.message}`; + this.sendErrorResponse(response, 1, message); } } @@ -302,6 +304,14 @@ export class CmsisDebugSession extends GDBDebugSession { }); } + protected spawn(args: CmsisRequestArguments): Promise { + const varRegexp = /\$\{.*\}/; + if (args.gdb && varRegexp.test(args.gdb)) { + throw new Error(`Unresolved variable: ${args.gdb}`) + } + return super.spawn(args); + } + private async getGlobalVariables(frameHandle: number): Promise { const frame = this.frameHandles.get(frameHandle); const symbolInfo = this.symbolTable.getGlobalVariables(); @@ -393,7 +403,7 @@ export class CmsisDebugSession extends GDBDebugSession { codeOrMessage: number | DebugProtocol.Message, format?: string, variables?: any, dest?: ErrorDestination): void { if (!!format && (dest === undefined || dest === ErrorDestination.User)) { - format = format.replace('\n', '
'); + format = format.replace(/\n/g, '
'); } super.sendErrorResponse(response, codeOrMessage, format, variables, dest); } diff --git a/arduino-debugger-extension/src/node/debug-adapter/symbols.ts b/arduino-debugger-extension/src/node/debug-adapter/symbols.ts index 1846cb2b..716f596d 100644 --- a/arduino-debugger-extension/src/node/debug-adapter/symbols.ts +++ b/arduino-debugger-extension/src/node/debug-adapter/symbols.ts @@ -25,7 +25,7 @@ */ import { spawnSync } from 'child_process'; -import { platform, EOL } from 'os'; +import { EOL } from 'os'; import { dirname, normalize, basename } from 'path'; export enum SymbolType { @@ -53,7 +53,6 @@ export interface SymbolInformation { hidden: boolean; } -const DEFAULT_OBJDUMP = platform() !== 'win32' ? 'arm-none-eabi-objdump' : 'arm-none-eabi-objdump.exe'; const SYMBOL_REGEX = /^([0-9a-f]{8})\s([lg\ !])([w\ ])([C\ ])([W\ ])([I\ ])([dD\ ])([FfO\ ])\s([^\s]+)\s([0-9a-f]+)\s(.*)\r?$/; const TYPE_MAP: { [id: string]: SymbolType } = { @@ -74,7 +73,14 @@ export class SymbolTable { private symbols: SymbolInformation[] = []; - constructor(private program: string, private objdump: string = DEFAULT_OBJDUMP) { + constructor(private program: string, private objdump?: string) { + const varRegexp = /\$\{.*\}/; + if (varRegexp.test(program)) { + throw new Error(`Unresolved variable: ${program}`); + } + if (objdump && varRegexp.test(objdump)) { + throw new Error(`Unresolved variable: ${objdump}`); + } } public async loadSymbols(): Promise { @@ -128,6 +134,9 @@ export class SymbolTable { private execute(): Promise { return new Promise((resolve, reject) => { + if (!this.objdump) { + return reject(new Error('Missing parameter: objdump')); + } try { const { stdout, stderr } = spawnSync(this.objdump, [ '--syms',