In my efforts to implement debugging for Indel embedded systems using Indel’s own GDB stub, I have arrived at a point where I believe it no longer makes sense to use the CDT GDB Debug Adapter Extension (hereafter cdt-gdb-vscode) and its gdbtarget debug type as-is, but I need to extend it and the adapter provided by it (cdt-gdb-adapter) with functionality that is too Indel-specific to be upstreamed, using at least a subclass of GDBTargetDebugSession.
I am describing here my current approach of doing that, in hopes of
- discussing whether there are better ways of achieving my goals in my VS Code extension,
- discussing how we can make cdt-gdb-vscode and cdt-gdb-adapter more amenable to this kind of extension by downstream adopters,
- finding out how other adopters, if there are any, are approaching such challenges,
- serving as examples of adopter requirements to keep in mind while making changes to cdt-gdb-vscode and cdt-gdb-adapter.
The extension this is happening in is INOS Tools and it is not presently open-source, but I can show relevant excerpts of the code. The things shown below are released in its current version and used by a small number of early-adopter users.
Requirements and choices
- In their launch.json, the user should not have to deal with the entirety of options expected by cdt-gdb-adapter, because many of them need to be set to very specific values, and some of these values are system-dependent and not suitable for checking into version control. Instead, a typical launch configuration is simply
{
"type": "inos",
"request": "attach",
"name": "INOS Attach"
}
and all the other necessary options are added programmatically by a DebugConfigurationProvider.
- The stop notifications and thread list XML output by our stub contain some INOS-specific information that I want to display in certain ways in the Call Stack view. This seems to require subclassing
GDBTargetDebugSession and overriding some methods to modify how they fill that information into DAP structures. I also need some additional buttons in that view to interact with threads, as part of the way I work around GDB’s tendency to stop threads when we don’t want it to (which is a separate topic that I could describe if desired).
- I avoid bundling cdt-gdb-adapter with the INOS Tools extension, but load it at runtime from the separately installed cdt-gdb-vscode extension. Search for
INOSTOOLS_CDT_GDB_ADAPTER_PATH in the code snippets below to locate how this is done. Advantages of that are:
- It allows users to use the adapter standalone for other purposes without duplication.
- It spares me the hassle of building the native parts contained within it (serial port support?).
- It keeps the EPL-licensed and non-EPL-licensed parts neatly separated.
- I can still support type checking in the editor and convenient debugging that way, for that I still add cdt-gdb-adapter as a development dependency.
- Probably further INOS-specific requirements will come up in the future.
Implementation
I define my own debug type "inos". This seems necessary to use my own subclassed debug adapter, makes it easier for the DebugConfigurationProvider to recognize the configurations meant for it (before, when they also had type gdbtarget, I used the heuristic whether the name contained "INOS" to distinguish them from unrelated gdbtarget ones – I could also have added a special attribute, but that would have been underlined as an error in the editor because it doesn’t appear in the gdbtarget schema from cdt-gdb-vscode), allows new debug configurations to be created using IntelliSense, and allows restricting commands to exactly my debug sessions using when conditions. On the flip side, it causes some duplication, makes it harder to reuse some functionality that cdt-gdb-vscode only registers for gdb and gdbtarget (as we are currently seeing in #208), and relies on cdt-gdb-adapter being able to cope with types other than gdb and gdbtarget (which it currently does, as far as I can tell, but the burden is on me to make sure that stays that way).
package.json
{
…
"contributes": {
…
"debuggers": [
{
"type": "inos",
"label": "INOS",
"program": "./dist/debugadapter.js",
"runtime": "node",
"languages": [
"c",
"cpp"
],
"configurationAttributes": {
"attach": {
"properties": {
… lots of stuff duplicated from cdt-gdb-vscode
}
}
},
"initialConfigurations": [
{
"type": "inos",
"request": "attach",
"name": "INOS Attach"
}
],
"configurationSnippets": [
{
"label": "INOS: Attach",
"description": "A new configuration for attaching to an INOS target",
"body": {
"type": "inos",
"request": "attach",
"name": "${1:INOS Attach}"
}
}
]
}
]
},
…
}
extension.ts
export function activate(context: vscode.ExtensionContext) {
…
context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider(
'inos',
{
provideDebugConfigurations(folder, token) {
// This is used when no launch.json exists. What we return here
// does not seem to matter for "Run and Debug" > "INOS",
// resolveDebugConfiguration() receives an empty object anyway
// (the function does need to exist for that to work though,
// otherwise nothing happens after clicking "INOS"), only for
// "Run and Debug" > "More INOS options…".
return [
{
type: 'inos',
request: 'attach',
name: 'INOS Attach',
}
];
},
resolveDebugConfiguration(folder, debugConfiguration, token) {
if (Object.keys(debugConfiguration).length === 0) {
// This happens when clicking "Run and Debug" > "INOS"
// when no launch.json exists.
debugConfiguration.type = 'inos';
debugConfiguration.request = 'attach';
debugConfiguration.name = 'INOS Attach';
}
resolveInosDebugConfiguration(debugConfiguration); // this fills in all required cdt-gdb-adapter options
logger.info('Resolved debug configuration:\n' + JSON.stringify(debugConfiguration, null, '\t'));
return debugConfiguration;
},
},
vscode.DebugConfigurationProviderTriggerKind.Dynamic
));
context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory(
'inos',
{
// this is called after the DebugConfigurationProvider
createDebugAdapterDescriptor(session, executable) {
if (!executable) {
return undefined;
}
const cdtext = vscode.extensions.getExtension('eclipse-cdt.cdt-gdb-vscode');
if (!cdtext) {
vscode.window.showErrorMessage('Cannot find extension “CDT GDB Debug Adapter”. Make sure it is installed.', { modal: true });
return undefined;
}
const env = executable.options?.env ?? {};
env['INOSTOOLS_CDT_GDB_ADAPTER_PATH'] = cdtext.extensionPath;
return new vscode.DebugAdapterExecutable(
executable.command,
executable.args,
{ env }
);
}
}
));
…
}
debugadapter.ts
The method overriding done here is not very elegant, GDBTargetDebugSession clearly was not designed to have a clean API for overriding subclasses. But it works for now. Fortunately everything is protected, so I can override whatever I want, but at the cost of duplicated superclass functionality (fragile) or convoluted workarounds. Probably cases will come up in the future where the current state is insufficient and dedicated override points will need to be added.
import util from "node:util";
import { GDBTargetDebugSession as GDBTargetDebugSession_t } from "cdt-gdb-adapter";
import { DebugProtocol } from "@vscode/debugprotocol";
import { InvalidatedEvent, OutputEvent } from "@vscode/debugadapter";
// Rudimentary DAP implementation to send error messages to the GUI for failures
// before the actual DAP server is loaded. Apparently VS Code reports neither
// nonzero exit status nor stderr when a debug adapter fails to start before
// answering DAP requests.
function sendOutput(message: string) {
const event = Buffer.from(JSON.stringify(newOutputEvent(message, 'important')));
process.stdout.write(`Content-Length: ${event.length}\r\n\r\n`);
process.stdout.write(event);
process.stdout.write('\r\n');
}
process.on('uncaughtException', (err) => {
sendOutput(((err instanceof Error) ? err.message + '\n\n' : '') + util.inspect(err));
console.error(err);
process.exit(1);
});
const GDBTargetDebugSession: typeof GDBTargetDebugSession_t = require(process.env['INOSTOOLS_CDT_GDB_ADAPTER_PATH'] + '/node_modules/cdt-gdb-adapter/dist/desktop/GDBTargetDebugSession.js').GDBTargetDebugSession;
// Use these instead of `new XxxEvent` because the latter pulls all of @vscode/
// debugadapter into the bundle. (We can use the types at compile time, just not
// the constructors at runtime).
function newOutputEvent(output: string, category?: string, data?: any): OutputEvent {
return {
"seq": 0, "type": "event", "event": "output",
"body": { category: category ?? 'console', output, data }
};
}
function newInvalidatedEvent(areas?: DebugProtocol.InvalidatedAreas[], threadId?: number, stackFrameId?: number): InvalidatedEvent {
return {
"seq": 0, "type": "event", "event": "invalidated",
"body": { areas, threadId, stackFrameId }
};
}
// Same for classes from cdt-gdb-adapter
function newStoppedEvent(reason: string, threadId: number, allThreadsStopped = false, description?: string, text?: string): DebugProtocol.StoppedEvent {
return {
"seq": 0, "type": "event", "event": "stopped",
"body": { reason, threadId, allThreadsStopped, description, text }
};
}
class InosTargetDebugSession extends GDBTargetDebugSession {
private currentGdbStoppedResult: any;
constructor() {
super();
}
protected customRequest(command: string, response: DebugProtocol.Response, args: any): void {
if (command === 'inos-tools/thread-refresh') {
// Passing the thread ID here is not very effective, as the
// 'threads' request that comes in response can only ask for all
// threads, not for individual ones (and GDB could not either), but
// we may just as well.
this.sendEvent(newInvalidatedEvent(['threads'], args.threadId));
this.sendResponse(response);
}
else {
return super.customRequest(command, response, args);
}
}
protected sendStoppedEvent(reason: string, threadId: number, allThreadsStopped?: boolean): void {
// superclass stuff
this.frameHandles.reset();
this.variableHandles.reset();
const event = newStoppedEvent(reason, threadId, allThreadsStopped);
if (this.currentGdbStoppedResult) {
const corenum = parseInt(this.currentGdbStoppedResult['core'], 16); // correctly handles undefined, but only because 'u' is not a base-16 digit
if (!isNaN(corenum) && (corenum & 0x4000)) { // eCoreIdFlagPretendStopped
event.body.description = 'Watched';
}
else if (this.currentGdbStoppedResult['signal-name'] === '0') {
// Happens after an explicit pause request. "Paused on 0"
// (default derived from signal-name) or "Signal 0" (signal-
// meaning) is not useful. "Paused" is no more informative, but
// looks consistent with VS Code's "Paused on Step" or "Paused
// on Breakpoint".
event.body.description = 'Paused';
}
else if (this.currentGdbStoppedResult['signal-meaning']) {
// Replace "Paused on SIG<n>". Especially needed for traps
// because this is the only way we get the trap number (it's not
// in the thread state from the thread list).
event.body.description = String(this.currentGdbStoppedResult['signal-meaning']);
}
}
this.sendEvent(event);
}
protected handleGDBStopped(result: any): void {
// gather additional info for sendStoppedEvent
this.currentGdbStoppedResult = result;
try {
// this may cause a call to sendStoppedEvent
super.handleGDBStopped(result);
}
finally {
this.currentGdbStoppedResult = undefined;
}
}
protected async continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): Promise<void> {
await super.continueRequest(response, args);
// Update the thread list, otherwise the thread state is still displayed
// as "Debug suspended".
// Needs "updateThreadInfo": "when-requested" in debug configuration
// (automatically set up by resolveInosDebugConfiguration()).
this.sendEvent(newInvalidatedEvent(['threads'], args.threadId));
}
}
InosTargetDebugSession.run(InosTargetDebugSession);
In my efforts to implement debugging for Indel embedded systems using Indel’s own GDB stub, I have arrived at a point where I believe it no longer makes sense to use the CDT GDB Debug Adapter Extension (hereafter cdt-gdb-vscode) and its
gdbtargetdebug type as-is, but I need to extend it and the adapter provided by it (cdt-gdb-adapter) with functionality that is too Indel-specific to be upstreamed, using at least a subclass ofGDBTargetDebugSession.I am describing here my current approach of doing that, in hopes of
The extension this is happening in is INOS Tools and it is not presently open-source, but I can show relevant excerpts of the code. The things shown below are released in its current version and used by a small number of early-adopter users.
Requirements and choices
{ "type": "inos", "request": "attach", "name": "INOS Attach" }GDBTargetDebugSessionand overriding some methods to modify how they fill that information into DAP structures. I also need some additional buttons in that view to interact with threads, as part of the way I work around GDB’s tendency to stop threads when we don’t want it to (which is a separate topic that I could describe if desired).INOSTOOLS_CDT_GDB_ADAPTER_PATHin the code snippets below to locate how this is done. Advantages of that are:Implementation
I define my own debug type
"inos". This seems necessary to use my own subclassed debug adapter, makes it easier for the DebugConfigurationProvider to recognize the configurations meant for it (before, when they also had typegdbtarget, I used the heuristic whether the name contained "INOS" to distinguish them from unrelatedgdbtargetones – I could also have added a special attribute, but that would have been underlined as an error in the editor because it doesn’t appear in thegdbtargetschema from cdt-gdb-vscode), allows new debug configurations to be created using IntelliSense, and allows restricting commands to exactly my debug sessions usingwhenconditions. On the flip side, it causes some duplication, makes it harder to reuse some functionality that cdt-gdb-vscode only registers forgdbandgdbtarget(as we are currently seeing in #208), and relies on cdt-gdb-adapter being able to cope with types other thangdbandgdbtarget(which it currently does, as far as I can tell, but the burden is on me to make sure that stays that way).package.json
{ … "contributes": { … "debuggers": [ { "type": "inos", "label": "INOS", "program": "./dist/debugadapter.js", "runtime": "node", "languages": [ "c", "cpp" ], "configurationAttributes": { "attach": { "properties": { … lots of stuff duplicated from cdt-gdb-vscode } } }, "initialConfigurations": [ { "type": "inos", "request": "attach", "name": "INOS Attach" } ], "configurationSnippets": [ { "label": "INOS: Attach", "description": "A new configuration for attaching to an INOS target", "body": { "type": "inos", "request": "attach", "name": "${1:INOS Attach}" } } ] } ] }, … }extension.ts
debugadapter.ts
The method overriding done here is not very elegant,
GDBTargetDebugSessionclearly was not designed to have a clean API for overriding subclasses. But it works for now. Fortunately everything isprotected, so I can override whatever I want, but at the cost of duplicated superclass functionality (fragile) or convoluted workarounds. Probably cases will come up in the future where the current state is insufficient and dedicated override points will need to be added.