Ran first debugging session

This commit is contained in:
Christian Weichel 2019-11-15 13:55:26 +01:00 committed by Miro Spönemann
parent e189a8c33e
commit 3d6d2ce814
22 changed files with 1428 additions and 9 deletions

4
.vscode/arduino.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"board": "arduino:samd:arduino_zero_edbg",
"port": "/dev/cu.usbmodem141402"
}

14
.vscode/c_cpp_properties.json vendored Normal file
View File

@ -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"
]
}
]
}

14
.vscode/launch.json vendored
View File

@ -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"
}
]
}

View File

@ -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"
}
]
}

View File

@ -0,0 +1,2 @@
gdb_port 50000
telnet_port 44444

View File

@ -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"

View File

@ -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<IJSONSchema[]> {
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<IJSONSchemaSnippet[]> {
return []
}
provideDebugAdapterExecutable?(config: DebugConfiguration): MaybePromise<DebugAdapterExecutable> {
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<DebugConfiguration[]> {
return [];
}
resolveDebugConfiguration?(config: DebugConfiguration, workspaceFolderUri?: string): MaybePromise<DebugConfiguration> {
return config;
}
}

View File

@ -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();
});

View File

@ -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<void> {
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<string[]> {
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;
}

View File

@ -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;
}
}

View File

@ -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<void> {
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<void> {
try {
await this.runSession(args);
this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 1, err.message);
}
}
protected async configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse): Promise<void> {
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<void> {
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<void> {
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<void> {
try {
response.body = {
variables: new Array<DebugProtocol.Variable>()
};
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<void> {
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<void> {
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<void> {
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<DebugProtocol.Variable[]> {
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<DebugProtocol.Variable[]> {
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<DebugProtocol.Variable> {
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) ? '<unknown>' : 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();
}
}

View File

@ -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);

View File

@ -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<any> {
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';

View File

@ -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<string[]> {
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);
}
}

View File

@ -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<number | undefined> {
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<number> {
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<number> {
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<number> {
// 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<number> {
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<string> {
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;
}
}

View File

@ -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<string[]> {
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);
}
}

View File

@ -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<void> {
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<string> {
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));
}
});
}
}

View File

@ -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"
]
}

View File

@ -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"
]
}
}

View File

@ -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": {

View File

@ -20,6 +20,7 @@
},
"workspaces": [
"arduino-ide-extension",
"arduino-debugger-extension",
"electron-app",
"browser-app"
]