Automated debug config setup

This commit is contained in:
Christian Weichel 2019-11-19 18:24:30 +01:00 committed by Miro Spönemann
parent ea5f528ef0
commit 8aa356bd6e
9 changed files with 325 additions and 66 deletions

View File

@ -9,7 +9,7 @@
"dependencies": {
"@theia/core": "next",
"@theia/debug": "next",
"arduino-ide-extension": "0.0.2",
"cdt-gdb-adapter": "^0.0.14-next.4783033.0",
"vscode-debugadapter": "^1.26.0",
"vscode-debugprotocol": "^1.26.0"
@ -34,7 +34,8 @@
],
"theiaExtensions": [
{
"backend": "lib/node/backend-module"
"backend": "lib/node/backend-module",
"frontend": "lib/browser/frontend-module"
}
]
}

View File

@ -0,0 +1,100 @@
import { VariableContribution, VariableRegistry, Variable } from '@theia/variable-resolver/lib/browser';
import { injectable, inject } from 'inversify';
import URI from '@theia/core/lib/common/uri';
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 {
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(WorkspaceVariableContribution)
protected readonly workspaceVars: WorkspaceVariableContribution;
registerVariables(variables: VariableRegistry): void {
variables.registerVariable(<Variable>{
name: `boardTools`,
description: "Provides paths and access to board specific tooling",
resolve: this.resolveBoardTools.bind(this),
});
variables.registerVariable(<Variable>{
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",
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<Object> {
let sketchPath = argument || this.workspaceVars.getResourceUri()!.path.toString();
return sketchPath.substring(0, sketchPath.length - 3) + "arduino.samd.arduino_zero_edbg.elf";
}
protected async resolveBoard(context?: URI, argument?: string, configurationSection?: string): Promise<Object> {
const { boardsConfig } = this.boardsServiceClient;
if (!boardsConfig || !boardsConfig.selectedBoard) {
throw new Error('No boards selected. Please select a board.');
}
if (!argument || argument === "fqbn") {
return boardsConfig.selectedBoard.fqbn!;
}
if (argument === "name") {
return boardsConfig.selectedBoard.name;
}
const details = await this.boardsService.detail({id: boardsConfig.selectedBoard.fqbn!});
if (!details.item) {
throw new Error("Cannot get board details");
}
if (argument === "openocd-debug-file") {
return details.item.locations!.debugScript;
}
return boardsConfig.selectedBoard.fqbn!;
}
protected async resolveBoardTools(context?: URI, argument?: string, configurationSection?: string): Promise<Object> {
const { boardsConfig } = this.boardsServiceClient;
if (!boardsConfig || !boardsConfig.selectedBoard) {
throw new Error('No boards selected. Please select a board.');
}
const details = await this.boardsService.detail({id: boardsConfig.selectedBoard.fqbn!});
if (!details.item) {
throw new Error("Cannot get board details")
}
let toolLocations: { [name: string]: ToolLocations } = {};
details.item.requiredTools.forEach(t => {
toolLocations[t.name] = t.locations!;
})
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;
}
return boardsConfig.selectedBoard.name;
}
}

View File

@ -0,0 +1,8 @@
import { ContainerModule } from 'inversify';
import { VariableContribution } from '@theia/variable-resolver/lib/browser';
import { ArduinoVariableResolver } from './arduino-variable-resolver';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ArduinoVariableResolver).toSelf().inSingletonScope();
bind(VariableContribution).toService(ArduinoVariableResolver);
});

View File

