Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow pre-filling launch/attach on command line to adapter #228

Merged
merged 5 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .mocharc-windows-ci.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"//": "This timeout should match what is in CdtDebugClient constructor",
"timeout": "25000"
}
1 change: 1 addition & 0 deletions .mocharc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"//": "This timeout should match what is in CdtDebugClient constructor",
"timeout": "5000"
}
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,54 @@ Build is pretty simple.
yarn
```

## Running

The entry point for the adapter is `cdtDebugAdapter` for local debugging
and `cdtDebugTargetAdapter` for target (remote) debugging.

### Command line arguments

#### `--server=PORT`

Start the adapter listening on the given port instead of on stdin/stdout.

#### `--config=INITIALCONFIG`

Start the adapter using the given configuration as a starting point for the args in `launch` or `attach` request.

For example, the default GDB can be set like this:

```sh
node debugTargetAdapter.js --config='{"gdb":"arm-none-eabi-gdb"}'
```

The config can be passed on the command line as JSON, or a response file can be used by starting the argument with `@`.
The rest of the argument will be interpreted as a file name to read.
For example, to start the adapter defaulting to a process ID to attach to, create a file containing the JSON and reference it like this:

```sh
cat >config.json <<END
{
"processId": 1234
}
END
node debugAdapter.js [email protected]

