Improved variable resolution and error handling

This commit is contained in:
Miro Spönemann 2019-12-20 15:58:13 +01:00
parent 1441b685ee
commit a886a106f5
5 changed files with 212 additions and 93 deletions

View File

@ -2,9 +2,12 @@
import { VariableContribution, VariableRegistry, Variable } from '@theia/variable-resolver/lib/browser'; import { VariableContribution, VariableRegistry, Variable } from '@theia/variable-resolver/lib/browser';
import { injectable, inject } from 'inversify'; import { injectable, inject } from 'inversify';
import URI from '@theia/core/lib/common/uri'; 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 { 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 { BoardsService, ToolLocations } from 'arduino-ide-extension/lib/common/protocol/boards-service';
import { WorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution';
@injectable() @injectable()
export class ArduinoVariableResolver implements VariableContribution { export class ArduinoVariableResolver implements VariableContribution {
@ -18,64 +21,134 @@ export class ArduinoVariableResolver implements VariableContribution {
@inject(WorkspaceVariableContribution) @inject(WorkspaceVariableContribution)
protected readonly workspaceVars: 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 { registerVariables(variables: VariableRegistry): void {
variables.registerVariable(<Variable>{ variables.registerVariable(<Variable>{
name: `boardTools`, name: 'boardTools',
description: "Provides paths and access to board specific tooling", description: 'Provides paths and access to board specific tooling',
resolve: this.resolveBoardTools.bind(this), resolve: this.resolveBoardTools.bind(this),
}); });
variables.registerVariable(<Variable>{ variables.registerVariable(<Variable>{
name: "board", name: 'board',
description: "Provides details about the currently selected board", description: 'Provides details about the currently selected board',
resolve: this.resolveBoard.bind(this), resolve: this.resolveBoard.bind(this),
}); });
variables.registerVariable({ variables.registerVariable({
name: "sketchBinary", name: 'sketchBinary',
description: "Path to the sketch's binary file", description: 'Path to the sketch\'s binary file',
resolve: this.resolveSketchBinary.bind(this) 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 protected resolveSketchBinary(context?: URI, argument?: string, configurationSection?: string): Promise<Object | undefined> {
// that properly udnerstands the filesystem. if (argument) {
protected async resolveSketchBinary(context?: URI, argument?: string, configurationSection?: string): Promise<Object> { return this.resolveBinaryWithHint(argument);
let sketchPath = argument || this.workspaceVars.getResourceUri()!.path.toString(); }
return sketchPath.substring(0, sketchPath.length - 3) + "arduino.samd.arduino_zero_edbg.elf"; 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<Object> { private async resolveBinaryWithHint(hint: string): Promise<string | undefined> {
const { boardsConfig } = this.boardsServiceClient; const fileStat = await this.fileSystem.getFileStat(hint);
if (!boardsConfig || !boardsConfig.selectedBoard) { if (!fileStat) {
throw new Error('No boards selected. Please select a board.'); 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<string | undefined> {
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!; return boardsConfig.selectedBoard.fqbn!;
} }
if (argument === "name") { if (argument === 'name') {
return boardsConfig.selectedBoard.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) { 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 details.item.locations!.debugScript;
} }
return boardsConfig.selectedBoard.fqbn!; return boardsConfig.selectedBoard.fqbn!;
} }
protected async resolveBoardTools(context?: URI, argument?: string, configurationSection?: string): Promise<Object> { protected async resolveBoardTools(context?: URI, argument?: string, configurationSection?: string): Promise<string | undefined> {
const { boardsConfig } = this.boardsServiceClient; const { boardsConfig } = this.boardsServiceClient;
if (!boardsConfig || !boardsConfig.selectedBoard) { 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) { 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 } = {}; let toolLocations: { [name: string]: ToolLocations } = {};
@ -84,17 +157,37 @@ export class ArduinoVariableResolver implements VariableContribution {
}) })
switch (argument) { switch (argument) {
case "openocd": case 'openocd': {
return toolLocations["openocd"].main; const openocd = toolLocations['openocd'];
case "openocd-scripts": if (openocd) {
return toolLocations["openocd"].scripts; return openocd.main;
case "objdump": }
return toolLocations["arm-none-eabi-gcc"].objdump; this.messageService.error('Unable to find debugging tool: openocd');
case "gdb": return undefined;
return toolLocations["arm-none-eabi-gcc"].gdb; }
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; return boardsConfig.selectedBoard.name;
} }
} }

View File

@ -1,59 +1,59 @@
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { DebugAdapterContribution, DebugAdapterExecutable, DebugAdapterSessionFactory } from '@theia/debug/lib/common/debug-model'; import { DebugAdapterContribution, DebugAdapterExecutable, DebugAdapterSessionFactory } from '@theia/debug/lib/common/debug-model';
import { DebugConfiguration } from "@theia/debug/lib/common/debug-configuration"; import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration';
import { MaybePromise } from "@theia/core/lib/common/types"; import { MaybePromise } from '@theia/core/lib/common/types';
import { IJSONSchema, IJSONSchemaSnippet } from "@theia/core/lib/common/json-schema"; import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema';
import * as path from 'path'; import * as path from 'path';
@injectable() @injectable()
export class ArduinoDebugAdapterContribution implements DebugAdapterContribution { export class ArduinoDebugAdapterContribution implements DebugAdapterContribution {
type = "arduino"; type = 'arduino';
label = "Arduino"; label = 'Arduino';
languages = ["c", "cpp", "ino"]; languages = ['c', 'cpp', 'ino'];
debugAdapterSessionFactory?: DebugAdapterSessionFactory; debugAdapterSessionFactory?: DebugAdapterSessionFactory;
getSchemaAttributes?(): MaybePromise<IJSONSchema[]> { getSchemaAttributes(): MaybePromise<IJSONSchema[]> {
return [ return [
{ {
"required": [ 'required': [
"program" 'program'
], ],
"properties": { 'properties': {
"sketch": { 'sketch': {
"type": "string", 'type': 'string',
"description": "path to the sketch root ino file", 'description': 'path to the sketch root ino file',
"default": "${file}", 'default': '${file}',
}, },
"fqbn": { 'fqbn': {
"type": "string", 'type': 'string',
"description": "Fully-qualified board name to debug on", 'description': 'Fully-qualified board name to debug on',
"default": "" 'default': ''
}, },
"runToMain": { 'runToMain': {
"description": "If enabled the debugger will run until the start of the main function.", 'description': 'If enabled the debugger will run until the start of the main function.',
"type": "boolean", 'type': 'boolean',
"default": false 'default': false
}, },
"verbose": { 'verbose': {
"type": "boolean", 'type': 'boolean',
"description": "Produce verbose log output", 'description': 'Produce verbose log output',
"default": false 'default': false
}, },
"debugDebugAdapter": { 'debugDebugAdapter': {
"type": "boolean", 'type': 'boolean',
"description": "Start the debug adapter in debug mode (with --inspect-brk)", 'description': 'Start the debug adapter in debug mode (with --inspect-brk)',
"default": false 'default': false
}, },
} }
} }
] ]
} }
getConfigurationSnippets?(): MaybePromise<IJSONSchemaSnippet[]> { getConfigurationSnippets(): MaybePromise<IJSONSchemaSnippet[]> {
return [] return []
} }
@ -65,29 +65,29 @@ export class ArduinoDebugAdapterContribution implements DebugAdapterContribution
args = args.concat([path.join(__dirname, 'debug-adapter', 'main')]); args = args.concat([path.join(__dirname, 'debug-adapter', 'main')]);
return { return {
command: "node", command: 'node',
args: args, args: args,
} }
} }
provideDebugConfigurations?(workspaceFolderUri?: string): MaybePromise<DebugConfiguration[]> { provideDebugConfigurations(workspaceFolderUri?: string): MaybePromise<DebugConfiguration[]> {
return [ return [
<DebugConfiguration>{ <DebugConfiguration>{
name: this.label, name: this.label,
type: this.type, type: this.type,
request: "launch", request: 'launch',
sketch: "${file}", sketch: '${file}',
}, },
<DebugConfiguration>{ <DebugConfiguration>{
name: this.label + " (explicit)", name: this.label + ' (explicit)',
type: this.type, type: this.type,
request: "launch", request: 'launch',
program: "${sketchBinary}", program: '${sketchBinary}',
objdump: "${boardTools:objdump}", objdump: '${boardTools:objdump}',
gdb: "${boardTools:gdb}", gdb: '${boardTools:gdb}',
gdbServer: "${boardTools:openocd}", gdbServer: '${boardTools:openocd}',
gdbServerArguments: ["-s", "${boardTools:openocd-scripts}", "--file", "${board:openocd-debug-file}"], gdbServerArguments: ['-s', '${boardTools:openocd-scripts}', '--file', '${board:openocd-debug-file}'],
runToMain: false, runToMain: false,
verbose: false, verbose: false,
@ -95,23 +95,23 @@ export class ArduinoDebugAdapterContribution implements DebugAdapterContribution
]; ];
} }
async resolveDebugConfiguration?(config: DebugConfiguration, workspaceFolderUri?: string): Promise<DebugConfiguration> { async resolveDebugConfiguration(config: DebugConfiguration, workspaceFolderUri?: string): Promise<DebugConfiguration> {
// if program is present we expect to have an explicit config here // if program is present we expect to have an explicit config here
if (!!config.program) { if (!!config.program) {
return config; return config;
} }
let sketchBinary = "${sketchBinary}" let sketchBinary = '${sketchBinary}'
if (config.sketch !== "${file}") { if (typeof config.sketch === 'string' && config.sketch.indexOf('${') < 0) {
sketchBinary = "${sketchBinary:" + config.sketch + "}"; sketchBinary = '${sketchBinary:' + config.sketch + '}';
} }
const res: ActualDebugConfig = { const res: ActualDebugConfig = {
...config, ...config,
objdump: "${boardTools:objdump}", objdump: '${boardTools:objdump}',
gdb: "${boardTools:gdb}", gdb: '${boardTools:gdb}',
gdbServer: "${boardTools:openocd}", gdbServer: '${boardTools:openocd}',
gdbServerArguments: ["-s", "${boardTools:openocd-scripts}", "--file", "${board:openocd-debug-file}"], gdbServerArguments: ['-s', '${boardTools:openocd-scripts}', '--file', '${board:openocd-debug-file}'],
program: sketchBinary program: sketchBinary
} }
return res; return res;

View File

@ -48,7 +48,14 @@ export abstract class AbstractServer extends EventEmitter {
try { try {
this.timer = setTimeout(() => this.onSpawnError(new Error('Timeout waiting for gdb server to start')), TIMEOUT); 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); const serverArguments = await this.resolveServerArguments(args);
this.process = spawn(command, serverArguments, { this.process = spawn(command, serverArguments, {
cwd: dirname(command), cwd: dirname(command),
@ -110,7 +117,7 @@ export abstract class AbstractServer extends EventEmitter {
protected onData(chunk: string | Buffer, buffer: string, event: string) { protected onData(chunk: string | Buffer, buffer: string, event: string) {
buffer += typeof chunk === 'string' ? chunk buffer += typeof chunk === 'string' ? chunk
: chunk.toString('utf8'); : chunk.toString('utf8');
const end = buffer.lastIndexOf('\n'); const end = buffer.lastIndexOf('\n');
if (end !== -1) { if (end !== -1) {

View File

@ -71,7 +71,8 @@ export class CmsisDebugSession extends GDBDebugSession {
await this.runSession(args); await this.runSession(args);
this.sendResponse(response); this.sendResponse(response);
} catch (err) { } 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); await this.runSession(args);
this.sendResponse(response); this.sendResponse(response);
} catch (err) { } 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<void> {
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<DebugProtocol.Variable[]> { private async getGlobalVariables(frameHandle: number): Promise<DebugProtocol.Variable[]> {
const frame = this.frameHandles.get(frameHandle); const frame = this.frameHandles.get(frameHandle);
const symbolInfo = this.symbolTable.getGlobalVariables(); const symbolInfo = this.symbolTable.getGlobalVariables();
@ -393,7 +403,7 @@ export class CmsisDebugSession extends GDBDebugSession {
codeOrMessage: number | DebugProtocol.Message, format?: string, codeOrMessage: number | DebugProtocol.Message, format?: string,
variables?: any, dest?: ErrorDestination): void { variables?: any, dest?: ErrorDestination): void {
if (!!format && (dest === undefined || dest === ErrorDestination.User)) { if (!!format && (dest === undefined || dest === ErrorDestination.User)) {
format = format.replace('\n', '<br>'); format = format.replace(/\n/g, '<br>');
} }
super.sendErrorResponse(response, codeOrMessage, format, variables, dest); super.sendErrorResponse(response, codeOrMessage, format, variables, dest);
} }

View File

@ -25,7 +25,7 @@
*/ */
import { spawnSync } from 'child_process'; import { spawnSync } from 'child_process';
import { platform, EOL } from 'os'; import { EOL } from 'os';
import { dirname, normalize, basename } from 'path'; import { dirname, normalize, basename } from 'path';
export enum SymbolType { export enum SymbolType {
@ -53,7 +53,6 @@ export interface SymbolInformation {
hidden: boolean; 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 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 } = { const TYPE_MAP: { [id: string]: SymbolType } = {
@ -74,7 +73,14 @@ export class SymbolTable {
private symbols: SymbolInformation[] = []; 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<void> { public async loadSymbols(): Promise<void> {
@ -128,6 +134,9 @@ export class SymbolTable {
private execute(): Promise<string> { private execute(): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!this.objdump) {
return reject(new Error('Missing parameter: objdump'));
}
try { try {
const { stdout, stderr } = spawnSync(this.objdump, [ const { stdout, stderr } = spawnSync(this.objdump, [
'--syms', '--syms',