@ -1,12 +1,24 @@
import { injectable } from 'inversify';
import { injectable, inject } 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 * as path from 'path';
import { BoardsService } from 'arduino-ide-extension/lib/common/protocol/boards-service';
import { CoreService } from 'arduino-ide-extension/lib/common/protocol/core-service';
import { FileSystem } from '@theia/filesystem/lib/common';
@injectable()
export class ArduinoDebugAdapterContribution implements DebugAdapterContribution {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
type = "arduino";
label = "Arduino";
@ -22,56 +34,22 @@ export class ArduinoDebugAdapterContribution implements DebugAdapterContribution
"program"
],
"properties": {
"program": {
"type": "string",
"description": "Path to the program to be launched",
"default": "${workspaceFolder}/${command:askProgramPath}"
},
"sketch": {
"type": "string",
"description": "Path to the sketch folder",
"default": "${workspaceFolder}"
},
"fbqn": {
"type": "string",
"description": "Fully qualified board name of the debugging target",
"default": "unknown"
"description": "path to the sketch root ino file",
"default": "${file}",
},
"runToMain": {
"description": "If enabled the debugger will run until the start of the main function.",
"type": "boolean",
"default": false
},
"gdb": {
"fqbn": {
"type": "string",
"description": "Path to gdb",
"default": "arm-none-eabi-gdb"
},
"gdbArguments": {
"description": "Additional arguments to pass to GDB command line",
"type": "array",
"default": []
},
"gdbServer": {
"type": "string",
"description": "Path to gdb server",
"default": "pyocd"
},
"gdbServerArguments": {
"description": "Additional arguments to pass to GDB server",
"type": "array",
"default": []
},
"objdump": {
"type": "string",
"description": "Path to objdump executable",
"default": "arm-none-eabi-objdump"
},
"initCommands": {
"description": "Extra gdb commands to run after initialisation",
"type": "array",
"default": []
"description": "Fully-qualified board name to debug on",
"default": ""
},
"verbose": {
"type": "boolean",
"description": "Produce verbose log output",
@ -105,11 +83,71 @@ export class ArduinoDebugAdapterContribution implements DebugAdapterContribution
}
provideDebugConfigurations?(workspaceFolderUri?: string): MaybePromise<DebugConfiguration[]> {
return [];
return [
<DebugConfiguration>{
name: this.label,
type: this.type,
request: "launch",
sketch: "${file}",
verbose: true,
runToMain: true,
},
<DebugConfiguration>{
name: this.label + " (explicit)",
type: this.type,
request: "launch",
program: "${sketchBinary}",
objdump: "${boardTools:objdump}",
gdb: "${boardTools:gdb}",
gdbServer: "${boardTools:openocd}",
gdbServerArguments: ["-s", "${boardTools:openocd-scripts}", "--file", "${board:openocd-debug-file}"],
verbose: true,
runToMain: true,
}
];
}
resolveDebugConfiguration?(config: DebugConfiguration, workspaceFolderUri?: string): MaybePromise<DebugConfiguration> {
return config;
async resolveDebugConfiguration?(config: DebugConfiguration, workspaceFolderUri?: string): Promise<DebugConfiguration> {
// 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 + "}";
}
const res: ActualDebugConfig = {
...config,
objdump: "${boardTools:objdump}",
gdb: "${boardTools:gdb}",
gdbServer: "${boardTools:openocd}",
gdbServerArguments: ["-s", "${boardTools:openocd-scripts}", "--file", "${board:openocd-debug-file}"],
program: sketchBinary
}
return res;
}
}
interface ActualDebugConfig extends DebugConfiguration {
// path to the program to be launched
program: string
// path to gdb
gdb: string
// additional arguments to pass to GDB command line
gdbArguments?: string[]
// path to the gdb server
gdbServer: string
// additional arguments to pass to GDB server
gdbServerArguments: string[]
// path to objdump executable
objdump: string
// extra gdb commands to run after initialisation
initCommands?: string[]
}

View File

@ -26,7 +26,7 @@
import { normalize } from 'path';
import { DebugProtocol } from 'vscode-debugprotocol';
import { Logger, logger, InitializedEvent, OutputEvent, Scope, TerminatedEvent } from 'vscode-debugadapter';
import { GDBDebugSession, RequestArguments, FrameVariableReference, FrameReference, ObjectVariableReference } from 'cdt-gdb-adapter/dist/GDBDebugSession';
import { GDBDebugSession, RequestArguments, FrameVariableReference, FrameReference } from 'cdt-gdb-adapter/dist/GDBDebugSession';
import { GDBBackend } from 'cdt-gdb-adapter/dist/GDBBackend';
import { CmsisBackend } from './cmsis-backend';
// import { PyocdServer } from './pyocd-server';
@ -123,15 +123,15 @@ export class CmsisDebugSession extends GDBDebugSession {
type: 'frame',
frameHandle: args.frameId,
};
const pins: ObjectVariableReference = {
type: "object",
varobjName: "__pins",
frameHandle: args.frameId,
}
// const pins: ObjectVariableReference = {
// type: "object",
// varobjName: "__pins",
// frameHandle: 42000,
// }
response.body = {
scopes: [
new Scope('Pins', this.variableHandles.create(pins), false),
// new Scope('Pins', this.variableHandles.create(pins), false),
new Scope('Local', this.variableHandles.create(frame), false),
new Scope('Global', GLOBAL_HANDLE_ID, false),
new Scope('Static', STATIC_HANDLES_START + parseInt(args.frameId as any, 10), false)
@ -162,6 +162,8 @@ export class CmsisDebugSession extends GDBDebugSession {
} else if (ref && ref.type === 'frame') {
// List variables for current frame
response.body.variables = await this.handleVariableRequestFrame(ref);
} else if (ref && ref.varobjName === '__pins') {
response.body.variables = await this.handlePinStatusRequest();
} else if (ref && ref.type === 'object') {
// List data under any variable
response.body.variables = await this.handleVariableRequestObject(ref);
@ -300,6 +302,17 @@ export class CmsisDebugSession extends GDBDebugSession {
return variables;
}
private async handlePinStatusRequest(): Promise<DebugProtocol.Variable[]> {
const variables: DebugProtocol.Variable[] = [];
variables.push({
name: "D2",
type: "gpio",
value: "0x00",
variablesReference: 0
})
return variables;
}
private async getStaticVariables(frameHandle: number): Promise<DebugProtocol.Variable[]> {
const frame = this.frameHandles.get(frameHandle);
const result = await mi.sendStackInfoFrame(this.gdb, frame.threadId, frame.frameId);

View File

@ -3,6 +3,7 @@ import { PortScanner } from './port-scanner';
import { CmsisRequestArguments } from './cmsis-debug-session';
import * as fs from 'fs-extra';
import * as path from 'path';
import * as os from 'os';
const LAUNCH_REGEX = /GDB server started/;
const ERROR_REGEX = /:ERROR:gdbserver:/;
@ -20,7 +21,7 @@ export class OpenocdServer extends AbstractServer {
}
sessionConfigFile += `echo "GDB server started"${"\n"}`
const tmpdir = await fs.mkdtemp("arduino-debugger");
const tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "arduino-debugger"));
const sessionCfgPath = path.join(tmpdir, "gdb.cfg");
await fs.writeFile(sessionCfgPath, sessionConfigFile);

View File

@ -2,6 +2,7 @@ import { isWindows, isOSX } from '@theia/core/lib/common/os';
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
import { Searchable } from './searchable';
import { Installable } from './installable';
import { Detailable } from './detailable';
import { ArduinoComponent } from './arduino-component';
const naturalCompare: (left: string, right: string) => number = require('string-natural-compare').caseInsensitive;
@ -59,7 +60,7 @@ export interface BoardsServiceClient {
export const BoardsServicePath = '/services/boards-service';
export const BoardsService = Symbol('BoardsService');
export interface BoardsService extends Installable<BoardPackage>, Searchable<BoardPackage>, JsonRpcServer<BoardsServiceClient> {
export interface BoardsService extends Installable<BoardPackage>, Searchable<BoardPackage>, Detailable<BoardDetails>, JsonRpcServer<BoardsServiceClient> {
getAttachedBoards(): Promise<{ boards: Board[] }>;
getAvailablePorts(): Promise<{ ports: Port[] }>;
}
@ -181,6 +182,28 @@ export interface Board {
fqbn?: string
}
export interface BoardDetails extends Board {
fqbn: string;
requiredTools: Tool[];
locations?: BoardDetailLocations;
}
export interface BoardDetailLocations {
debugScript: string;
}
export interface Tool {
readonly packager: string;
readonly name: string;
readonly version: string;
readonly locations?: ToolLocations;
}
export interface ToolLocations {
main: string
[key: string]: string
}
export namespace Board {
export function is(board: any): board is Board {

View File

@ -0,0 +1,10 @@
export interface Detailable<T> {
detail(options: Detailable.Options): Promise<{ item?: T }>;
}
export namespace Detailable {
export interface Options {
readonly id: string;
}
}

View File

@ -2,22 +2,21 @@ import * as PQueue from 'p-queue';
import { injectable, inject, postConstruct, named } from 'inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { BoardsService, AttachedSerialBoard, BoardPackage, Board, AttachedNetworkBoard, BoardsServiceClient, Port } from '../common/protocol/boards-service';
import {
PlatformSearchReq,
PlatformSearchResp,
PlatformInstallReq,
PlatformInstallResp,
PlatformListReq,
PlatformListResp,
Platform,
PlatformUninstallReq,
PlatformUninstallResp
BoardsService, AttachedSerialBoard, BoardPackage, Board, AttachedNetworkBoard, BoardsServiceClient,
Port, BoardDetails, Tool, ToolLocations, BoardDetailLocations
} from '../common/protocol/boards-service';
import {
PlatformSearchReq, PlatformSearchResp, PlatformInstallReq, PlatformInstallResp, PlatformListReq,
PlatformListResp, Platform, PlatformUninstallResp, PlatformUninstallReq
} from './cli-protocol/commands/core_pb';
import { CoreClientProvider } from './core-client-provider';
import { BoardListReq, BoardListResp } from './cli-protocol/commands/board_pb';
import { BoardListReq, BoardListResp, BoardDetailsReq, BoardDetailsResp, RequiredTool } from './cli-protocol/commands/board_pb';
import { ToolOutputServiceServer } from '../common/protocol/tool-output-service';
import { Installable } from '../common/protocol/installable';
import { ConfigService } from '../common/protocol/config-service';
import * as path from 'path';
import URI from '@theia/core/lib/common/uri';
@injectable()
export class BoardsServiceImpl implements BoardsService {
@ -35,6 +34,9 @@ export class BoardsServiceImpl implements BoardsService {
@inject(ToolOutputServiceServer)
protected readonly toolOutputService: ToolOutputServiceServer;
@inject(ConfigService)
protected readonly configService: ConfigService;
protected discoveryInitialized = false;
protected discoveryTimer: NodeJS.Timer | undefined;
/**
@ -215,6 +217,69 @@ export class BoardsServiceImpl implements BoardsService {
});
}
async detail(options: { id: string }): Promise<{ item?: BoardDetails }> {
const coreClient = await this.coreClientProvider.getClient();
if (!coreClient) {
return {};
}
const { client, instance } = coreClient;
const req = new BoardDetailsReq();
req.setInstance(instance);
req.setFqbn(options.id);
const resp = await new Promise<BoardDetailsResp>((resolve, reject) => client.boardDetails(req, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp)));
const tools = await Promise.all(resp.getRequiredToolsList().map(async t => <Tool>{
name: t.getName(),
packager: t.getPackager(),
version: t.getVersion(),
locations: await this.getToolLocations(t),
}));
return {
item: {
name: resp.getName(),
fqbn: options.id,
requiredTools: tools,
locations: await this.getBoardLocations(resp)
}
};
}
// TODO: these location should come from the CLI/daemon rather than us botching them together
protected async getBoardLocations(details: BoardDetailsResp): Promise<BoardDetailLocations | undefined> {
const config = await this.configService.getConfiguration();
const datadir = new URI(config.dataDirUri).path.toString();
return {
debugScript: path.join(datadir, "packages", "arduino", "hardware", "samd", "1.8.4", "variants", "arduino_zero", "openocd_scripts", "arduino_zero.cfg")
}
}
// TODO: these location should come from the CLI/daemon rather than us botching them together
protected async getToolLocations(t: RequiredTool) {
const config = await this.configService.getConfiguration();
const datadir = new URI(config.dataDirUri).path.toString();
const toolBasePath = path.join(datadir, "packages", "arduino", "tools");
let loc: ToolLocations = {
main: path.join(toolBasePath, t.getName(), t.getVersion())
};
switch (t.getName()) {
case "openocd":
loc.scripts = path.join(loc.main, "share", "openocd", "scripts");
loc.main = path.join(loc.main, "bin", "openocd");
break
case "arm-none-eabi-gcc":
["gdb", "objdump"].forEach(s => loc[s] = path.join(loc.main, "bin", `arm-none-eabi-${s}`));
}
return loc;
}
async search(options: { query?: string }): Promise<{ items: BoardPackage[] }> {
const coreClient = await this.coreClientProvider.getClient();
if (!coreClient) {