From 8aa356bd6e5372688dab74ebf1688ee40d66287a Mon Sep 17 00:00:00 2001 From: Christian Weichel Date: Tue, 19 Nov 2019 18:24:30 +0100 Subject: [PATCH] Automated debug config setup --- arduino-debugger-extension/package.json | 5 +- .../src/browser/arduino-variable-resolver.ts | 100 ++++++++++++++ .../src/browser/frontend-module.ts | 8 ++ .../arduino-debug-adapter-contribution.ts | 126 ++++++++++++------ .../node/debug-adapter/cmsis-debug-session.ts | 27 +++- .../src/node/debug-adapter/openocd-server.ts | 3 +- .../src/common/protocol/boards-service.ts | 25 +++- .../src/common/protocol/detailable.ts | 10 ++ .../src/node/boards-service-impl.ts | 87 ++++++++++-- 9 files changed, 325 insertions(+), 66 deletions(-) create mode 100644 arduino-debugger-extension/src/browser/arduino-variable-resolver.ts create mode 100644 arduino-debugger-extension/src/browser/frontend-module.ts create mode 100644 arduino-ide-extension/src/common/protocol/detailable.ts diff --git a/arduino-debugger-extension/package.json b/arduino-debugger-extension/package.json index 0eafe66c..f986bee0 100644 --- a/arduino-debugger-extension/package.json +++ b/arduino-debugger-extension/package.json @@ -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" } ] } \ No newline at end of file diff --git a/arduino-debugger-extension/src/browser/arduino-variable-resolver.ts b/arduino-debugger-extension/src/browser/arduino-variable-resolver.ts new file mode 100644 index 00000000..8dbd05f7 --- /dev/null +++ b/arduino-debugger-extension/src/browser/arduino-variable-resolver.ts @@ -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({ + 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", + 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 { + 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 { + 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 { + 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; + } + +} \ No newline at end of file diff --git a/arduino-debugger-extension/src/browser/frontend-module.ts b/arduino-debugger-extension/src/browser/frontend-module.ts new file mode 100644 index 00000000..fdd71fe9 --- /dev/null +++ b/arduino-debugger-extension/src/browser/frontend-module.ts @@ -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); +}); 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 17b1a3ab..b52d61b4 100644 --- a/arduino-debugger-extension/src/node/arduino-debug-adapter-contribution.ts +++ b/arduino-debugger-extension/src/node/arduino-debug-adapter-contribution.ts @@ -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 { - return []; + return [ + { + name: this.label, + type: this.type, + request: "launch", + + sketch: "${file}", + + verbose: true, + runToMain: true, + }, + { + 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 { - return config; + 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 + "}"; + } + 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[] } \ No newline at end of file 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 8c746d91..ceadb801 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 @@ -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 { + const variables: DebugProtocol.Variable[] = []; + variables.push({ + name: "D2", + type: "gpio", + value: "0x00", + variablesReference: 0 + }) + return variables; + } + private async getStaticVariables(frameHandle: number): Promise { const frame = this.frameHandles.get(frameHandle); const result = await mi.sendStackInfoFrame(this.gdb, frame.threadId, frame.frameId); diff --git a/arduino-debugger-extension/src/node/debug-adapter/openocd-server.ts b/arduino-debugger-extension/src/node/debug-adapter/openocd-server.ts index 8a7b1f99..079ddcb0 100644 --- a/arduino-debugger-extension/src/node/debug-adapter/openocd-server.ts +++ b/arduino-debugger-extension/src/node/debug-adapter/openocd-server.ts @@ -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); diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index 2a786567..cb25650e 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -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, Searchable, JsonRpcServer { +export interface BoardsService extends Installable, Searchable, Detailable, JsonRpcServer { 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 { diff --git a/arduino-ide-extension/src/common/protocol/detailable.ts b/arduino-ide-extension/src/common/protocol/detailable.ts new file mode 100644 index 00000000..456dd626 --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/detailable.ts @@ -0,0 +1,10 @@ + +export interface Detailable { + detail(options: Detailable.Options): Promise<{ item?: T }>; +} + +export namespace Detailable { + export interface Options { + readonly id: string; + } +} \ No newline at end of file diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index a79af50f..8dd110a8 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -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((resolve, reject) => client.boardDetails(req, (err, resp) => (!!err ? reject : resolve)(!!err ? err : resp))); + + + + const tools = await Promise.all(resp.getRequiredToolsList().map(async t => { + 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 { + 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) {