Files
arduino-ide/arduino-debugger-extension/src/node/debug-adapter/cmsis-debug-session.ts
2020-02-13 10:15:12 +01:00

433 lines
17 KiB
TypeScript

/*
* 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, ErrorDestination } from 'vscode-debugadapter';
import { GDBDebugSession, RequestArguments, FrameVariableReference, FrameReference } from 'cdt-gdb-adapter/dist/GDBDebugSession';
import { GDBBackend } from 'cdt-gdb-adapter/dist/GDBBackend';
import { CmsisBackend } from './cmsis-backend';
// import { PyocdServer } from './pyocd-server';
import { PortScanner } from './port-scanner';
import { SymbolTable } from './symbols';
import { VarManager } from 'cdt-gdb-adapter/dist/varManager';
import * as mi from './mi';
import { OpenocdServer } from './openocd-server';
export interface CmsisRequestArguments extends RequestArguments {
runToMain?: boolean;
gdbServer?: string;
gdbServerArguments?: string[];
gdbServerPort?: number;
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 OpenocdServer();
protected portScanner = new PortScanner();
protected symbolTable!: SymbolTable;
protected globalHandle!: number;
protected varMgr: VarManager;
constructor() {
super();
}
protected createBackend(): GDBBackend {
const gdb = new CmsisBackend();
this.varMgr = new VarManager(gdb);
return gdb;
}
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 async setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): Promise<void> {
await super.setBreakPointsRequest(response, args);
}
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: 42000,
// }
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.varobjName === '__pins') {
response.body.variables = await this.handlePinStatusRequest();
} else if (ref && ref.type === 'object') {
// List data under any variable
response.body.variables = await this.handleVariableRequestObject(ref);
}
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')));
const gdbServerErrors: any[] = []
const gdbServerErrorAccumulator = (message: any) => gdbServerErrors.push(message);
this.gdbServer.on('error', gdbServerErrorAccumulator);
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.gdbServerPort = port;
// Start gdb client and server
this.progressEvent(0, 'Starting Debugger');
this.sendEvent(new OutputEvent(`Starting debugger: ${JSON.stringify(args)}`));
await this.gdbServer.spawn(args);
await this.spawn(args);
if (gdbServerErrors.length > 0) {
throw new Error(gdbServerErrors.join('\n'));
}
// 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}`));
if (gdbServerErrors.length > 0) {
throw new Error(gdbServerErrors.join('\n'));
}
// 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);
}
if (gdbServerErrors.length > 0) {
throw new Error(gdbServerErrors.join('\n'));
}
this.sendEvent(new OutputEvent(`Image loaded: ${args.program}`));
this.sendEvent(new InitializedEvent());
this.gdbServer.removeListener('error', gdbServerErrorAccumulator);
this.gdbServer.on('error', message => {
logger.error(JSON.stringify(message));
this.sendEvent(new TerminatedEvent());
});
}
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 handlePinStatusRequest(): Promise<DebugProtocol.Variable[]> {
const variables: DebugProtocol.Variable[] = [];
variables.push({
name: "D2",
type: "gpio",
value: "0x00",
variablesReference: 0
})
return variables;
}
private async getStaticVariables(frameHandle: number): Promise<DebugProtocol.Variable[]> {
const frame = this.frameHandles.get(frameHandle);
const result = await mi.sendStackInfoFrame(this.gdb, frame.threadId, frame.frameId);
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 = this.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 = this.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 sendErrorResponse(response: DebugProtocol.Response,
codeOrMessage: number | DebugProtocol.Message, format?: string,
variables?: any, dest?: ErrorDestination): void {
if (!!format && (dest === undefined || dest === ErrorDestination.User)) {
format = format.replace('\n', '<br>');
}
super.sendErrorResponse(response, codeOrMessage, format, variables, dest);
}
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 - we give GDB five seconds to exit orderly before we kill the GDB server
setTimeout(() => this.gdbServer.kill(), 5000);
try {
await this.gdb.sendGDBExit();
} catch (e) {
// Need to catch here in case the connection has already been closed
}
}
public async shutdown() {
await this.stopSession();
super.shutdown();
}
}