```

#### `--config-frozen=FROZENCONFIG`

Similar to `--config`, the `--config-frozen` sets the provided configuration fields in the args to the `launch` or `attach` request to the given values, not allowing the user to override them.
Specifying which type of request is allowed (`launch` or `attach`) can be specified with the `request` field.
When freezing the type of request, regardless of which type of request the user requested, the frozen request type will be used.

For example, the adapter can be configured for program to be frozen to a specific value.
This may be useful for starting adapters in a container and exposing the server port.

```sh
node debugAdapter.js --server=23221 --config-frozen='{"program":"/path/to/my.elf"}'
```

## Testing

Testing of the adapter can be run with `yarn test`. See [Integration Tests readme](https://github.com/eclipse-cdt-cloud/cdt-gdb-adapter/blob/main/src/integration-tests/README.md)
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,15 @@
"dependencies": {
"@vscode/debugadapter": "^1.48.0",
"@vscode/debugprotocol": "^1.48.0",
"node-addon-api": "^4.3.0"
"node-addon-api": "^4.3.0",
"tmp": "^0.2.1"
},
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/chai-string": "^1.4.2",
"@types/mocha": "^9.1.0",
"@types/node": "^14.18.17",
"@types/tmp": "^0.2.3",
"@typescript-eslint/eslint-plugin": "^5.10.1",
"@typescript-eslint/parser": "^5.10.1",
"@vscode/debugadapter-testsupport": "^1.37.1",
Expand Down
199 changes: 139 additions & 60 deletions src/GDBDebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
*********************************************************************/
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
import {
DebugSession,
Handles,
InitializedEvent,
Logger,
Expand Down Expand Up @@ -152,6 +154,18 @@ export function base64ToHex(base64: string): string {
}

export class GDBDebugSession extends LoggingDebugSession {
/**
* Initial (aka default) configuration for launch/attach request
* typically supplied with the --config command line argument.
*/
protected static defaultRequestArguments?: any;

/**
* Frozen configuration for launch/attach request
* typically supplied with the --config-frozen command line argument.
*/
protected static frozenRequestArguments?: { request?: string };

protected gdb: GDBBackend = this.createBackend();
protected isAttach = false;
// isRunning === true means there are no threads stopped.
Expand Down Expand Up @@ -184,6 +198,66 @@ export class GDBDebugSession extends LoggingDebugSession {
this.logger = logger;
}

/**
* Main entry point
*/
public static run(debugSession: typeof GDBDebugSession) {
GDBDebugSession.processArgv(process.argv.slice(2));
DebugSession.run(debugSession);
}

/**
* Parse an optional config file which is a JSON string of launch/attach request arguments.
* The config can be a response file by starting with an @.
*/
public static processArgv(args: string[]) {
args.forEach(function (val, _index, _array) {
const configMatch = /^--config(-frozen)?=(.*)$/.exec(val);
if (configMatch) {
let configJson;
const configStr = configMatch[2];
if (configStr.startsWith('@')) {
const configFile = configStr.slice(1);
configJson = JSON.parse(
fs.readFileSync(configFile).toString('utf8')
);
} else {
configJson = JSON.parse(configStr);
}
if (configMatch[1]) {
GDBDebugSession.frozenRequestArguments = configJson;
} else {
GDBDebugSession.defaultRequestArguments = configJson;
}
}
});
}

/**
* Apply the initial and frozen launch/attach request arguments.
* @param request the default request type to return if request type is not frozen
* @param args the arguments from the user to apply initial and frozen arguments to.
* @returns resolved request type and the resolved arguments
*/
protected applyRequestArguments(
request: 'launch' | 'attach',
args: LaunchRequestArguments | AttachRequestArguments
): ['launch' | 'attach', LaunchRequestArguments | AttachRequestArguments] {
const frozenRequest = GDBDebugSession.frozenRequestArguments?.request;
thegecko marked this conversation as resolved.
Show resolved Hide resolved
if (frozenRequest === 'launch' || frozenRequest === 'attach') {
request = frozenRequest;
}

return [
request,
{
...GDBDebugSession.defaultRequestArguments,
...args,
...GDBDebugSession.frozenRequestArguments,
},
];
}

protected createBackend(): GDBBackend {
return new GDBBackend();
}
Expand Down Expand Up @@ -242,40 +316,74 @@ export class GDBDebugSession extends LoggingDebugSession {
this.sendResponse(response);
}

protected async attachRequest(
response: DebugProtocol.AttachResponse,
args: AttachRequestArguments
): Promise<void> {
try {
logger.setup(
args.verbose ? Logger.LogLevel.Verbose : Logger.LogLevel.Warn,
args.logFile || false
);
protected async attachOrLaunchRequest(
response: DebugProtocol.Response,
request: 'launch' | 'attach',
args: LaunchRequestArguments | AttachRequestArguments
) {
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('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)
);
this.gdb.on('execAsync', (resultClass, resultData) =>
this.handleGDBAsync(resultClass, resultData)
);
this.gdb.on('notifyAsync', (resultClass, resultData) =>
this.handleGDBNotify(resultClass, resultData)
);

await this.spawn(args);
await this.gdb.sendFileExecAndSymbols(args.program);
await this.gdb.sendEnablePrettyPrint();
await this.spawn(args);
if (!args.program) {
this.sendErrorResponse(
response,
1,
'The program must be specified in the request arguments'
);
return;
}
await this.gdb.sendFileExecAndSymbols(args.program);
await this.gdb.sendEnablePrettyPrint();

await mi.sendTargetAttachRequest(this.gdb, { pid: args.processId });
if (request === 'attach') {
const attachArgs = args as AttachRequestArguments;
await mi.sendTargetAttachRequest(this.gdb, {
pid: attachArgs.processId,
});
this.sendEvent(
new OutputEvent(`attached to process ${args.processId}`)
new OutputEvent(`attached to process ${attachArgs.processId}`)
);
await this.gdb.sendCommands(args.initCommands);
}

this.sendEvent(new InitializedEvent());
this.sendResponse(response);
this.isInitialized = true;
await this.gdb.sendCommands(args.initCommands);

if (request === 'launch') {
const launchArgs = args as LaunchRequestArguments;
if (launchArgs.arguments) {
await mi.sendExecArguments(this.gdb, {
arguments: launchArgs.arguments,
});
}
}
this.sendEvent(new InitializedEvent());
this.sendResponse(response);
this.isInitialized = true;
}

protected async attachRequest(
response: DebugProtocol.AttachResponse,
args: AttachRequestArguments
): Promise<void> {
try {
const [request, resolvedArgs] = this.applyRequestArguments(
'attach',
args
);
await this.attachOrLaunchRequest(response, request, resolvedArgs);
} catch (err) {
this.sendErrorResponse(
response,
Expand All @@ -290,40 +398,11 @@ export class GDBDebugSession extends LoggingDebugSession {
args: LaunchRequestArguments
): Promise<void> {
try {
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)
const [request, resolvedArgs] = this.applyRequestArguments(
'launch',
args
);
this.gdb.on('notifyAsync', (resultClass, resultData) =>
this.handleGDBNotify(resultClass, resultData)
);

await this.spawn(args);
await this.gdb.sendFileExecAndSymbols(args.program);
await this.gdb.sendEnablePrettyPrint();

if (args.initCommands) {
for (const command of args.initCommands) {
await this.gdb.sendCommand(command);
}
}

if (args.arguments) {
await mi.sendExecArguments(this.gdb, {
arguments: args.arguments,
});
}
this.sendEvent(new InitializedEvent());
this.sendResponse(response);
this.isInitialized = true;
await this.attachOrLaunchRequest(response, request, resolvedArgs);
} catch (err) {
this.sendErrorResponse(
response,
Expand Down
41 changes: 36 additions & 5 deletions src/GDBTargetDebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,42 @@ export interface TargetLaunchRequestArguments
export class GDBTargetDebugSession extends GDBDebugSession {
protected gdbserver?: ChildProcess;

protected async attachOrLaunchRequest(
response: DebugProtocol.Response,
request: 'launch' | 'attach',
args: TargetLaunchRequestArguments | TargetAttachRequestArguments
) {
this.setupCommonLoggerAndHandlers(args);

if (request === 'launch') {
const launchArgs = args as TargetLaunchRequestArguments;
if (
launchArgs.target?.serverParameters === undefined &&
!launchArgs.program
) {
this.sendErrorResponse(
response,
1,
'The program must be specified in the launch request arguments'
);
return;
}
await this.startGDBServer(launchArgs);
}

await this.startGDBAndAttachToTarget(response, args);
}

protected async launchRequest(
response: DebugProtocol.LaunchResponse,
args: TargetLaunchRequestArguments
): Promise<void> {
try {
this.setupCommonLoggerAndHandlers(args);
await this.startGDBServer(args);
await this.startGDBAndAttachToTarget(response, args);
const [request, resolvedArgs] = this.applyRequestArguments(
'launch',
args
);
await this.attachOrLaunchRequest(response, request, resolvedArgs);
} catch (err) {
this.sendErrorResponse(
response,
Expand All @@ -99,8 +127,11 @@ export class GDBTargetDebugSession extends GDBDebugSession {
args: TargetAttachRequestArguments
): Promise<void> {
try {
this.setupCommonLoggerAndHandlers(args);
await this.startGDBAndAttachToTarget(response, args);
const [request, resolvedArgs] = this.applyRequestArguments(
'attach',
args
);
await this.attachOrLaunchRequest(response, request, resolvedArgs);
} catch (err) {
this.sendErrorResponse(
response,
Expand Down
Loading