From 3d6d2ce814567c835b7e749896180dc6104c54c1 Mon Sep 17 00:00:00 2001 From: Christian Weichel Date: Fri, 15 Nov 2019 13:55:26 +0100 Subject: [PATCH] Ran first debugging session --- .vscode/arduino.json | 4 + .vscode/c_cpp_properties.json | 14 + .vscode/launch.json | 14 +- arduino-debugger-extension/package.json | 40 ++ .../src/common/index.ts | 0 .../src/common/openocd/gdb.cfg | 2 + .../src/common/openocd/openocd.cfg | 12 + .../arduino-debug-adapter-contribution.ts | 109 +++++ .../src/node/backend-module.ts | 7 + .../src/node/debug-adapter/abstract-server.ts | 151 +++++++ .../src/node/debug-adapter/cmsis-backend.ts | 39 ++ .../node/debug-adapter/cmsis-debug-session.ts | 388 ++++++++++++++++++ .../src/node/debug-adapter/index.ts | 34 ++ .../src/node/debug-adapter/mi.ts | 80 ++++ .../src/node/debug-adapter/openocd-server.ts | 51 +++ .../src/node/debug-adapter/port-scanner.ts | 192 +++++++++ .../src/node/debug-adapter/pyocd-server.ts | 75 ++++ .../src/node/debug-adapter/symbols.ts | 151 +++++++ arduino-debugger-extension/tsconfig.json | 31 ++ arduino-debugger-extension/tslint.json | 37 ++ browser-app/package.json | 5 +- package.json | 1 + 22 files changed, 1428 insertions(+), 9 deletions(-) create mode 100644 .vscode/arduino.json create mode 100644 .vscode/c_cpp_properties.json create mode 100644 arduino-debugger-extension/package.json create mode 100644 arduino-debugger-extension/src/common/index.ts create mode 100644 arduino-debugger-extension/src/common/openocd/gdb.cfg create mode 100644 arduino-debugger-extension/src/common/openocd/openocd.cfg create mode 100644 arduino-debugger-extension/src/node/arduino-debug-adapter-contribution.ts create mode 100644 arduino-debugger-extension/src/node/backend-module.ts create mode 100644 arduino-debugger-extension/src/node/debug-adapter/abstract-server.ts create mode 100644 arduino-debugger-extension/src/node/debug-adapter/cmsis-backend.ts create mode 100644 arduino-debugger-extension/src/node/debug-adapter/cmsis-debug-session.ts create mode 100644 arduino-debugger-extension/src/node/debug-adapter/index.ts create mode 100644 arduino-debugger-extension/src/node/debug-adapter/mi.ts create mode 100644 arduino-debugger-extension/src/node/debug-adapter/openocd-server.ts create mode 100644 arduino-debugger-extension/src/node/debug-adapter/port-scanner.ts create mode 100644 arduino-debugger-extension/src/node/debug-adapter/pyocd-server.ts create mode 100644 arduino-debugger-extension/src/node/debug-adapter/symbols.ts create mode 100644 arduino-debugger-extension/tsconfig.json create mode 100644 arduino-debugger-extension/tslint.json diff --git a/.vscode/arduino.json b/.vscode/arduino.json new file mode 100644 index 00000000..513efb45 --- /dev/null +++ b/.vscode/arduino.json @@ -0,0 +1,4 @@ +{ + "board": "arduino:samd:arduino_zero_edbg", + "port": "/dev/cu.usbmodem141402" +} \ No newline at end of file diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 00000000..3bf59290 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,14 @@ +{ + "configurations": [ + { + "name": "Mac", + "includePath": [ + "/Users/csweichel/Library/Arduino15/packages/arduino/tools/**", + "/Users/csweichel/Library/Arduino15/packages/arduino/hardware/samd/1.8.4/**" + ], + "forcedInclude": [ + "/Users/csweichel/Library/Arduino15/packages/arduino/hardware/samd/1.8.4/cores/arduino/Arduino.h" + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 2cf29886..28b2c89a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,6 +10,13 @@ "name": "Attach by Process ID", "processId": "${command:PickProcess}" }, + { + "type": "node", + "request": "launch", + "name": "Launch Electron Packager", + "program": "${workspaceRoot}/electron/packager/index.js", + "cwd": "${workspaceFolder}/electron/packager" + }, { "type": "node", "request": "launch", @@ -99,13 +106,6 @@ "smartStep": true, "internalConsoleOptions": "openOnSessionStart", "outputCapture": "std" - }, - { - "type": "node", - "request": "launch", - "name": "Packager", - "program": "${workspaceRoot}/electron/packager/index.js", - "cwd": "${workspaceFolder}/electron/packager" } ] } diff --git a/arduino-debugger-extension/package.json b/arduino-debugger-extension/package.json new file mode 100644 index 00000000..0eafe66c --- /dev/null +++ b/arduino-debugger-extension/package.json @@ -0,0 +1,40 @@ +{ + "name": "arduino-debugger-extension", + "version": "0.0.2", + "description": "An extension for debugging Arduino programs", + "license": "MIT", + "engines": { + "node": ">=10.10.0" + }, + "dependencies": { + "@theia/core": "next", + "@theia/debug": "next", + + "cdt-gdb-adapter": "^0.0.14-next.4783033.0", + "vscode-debugadapter": "^1.26.0", + "vscode-debugprotocol": "^1.26.0" + }, + "scripts": { + "prepare": "yarn run clean && yarn run build", + "clean": "rimraf lib", + "lint": "tslint -c ./tslint.json --project ./tsconfig.json", + "build": "tsc && yarn lint", + "watch": "tsc -w" + }, + "devDependencies": { + "rimraf": "^2.6.1", + "tslint": "^5.5.0", + "typescript": "3.5.1" + }, + "files": [ + "lib", + "src", + "build", + "data" + ], + "theiaExtensions": [ + { + "backend": "lib/node/backend-module" + } + ] +} \ No newline at end of file diff --git a/arduino-debugger-extension/src/common/index.ts b/arduino-debugger-extension/src/common/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/arduino-debugger-extension/src/common/openocd/gdb.cfg b/arduino-debugger-extension/src/common/openocd/gdb.cfg new file mode 100644 index 00000000..cdb6d76a --- /dev/null +++ b/arduino-debugger-extension/src/common/openocd/gdb.cfg @@ -0,0 +1,2 @@ +gdb_port 50000 +telnet_port 44444 \ No newline at end of file diff --git a/arduino-debugger-extension/src/common/openocd/openocd.cfg b/arduino-debugger-extension/src/common/openocd/openocd.cfg new file mode 100644 index 00000000..10d1382b --- /dev/null +++ b/arduino-debugger-extension/src/common/openocd/openocd.cfg @@ -0,0 +1,12 @@ +source [find interface/cmsis-dap.cfg] + +# chip name +set CHIPNAME at91samd21g18 +set ENDIAN little + +# choose a port here +set telnet_port 0 + +source [find target/at91samdXX.cfg] + +echo "GDB server started" \ 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 new file mode 100644 index 00000000..7a6ad517 --- /dev/null +++ b/arduino-debugger-extension/src/node/arduino-debug-adapter-contribution.ts @@ -0,0 +1,109 @@ +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 * as path from 'path'; + +@injectable() +export class ArduinoDebugAdapterContribution implements DebugAdapterContribution { + type = "arduino"; + + label = "Arduino"; + + languages = ["c", "cpp", "ino"]; + + debugAdapterSessionFactory?: DebugAdapterSessionFactory; + + getSchemaAttributes?(): MaybePromise { + return [ + { + "required": [ + "program" + ], + "properties": { + "program": { + "type": "string", + "description": "Path to the program to be launched", + "default": "${workspaceFolder}/${command:askProgramPath}" + }, + "arguments": { + "type": "string", + "description": "Arguments for the program" + }, + "runToMain": { + "description": "If enabled the debugger will run until the start of the main function.", + "type": "boolean", + "default": false + }, + "gdb": { + "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": [] + }, + "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" + } + } + } + ] + } + + getConfigurationSnippets?(): MaybePromise { + return [] + } + + provideDebugAdapterExecutable?(config: DebugConfiguration): MaybePromise { + let args: string[] = []; + if (!!config.debugDebugAdapter) { + args.push('--inspect-brk') + } + args = args.concat([path.join(__dirname, 'debug-adapter', 'index.js')]); + + return { + command: "node", + args: args, + } + } + + provideDebugConfigurations?(workspaceFolderUri?: string): MaybePromise { + return []; + } + + resolveDebugConfiguration?(config: DebugConfiguration, workspaceFolderUri?: string): MaybePromise { + return config; + } + +} \ No newline at end of file diff --git a/arduino-debugger-extension/src/node/backend-module.ts b/arduino-debugger-extension/src/node/backend-module.ts new file mode 100644 index 00000000..f9e72972 --- /dev/null +++ b/arduino-debugger-extension/src/node/backend-module.ts @@ -0,0 +1,7 @@ +import { ContainerModule } from 'inversify'; +import { DebugAdapterContribution } from '@theia/debug/lib/common/debug-model'; +import { ArduinoDebugAdapterContribution } from './arduino-debug-adapter-contribution'; + +export default new ContainerModule((bind, unbind, isBound, rebind) => { + bind(DebugAdapterContribution).to(ArduinoDebugAdapterContribution).inSingletonScope(); +}); diff --git a/arduino-debugger-extension/src/node/debug-adapter/abstract-server.ts b/arduino-debugger-extension/src/node/debug-adapter/abstract-server.ts new file mode 100644 index 00000000..0d8c369c --- /dev/null +++ b/arduino-debugger-extension/src/node/debug-adapter/abstract-server.ts @@ -0,0 +1,151 @@ +/* +* CMSIS Debug Adapter +* Copyright (c) 2017-2019 Marcel Ball +* Copyright (c) 2019 Arm Limited +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +import { EOL } from 'os'; +import { spawn, ChildProcess } from 'child_process'; +import { EventEmitter } from 'events'; +import { dirname } from 'path'; +import { CmsisRequestArguments } from './cmsis-debug-session'; + +const TIMEOUT = 1000 * 10; // 10 seconds + +export abstract class AbstractServer extends EventEmitter { + + protected process?: ChildProcess; + protected outBuffer: string = ''; + protected errBuffer: string = ''; + protected launchResolve?: () => void; + protected launchReject?: (error: any) => void; + protected timer?: NodeJS.Timer; + + public spawn(args: CmsisRequestArguments): Promise { + return new Promise(async (resolve, reject) => { + this.launchResolve = resolve; + this.launchReject = reject; + + try { + this.timer = setTimeout(() => this.onSpawnError(new Error('Timeout waiting for gdb server to start')), TIMEOUT); + + const command = args.gdbServer || 'gdb-server'; + const serverArguments = await this.resolveServerArguments(args.gdbServerArguments); + this.process = spawn(command, serverArguments, { + cwd: dirname(command), + }); + + if (!this.process) { + throw new Error('Unable to spawn gdb server'); + } + + this.process.on('exit', this.onExit.bind(this)); + this.process.on('error', this.onSpawnError.bind(this)); + + if (this.process.stdout) { + this.process.stdout.on('data', this.onStdout.bind(this)); + } + if (this.process.stderr) { + this.process.stderr.on('data', this.onStderr.bind(this)); + } + } catch (error) { + this.onSpawnError(error); + } + }); + } + + public kill() { + if (this.process) { + this.process.kill('SIGINT'); + } + } + + protected async resolveServerArguments(serverArguments?: string[]): Promise { + return serverArguments || []; + } + + protected onExit(code: number, signal: string) { + this.emit('exit', code, signal); + + // Code can be undefined, null or 0 and we want to ignore those values + if (!!code) { + this.emit('error', `GDB server stopped unexpectedly with exit code ${code}`); + } + } + + protected onSpawnError(error: Error) { + if (this.launchReject) { + this.clearTimer(); + this.launchReject(error); + this.clearPromises(); + } + } + + protected onStdout(chunk: string | Buffer) { + this.onData(chunk, this.outBuffer, 'stdout'); + } + + protected onStderr(chunk: string | Buffer) { + this.onData(chunk, this.errBuffer, 'stderr'); + } + + protected onData(chunk: string | Buffer, buffer: string, event: string) { + buffer += typeof chunk === 'string' ? chunk + : chunk.toString('utf8'); + + const end = buffer.lastIndexOf('\n'); + if (end !== -1) { + const data = buffer.substring(0, end); + this.emit(event, data); + this.handleData(data); + buffer = buffer.substring(end + 1); + } + } + + protected handleData(data: string) { + if (this.launchResolve && this.serverStarted(data)) { + this.clearTimer(); + this.launchResolve(); + this.clearPromises(); + } + + if (this.serverError(data)) { + this.emit('error', data.split(EOL)[0]); + } + } + + protected clearTimer() { + if (this.timer) { + clearTimeout(this.timer); + this.timer = undefined; + } + } + + protected clearPromises() { + this.launchResolve = undefined; + this.launchReject = undefined; + } + + protected abstract serverStarted(data: string): boolean; + protected abstract serverError(data: string): boolean; +} diff --git a/arduino-debugger-extension/src/node/debug-adapter/cmsis-backend.ts b/arduino-debugger-extension/src/node/debug-adapter/cmsis-backend.ts new file mode 100644 index 00000000..fd96566e --- /dev/null +++ b/arduino-debugger-extension/src/node/debug-adapter/cmsis-backend.ts @@ -0,0 +1,39 @@ +/* +* CMSIS Debug Adapter +* Copyright (c) 2019 Arm Limited +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +import { GDBBackend } from 'cdt-gdb-adapter/dist/GDBBackend'; +import * as mi from './mi'; + +export class CmsisBackend extends GDBBackend { + + public get isRunning(): boolean { + return !!this.out; + } + + public pause() { + mi.sendExecInterrupt(this); + return true; + } +} 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 new file mode 100644 index 00000000..c98879f5 --- /dev/null +++ b/arduino-debugger-extension/src/node/debug-adapter/cmsis-debug-session.ts @@ -0,0 +1,388 @@ +/* +* CMSIS Debug Adapter +* Copyright (c) 2019 Arm Limited +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +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 { GDBBackend } from 'cdt-gdb-adapter/dist/GDBBackend'; +import { CmsisBackend } from './cmsis-backend'; +import { PyocdServer } from './pyocd-server'; +import { PortScanner } from './port-scanner'; +import { SymbolTable } from './symbols'; +import * as varMgr from 'cdt-gdb-adapter/dist/varManager'; +import * as mi from './mi'; + +export interface CmsisRequestArguments extends RequestArguments { + runToMain?: boolean; + gdbServer?: string; + gdbServerArguments?: string[]; + objdump?: string; +} + +const GLOBAL_HANDLE_ID = 0xFE; +const STATIC_HANDLES_START = 0x010000; +const STATIC_HANDLES_FINISH = 0x01FFFF; + +export class CmsisDebugSession extends GDBDebugSession { + + protected gdbServer = new PyocdServer(); + protected portScanner = new PortScanner(); + protected symbolTable!: SymbolTable; + protected globalHandle!: number; + + constructor() { + super(); + } + + protected createBackend(): GDBBackend { + return new CmsisBackend(); + } + + protected async launchRequest(response: DebugProtocol.LaunchResponse, args: CmsisRequestArguments): Promise { + try { + await this.runSession(args); + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse(response, 1, err.message); + } + } + + protected async attachRequest(response: DebugProtocol.AttachResponse, args: CmsisRequestArguments): Promise { + try { + await this.runSession(args); + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse(response, 1, err.message); + } + } + + protected async configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse): Promise { + try { + await mi.sendExecContinue(this.gdb); + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse(response, 100, err.message); + } + } + + protected async pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments): Promise { + try { + await mi.sendExecInterrupt(this.gdb, args.threadId); + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse(response, 1, err.message); + } + } + + protected async stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): Promise { + try { + this.globalHandle = this.frameHandles.create({ + threadId: -1, + frameId: -1 + }); + + return super.stackTraceRequest(response, args); + } catch (err) { + this.sendErrorResponse(response, 1, err.message); + } + } + + protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { + try { + const frame: FrameVariableReference = { + type: 'frame', + frameHandle: args.frameId, + }; + const pins: ObjectVariableReference = { + type: "object", + varobjName: "__pins", + frameHandle: args.frameId, + } + + response.body = { + scopes: [ + 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) + ], + }; + + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse(response, 1, err.message); + } + } + + protected async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): Promise { + try { + response.body = { + variables: new Array() + }; + + const ref = this.variableHandles.get(args.variablesReference); + + if (args.variablesReference === GLOBAL_HANDLE_ID) { + // Use hardcoded global handle to load and store global variables + response.body.variables = await this.getGlobalVariables(this.globalHandle); + } else if (args.variablesReference >= STATIC_HANDLES_START && args.variablesReference <= STATIC_HANDLES_FINISH) { + // Use STATIC_HANDLES_START to shift the framehandles back + const frameHandle = args.variablesReference - STATIC_HANDLES_START; + response.body.variables = await this.getStaticVariables(frameHandle); + } else if (ref && ref.type === 'frame') { + // List variables for current frame + response.body.variables = await this.handleVariableRequestFrame(ref); + } else if (ref && ref.type === 'object') { + // List data under any variable + response.body.variables = await this.handleVariableRequestObject(ref); + } + + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse(response, 1, err.message); + } + } + + protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): Promise { + try { + if (args.context === 'repl') { + const command = args.expression; + const output = await mi.sendUserInput(this.gdb, command); + if (typeof output === 'undefined') { + response.body = { + result: '', + variablesReference: 0 + }; + } else { + response.body = { + result: JSON.stringify(output), + variablesReference: 0 + }; + } + + this.sendResponse(response); + } else { + return super.evaluateRequest(response, args); + } + } catch (err) { + this.sendErrorResponse(response, 1, err.message); + } + } + + protected async disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): Promise { + try { + this.stopSession(); + if (!args || !args.restart) { + this.sendEvent(new TerminatedEvent()); + } + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse(response, 1, err.message); + } + } + + private async runSession(args: CmsisRequestArguments): Promise { + logger.setup(args.verbose ? Logger.LogLevel.Verbose : Logger.LogLevel.Warn, args.logFile || false); + + this.gdb.on('consoleStreamOutput', (output, category) => this.sendEvent(new OutputEvent(output, category))); + this.gdb.on('execAsync', (resultClass, resultData) => this.handleGDBAsync(resultClass, resultData)); + this.gdb.on('notifyAsync', (resultClass, resultData) => this.handleGDBNotify(resultClass, resultData)); + + // gdb server has main info channel on stderr + this.gdbServer.on('stderr', data => { + this.sendEvent(new OutputEvent(data, 'stdout')) + }); + this.gdbServer.on('error', message => { + this.sendEvent(new TerminatedEvent()); + throw message; + }); + + try { + this.symbolTable = new SymbolTable(args.program, args.objdump); + await this.symbolTable.loadSymbols(); + } catch (error) { + this.sendEvent(new OutputEvent(`Unable to load debug symbols: ${error.message}`)); + } + + const port = await this.portScanner.findFreePort(); + if (!port) { + throw new Error('Unable to find a free port to use for debugging'); + } + this.sendEvent(new OutputEvent(`Selected port ${port} for debugging`)); + + const remote = `localhost:${port}`; + + // Set gdb arguments + if (!args.gdbArguments) { + args.gdbArguments = []; + } + args.gdbArguments.push('-q', args.program); + + // Set gdb server arguments + if (!args.gdbServerArguments) { + args.gdbServerArguments = []; + } + args.gdbServerArguments.push('--port', port.toString()); + + // Start gdb client and server + this.progressEvent(0, 'Starting Debugger'); + await this.gdbServer.spawn(args); + await this.spawn(args); + + // Send commands + await mi.sendTargetAsyncOn(this.gdb); + await mi.sendTargetSelectRemote(this.gdb, remote); + await mi.sendMonitorResetHalt(this.gdb); + this.sendEvent(new OutputEvent(`Attached to debugger on port ${port}`)); + + // Download image + const progressListener = (percent: number) => this.progressEvent(percent, 'Loading Image'); + progressListener(0); + this.gdbServer.on('progress', progressListener); + await mi.sendTargetDownload(this.gdb); + this.gdbServer.off('progress', progressListener); + progressListener(100); + + // Halt after image download + await mi.sendMonitorResetHalt(this.gdb); + await this.gdb.sendEnablePrettyPrint(); + + if (args.runToMain === true) { + await mi.sendBreakOnFunction(this.gdb); + } + + this.sendEvent(new OutputEvent(`Image loaded: ${args.program}`)); + this.sendEvent(new InitializedEvent()); + } + + private async getGlobalVariables(frameHandle: number): Promise { + const frame = this.frameHandles.get(frameHandle); + const symbolInfo = this.symbolTable.getGlobalVariables(); + const variables: DebugProtocol.Variable[] = []; + + for (const symbol of symbolInfo) { + const name = `global_var_${symbol.name}`; + const variable = await this.getVariables(frame, name, symbol.name, -1); + variables.push(variable); + } + + 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); + const file = normalize(result.frame.file || ''); + const symbolInfo = this.symbolTable.getStaticVariables(file); + const variables: DebugProtocol.Variable[] = []; + + // Fetch stack depth to obtain frameId/threadId/depth tuple + const stackDepth = await mi.sendStackInfoDepth(this.gdb, { maxDepth: 100 }); + const depth = parseInt(stackDepth.depth, 10); + + for (const symbol of symbolInfo) { + const name = `${file}_static_var_${symbol.name}`; + const variable = await this.getVariables(frame, name, symbol.name, depth); + variables.push(variable); + } + + return variables; + } + + private async getVariables(frame: FrameReference, name: string, expression: string, depth: number): Promise { + let global = varMgr.getVar(frame.frameId, frame.threadId, depth, name); + + if (global) { + // Update value if it is already loaded + const vup = await mi.sendVarUpdate(this.gdb, { name }); + const update = vup.changelist[0]; + if (update && update.in_scope === 'true' && update.name === global.varname) { + global.value = update.value; + } + } else { + // create var in GDB and store it in the varMgr + const varCreateResponse = await mi.sendVarCreate(this.gdb, { + name, + frame: 'current', + expression, + }); + + global = varMgr.addVar(frame.frameId, frame.threadId, depth, name, true, false, varCreateResponse); + } + + return { + name: expression, + value: (global.value === void 0) ? '' : global.value, + type: global.type, + variablesReference: parseInt(global.numchild, 10) > 0 + ? this.variableHandles.create({ + frameHandle: this.globalHandle, + type: 'object', + varobjName: global.varname, + }) + : 0, + }; + } + + private progressEvent(percent: number, message: string) { + this.sendEvent(new OutputEvent('progress', 'telemetry', { + percent, + message + })); + } + + protected async stopSession() { + // Pause debugging + if (this.isRunning) { + // Need to pause first + const waitPromise = new Promise(resolve => this.waitPaused = resolve); + this.gdb.pause(); + await waitPromise; + } + + // Detach + if ((this.gdb as CmsisBackend).isRunning) { + try { + await mi.sendTargetDetach(this.gdb); + } catch (e) { + // Need to catch here as the command result being returned will never exist as it's detached + } + } + + // Stop gdb client and server + try { + await this.gdb.sendGDBExit(); + } catch (e) { + // Need to catch here in case the connection has already been closed + } + this.gdbServer.kill(); + } + + public async shutdown() { + await this.stopSession(); + super.shutdown(); + } +} diff --git a/arduino-debugger-extension/src/node/debug-adapter/index.ts b/arduino-debugger-extension/src/node/debug-adapter/index.ts new file mode 100644 index 00000000..d0765327 --- /dev/null +++ b/arduino-debugger-extension/src/node/debug-adapter/index.ts @@ -0,0 +1,34 @@ +/* +* CMSIS Debug Adapter +* Copyright (c) 2019 Arm Limited +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +import * as process from 'process'; +import { logger } from 'vscode-debugadapter/lib/logger'; +import { CmsisDebugSession } from './cmsis-debug-session'; + +process.on('uncaughtException', (err: any) => { + logger.error(JSON.stringify(err)); +}); + +CmsisDebugSession.run(CmsisDebugSession); diff --git a/arduino-debugger-extension/src/node/debug-adapter/mi.ts b/arduino-debugger-extension/src/node/debug-adapter/mi.ts new file mode 100644 index 00000000..c9200e3e --- /dev/null +++ b/arduino-debugger-extension/src/node/debug-adapter/mi.ts @@ -0,0 +1,80 @@ +/* +* CMSIS Debug Adapter +* Copyright (c) 2019 Arm Limited +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +import { MIFrameInfo } from 'cdt-gdb-adapter/dist/mi'; +import { GDBBackend } from 'cdt-gdb-adapter/dist/GDBBackend'; + +export function sendTargetAsyncOn(gdb: GDBBackend) { + const set = 'target-async on'; + return gdb.sendGDBSet(set); +} + +export function sendMonitorResetHalt(gdb: GDBBackend) { + const command = '-interpreter-exec console "monitor reset halt"'; + return gdb.sendCommand(command); +} + +export function sendTargetSelectRemote(gdb: GDBBackend, remote: string) { + const command = `-target-select extended-remote ${remote}`; + return gdb.sendCommand(command); +} + +export function sendTargetDownload(gdb: GDBBackend) { + const command = '-target-download'; + return gdb.sendCommand(command); +} + +export function sendBreakOnFunction(gdb: GDBBackend, fn: string = 'main') { + const command = `-break-insert -t --function ${fn}`; + return gdb.sendCommand(command); +} + +export function sendExecInterrupt(gdb: GDBBackend, threadId?: number) { + let command = '-exec-interrupt'; + if (threadId) { + command += ` --thread ${threadId}`; + } + return gdb.sendCommand(command); +} + +export function sendStackInfoFrame(gdb: GDBBackend, threadId: number, frameId: number): Promise<{frame: MIFrameInfo}> { + const command = `-stack-info-frame --thread ${threadId} --frame ${frameId}`; + return gdb.sendCommand(command); +} + +export function sendUserInput(gdb: GDBBackend, command: string): Promise { + if (!command.startsWith('-')) { + command = `interpreter-exec console "${command}"`; + } + + return gdb.sendCommand(command); +} + +export function sendTargetDetach(gdb: GDBBackend) { + const command = '-target-detach'; + return gdb.sendCommand(command); +} + +export * from 'cdt-gdb-adapter/dist/mi'; diff --git a/arduino-debugger-extension/src/node/debug-adapter/openocd-server.ts b/arduino-debugger-extension/src/node/debug-adapter/openocd-server.ts new file mode 100644 index 00000000..798b9e8f --- /dev/null +++ b/arduino-debugger-extension/src/node/debug-adapter/openocd-server.ts @@ -0,0 +1,51 @@ +import { AbstractServer } from './abstract-server'; +import { PortScanner } from './port-scanner'; + +const LAUNCH_REGEX = /GDB server started/; +const ERROR_REGEX = /:ERROR:gdbserver:/; +const PERCENT_MULTIPLIER = 100 / 40; // pyOCD outputs 40 markers for progress + +export class OpenocdServer extends AbstractServer { + + protected portScanner = new PortScanner(); + protected progress = 0; + + protected async resolveServerArguments(serverArguments?: string[]): Promise { + if (!serverArguments) { + serverArguments = []; + } + + const telnetPort = await this.portScanner.findFreePort(4444); + + if (!telnetPort) { + return serverArguments; + } + + return [ + ...serverArguments, + '--telnet-port', + telnetPort.toString() + ]; + } + + protected onStdout(chunk: string | Buffer) { + super.onStdout(chunk); + const buffer = typeof chunk === 'string' ? chunk : chunk.toString('utf8'); + const match = buffer.match(/=/g); + + if (match) { + this.progress += match.length; + const percent = Math.round(this.progress * PERCENT_MULTIPLIER); + this.emit('progress', percent); + } + } + + protected serverStarted(data: string): boolean { + return LAUNCH_REGEX.test(data); + } + + protected serverError(data: string): boolean { + return ERROR_REGEX.test(data); + } + +} diff --git a/arduino-debugger-extension/src/node/debug-adapter/port-scanner.ts b/arduino-debugger-extension/src/node/debug-adapter/port-scanner.ts new file mode 100644 index 00000000..8e811627 --- /dev/null +++ b/arduino-debugger-extension/src/node/debug-adapter/port-scanner.ts @@ -0,0 +1,192 @@ +/* +* CMSIS Debug Adapter +* Copyright (c) 2016 Zoujie +* Copyright (c) 2019 Arm Limited +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +import { exec } from 'child_process'; + +const maxBuffer = 2 * 1024 * 1024; + +export class PortScanner { + + public async findFreePort(start: number = 50000, length: number = 100): Promise { + const fn = this.getFunction().bind(this); + + for (let i = start; i <= start + length; i++) { + try { + // Try to find pid of port + await fn(i); + } catch (_e) { + // Port is free when pid not found + return i; + } + } + + return undefined; + } + + private getFunction(): (port: number) => Promise { + switch (process.platform) { + case 'darwin': + case 'freebsd': + case 'sunos': { + return this.darwin; + break; + } + case 'linux': { + return this.linux; + break; + } + case 'win32': { + return this.windows; + break; + } + } + + return () => Promise.resolve(0); + } + + private async darwin(port: number): Promise { + const result = await this.execute('netstat -anv -p TCP && netstat -anv -p UDP'); + + // Replace header + const data = this.stripLine(result.toString(), 2); + + const found = this.extractColumns(data, [0, 3, 8], 10) + .filter(row => !!String(row[0]).match(/^(udp|tcp)/)) + .find(row => { + const matches = String(row[1]).match(/\.(\d+)$/); + return (matches && matches[1] === String(port)); + }); + + if (found && found[2].length) { + return parseInt(found[2], 10); + } + + throw new Error(`pid of port (${port}) not found`); + } + + private async linux(port: number): Promise { + // netstat -p ouputs warning if user is no-root + const result = await this.execute('netstat -tunlp'); + + // Replace header + const data = this.stripLine(result.toString(), 2); + + const columns = this.extractColumns(data, [3, 6], 7) + .find(column => { + const matches = String(column[0]).match(/:(\d+)$/); + return (matches && matches[1] === String(port)); + }); + + if (columns && columns[1]) { + const pid = columns[1].split('/', 1)[0]; + + if (pid.length) { + return parseInt(pid, 10); + } + } + + throw new Error(`pid of port (${port}) not found`); + } + + private async windows(port: number): Promise { + const result = await this.execute('netstat -ano'); + + // Replace header + const data = this.stripLine(result.toString(), 4); + + const columns = this.extractColumns(data, [1, 4], 5) + .find(column => { + const matches = String(column[0]).match(/:(\d+)$/); + return (matches && matches[1] === String(port)); + }); + + if (columns && columns[1].length && parseInt(columns[1], 10) > 0) { + return parseInt(columns[1], 10); + } + + throw new Error(`pid of port (${port}) not found`); + } + + private execute(cmd: string): Promise { + return new Promise((resolve, reject) => { + exec(cmd, { + maxBuffer, + windowsHide: true + }, (error: Error | null, stdout: string) => { + if (error) { + return reject(error); + } + + return resolve(stdout); + }); + }); + } + + private stripLine(text: string, num: number): string { + let idx = 0; + + while (num-- > 0) { + const nIdx = text.indexOf('\n', idx); + if (nIdx >= 0) { + idx = nIdx + 1; + } + } + + return idx > 0 ? text.substring(idx) : text; + } + + private extractColumns(text: string, idxes: number[], max: number): string[][] { + const lines = text.split(/(\r\n|\n|\r)/); + const columns: string[][] = []; + + if (!max) { + max = Math.max.apply(null, idxes) + 1; + } + + lines.forEach(line => { + const cols = this.split(line, max); + const column: string[] = []; + + idxes.forEach(idx => { + column.push(cols[idx] || ''); + }); + + columns.push(column); + }); + + return columns; + } + + private split(line: string, max: number): string[] { + const cols = line.trim().split(/\s+/); + + if (cols.length > max) { + cols[max - 1] = cols.slice(max - 1).join(' '); + } + + return cols; + } +} diff --git a/arduino-debugger-extension/src/node/debug-adapter/pyocd-server.ts b/arduino-debugger-extension/src/node/debug-adapter/pyocd-server.ts new file mode 100644 index 00000000..1450f488 --- /dev/null +++ b/arduino-debugger-extension/src/node/debug-adapter/pyocd-server.ts @@ -0,0 +1,75 @@ +/* +* CMSIS Debug Adapter +* Copyright (c) 2019 Arm Limited +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +import { AbstractServer } from './abstract-server'; +import { PortScanner } from './port-scanner'; + +const LAUNCH_REGEX = /GDB server started/; +const ERROR_REGEX = /:ERROR:gdbserver:/; +const PERCENT_MULTIPLIER = 100 / 40; // pyOCD outputs 40 markers for progress + +export class PyocdServer extends AbstractServer { + + protected portScanner = new PortScanner(); + protected progress = 0; + + protected async resolveServerArguments(serverArguments?: string[]): Promise { + if (!serverArguments) { + serverArguments = []; + } + + const telnetPort = await this.portScanner.findFreePort(4444); + + if (!telnetPort) { + return serverArguments; + } + + return [ + ...serverArguments, + '--telnet-port', + telnetPort.toString() + ]; + } + + protected onStdout(chunk: string | Buffer) { + super.onStdout(chunk); + const buffer = typeof chunk === 'string' ? chunk : chunk.toString('utf8'); + const match = buffer.match(/=/g); + + if (match) { + this.progress += match.length; + const percent = Math.round(this.progress * PERCENT_MULTIPLIER); + this.emit('progress', percent); + } + } + + protected serverStarted(data: string): boolean { + return LAUNCH_REGEX.test(data); + } + + protected serverError(data: string): boolean { + return ERROR_REGEX.test(data); + } +} diff --git a/arduino-debugger-extension/src/node/debug-adapter/symbols.ts b/arduino-debugger-extension/src/node/debug-adapter/symbols.ts new file mode 100644 index 00000000..1846cb2b --- /dev/null +++ b/arduino-debugger-extension/src/node/debug-adapter/symbols.ts @@ -0,0 +1,151 @@ +/* +* CMSIS Debug Adapter +* Copyright (c) 2017-2019 Marcel Ball +* Copyright (c) 2019 Arm Limited +* +* The MIT License (MIT) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +*/ + +import { spawnSync } from 'child_process'; +import { platform, EOL } from 'os'; +import { dirname, normalize, basename } from 'path'; + +export enum SymbolType { + Function, + File, + Object, + Normal +} + +export enum SymbolScope { + Local, + Global, + Neither, + Both +} + +export interface SymbolInformation { + address: number; + length: number; + name: string; + section: string; + type: SymbolType; + scope: SymbolScope; + file?: string; + 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 } = { + 'F': SymbolType.Function, + 'f': SymbolType.File, + 'O': SymbolType.Object, + ' ': SymbolType.Normal +}; + +const SCOPE_MAP: { [id: string]: SymbolScope } = { + 'l': SymbolScope.Local, + 'g': SymbolScope.Global, + ' ': SymbolScope.Neither, + '!': SymbolScope.Both +}; + +export class SymbolTable { + + private symbols: SymbolInformation[] = []; + + constructor(private program: string, private objdump: string = DEFAULT_OBJDUMP) { + } + + public async loadSymbols(): Promise { + const results = await this.execute(); + const output = results.toString(); + const lines = output.split(EOL); + let currentFile: string | undefined; + + for (const line of lines) { + const match = line.match(SYMBOL_REGEX); + if (match) { + if (match[7] === 'd' && match[8] === 'f') { + currentFile = match[11].trim(); + } + const type = TYPE_MAP[match[8]]; + const scope = SCOPE_MAP[match[2]]; + let name = match[11].trim(); + let hidden = false; + + if (name.startsWith('.hidden')) { + name = name.substring(7).trim(); + hidden = true; + } + + this.symbols.push({ + address: parseInt(match[1], 16), + type: type, + scope: scope, + section: match[9].trim(), + length: parseInt(match[10], 16), + name: name, + file: scope === SymbolScope.Local ? currentFile : undefined, + hidden: hidden + }); + } + } + } + + public getGlobalVariables(): SymbolInformation[] { + const matches = this.symbols.filter(s => s.type === SymbolType.Object && s.scope === SymbolScope.Global); + return matches; + } + + public getStaticVariables(file: string): SymbolInformation[] { + return this.symbols.filter(s => + s.type === SymbolType.Object // Only load objects + && s.scope === SymbolScope.Local // Scoped to this file + && !s.name.startsWith('.') // Ignore names beginning with '.' + && (normalize(s.file || '') === normalize(file) || normalize(s.file || '') === basename(file))); // Match full path or file name + } + + private execute(): Promise { + return new Promise((resolve, reject) => { + try { + const { stdout, stderr } = spawnSync(this.objdump, [ + '--syms', + this.program + ], { + cwd: dirname(this.objdump), + windowsHide: true + }); + + const error = stderr.toString('utf8'); + if (error) { + return reject(new Error(error)); + } + + resolve(stdout.toString('utf8')); + } catch (error) { + return reject(new Error(error)); + } + }); + } +} diff --git a/arduino-debugger-extension/tsconfig.json b/arduino-debugger-extension/tsconfig.json new file mode 100644 index 00000000..86907b52 --- /dev/null +++ b/arduino-debugger-extension/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "noImplicitAny": true, + "noEmitOnError": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "strictNullChecks": true, + "experimentalDecorators": true, + "downlevelIteration": true, + "emitDecoratorMetadata": true, + "module": "commonjs", + "moduleResolution": "node", + "target": "es6", + "outDir": "lib", + "lib": [ + "es6", + "dom" + ], + "jsx": "react", + "sourceMap": true, + "skipLibCheck": true + }, + "include": [ + "src" + ], + "files": [ + "../node_modules/@theia/monaco/src/typings/monaco/index.d.ts" + ] +} \ No newline at end of file diff --git a/arduino-debugger-extension/tslint.json b/arduino-debugger-extension/tslint.json new file mode 100644 index 00000000..55b00628 --- /dev/null +++ b/arduino-debugger-extension/tslint.json @@ -0,0 +1,37 @@ +{ + "rules": { + "class-name": true, + "comment-format": [true, "check-space"], + "curly": false, + "forin": false, + "indent": [true, "spaces"], + "max-line-length": [true, 180], + "no-trailing-whitespace": false, + "no-unused-expression": true, + "no-var-keyword": true, + "one-line": [true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "radix": true, + "trailing-comma": [false], + "triple-equals": [true, "allow-null-check"], + "typedef-whitespace": [true, { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + }], + "variable-name": false, + "whitespace": [true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } +} \ No newline at end of file diff --git a/browser-app/package.json b/browser-app/package.json index e0d7e676..7dbca598 100644 --- a/browser-app/package.json +++ b/browser-app/package.json @@ -19,14 +19,15 @@ "@theia/terminal": "next", "@theia/workspace": "next", "@theia/textmate-grammars": "next", - "arduino-ide-extension": "0.0.4" + "arduino-ide-extension": "0.0.4", + "arduino-debugger-extension": "0.0.2" }, "devDependencies": { "@theia/cli": "next" }, "scripts": { "prepare": "theia build --mode development", - "start": "theia start", + "start": "theia start --plugins=local-dir:../", "watch": "theia build --watch --mode development" }, "theia": { diff --git a/package.json b/package.json index d3418702..e68c9283 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "workspaces": [ "arduino-ide-extension", + "arduino-debugger-extension", "electron-app", "browser-app" ]