Skip to content

Commit

Permalink
Allow pre-filling launch/attach on command line to adapter
Browse files Browse the repository at this point in the history
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]

```

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.

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"}'
```

Fixes #227
  • Loading branch information
jonahgraham committed Jan 26, 2023
1 parent 419e439 commit 7e2014f
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 6 deletions.
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
86 changes: 84 additions & 2 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;
if (frozenRequest === 'launch' || frozenRequest === 'attach') {
request = frozenRequest;
}

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

protected createBackend(): GDBBackend {
return new GDBBackend();
}
Expand Down Expand Up @@ -305,7 +379,11 @@ export class GDBDebugSession extends LoggingDebugSession {
args: AttachRequestArguments
): Promise<void> {
try {
await this.attachOrLaunchRequest(response, 'attach', args);
const [request, resolvedArgs] = this.applyRequestArguments(
'attach',
args
);
await this.attachOrLaunchRequest(response, request, resolvedArgs);
} catch (err) {
this.sendErrorResponse(
response,
Expand All @@ -320,7 +398,11 @@ export class GDBDebugSession extends LoggingDebugSession {
args: LaunchRequestArguments
): Promise<void> {
try {
await this.attachOrLaunchRequest(response, 'launch', args);
const [request, resolvedArgs] = this.applyRequestArguments(
'launch',
args
);
await this.attachOrLaunchRequest(response, request, resolvedArgs);
} catch (err) {
this.sendErrorResponse(
response,
Expand Down
12 changes: 10 additions & 2 deletions src/GDBTargetDebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ export class GDBTargetDebugSession extends GDBDebugSession {
args: TargetLaunchRequestArguments
): Promise<void> {
try {
await this.attachOrLaunchRequest(response, 'launch', args);
const [request, resolvedArgs] = this.applyRequestArguments(
'launch',
args
);
await this.attachOrLaunchRequest(response, request, resolvedArgs);
} catch (err) {
this.sendErrorResponse(
response,
Expand All @@ -123,7 +127,11 @@ export class GDBTargetDebugSession extends GDBDebugSession {
args: TargetAttachRequestArguments
): Promise<void> {
try {
await this.attachOrLaunchRequest(response, 'attach', args);
const [request, resolvedArgs] = this.applyRequestArguments(
'attach',
args
);
await this.attachOrLaunchRequest(response, request, resolvedArgs);
} catch (err) {
this.sendErrorResponse(
response,
Expand Down
165 changes: 165 additions & 0 deletions src/integration-tests/config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*********************************************************************
* Copyright (c) 2023 Kichwa Coders Canada Inc. and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*********************************************************************/

import * as path from 'path';
import * as tmp from 'tmp';
import * as fs from 'fs';
import {
LaunchRequestArguments,
AttachRequestArguments,
} from '../GDBDebugSession';
import {
debugServerPort,
defaultAdapter,
fillDefaults,
standardBeforeEach,
testProgramsDir,
} from './utils';

describe('config', function () {
const emptyProgram = path.join(testProgramsDir, 'empty');
const emptySrc = path.join(testProgramsDir, 'empty.c');

async function verifyLaunchWorks(
test: Mocha.Context,
commandLine: string[],
requestArgs: LaunchRequestArguments
) {
if (debugServerPort) {
// This test requires launching the adapter to work
test.skip();
}

const dc = await standardBeforeEach(defaultAdapter, commandLine);

try {
await dc.hitBreakpoint(fillDefaults(test.test, requestArgs), {
path: emptySrc,
line: 3,
});
} finally {
await dc.stop();
}
}

it('can specify program via --config=', async function () {
const config = { program: emptyProgram };
await verifyLaunchWorks(
this,
[`--config=${JSON.stringify(config)}`],
{} as LaunchRequestArguments
);
});

it('program via --config= can be overridden', async function () {
const config = { program: '/program/that/does/not/exist' };
await verifyLaunchWorks(this, [`--config=${JSON.stringify(config)}`], {
program: emptyProgram,
} as LaunchRequestArguments);
});

it('can specify program via --config-frozen=', async function () {
const config = { program: emptyProgram };
await verifyLaunchWorks(
this,
[`--config-frozen=${JSON.stringify(config)}`],
{} as LaunchRequestArguments
);
});

it('program via --config-frozen= can not be overridden', async function () {
const config = { program: emptyProgram };
await verifyLaunchWorks(
this,
[`--config-frozen=${JSON.stringify(config)}`],
{
program: '/program/that/does/not/exist',
} as LaunchRequestArguments
);
});

it('can specify program via --config= using response file', async function () {
const config = { program: emptyProgram };
const json = JSON.stringify(config);
const jsonFile = tmp.fileSync();
fs.writeFileSync(jsonFile.fd, json);
fs.closeSync(jsonFile.fd);

await verifyLaunchWorks(
this,
[`--config=@${jsonFile.name}`],
{} as LaunchRequestArguments
);
});

it('can specify program via --config-frozen= using response file', async function () {
const config = { program: emptyProgram };
const json = JSON.stringify(config);
const jsonFile = tmp.fileSync();
fs.writeFileSync(jsonFile.fd, json);
fs.closeSync(jsonFile.fd);

await verifyLaunchWorks(
this,
[`--config-frozen=@${jsonFile.name}`],
{} as LaunchRequestArguments
);
});

// This test most closely models the original design goal
// for the change that added --config and --config-frozen
// as discussed in #227 and #228
// In summary we force a launch request for the given program,
// but the user does not specify the program and specifies
// an attach request
it('config frozen forces specific launch type', async function () {
if (debugServerPort) {
// This test requires launching the adapter to work
this.skip();
}

const config = { request: 'launch', program: emptyProgram };

// Launch the adapter with the frozen config
const dc = await standardBeforeEach(defaultAdapter, [
`--config-frozen=${JSON.stringify(config)}`,
]);

try {
await Promise.all([
// Do an attach request omitting the program that we want
// the adapter to force into a launch request
dc.attachRequest(
fillDefaults(this.test, {} as AttachRequestArguments)
),

// The rest of this code is to ensure we launcher properly by verifying
// we can run to a breakpoint
dc.waitForEvent('initialized').then((_event) => {
return dc
.setBreakpointsRequest({
lines: [3],
breakpoints: [{ line: 3 }],
source: { path: emptySrc },
})
.then((_response) => {
return dc.configurationDoneRequest();
});
}),
dc.assertStoppedLocation('breakpoint', {
path: emptySrc,
line: 3,
}),
]);
} finally {
await dc.stop();
}
});
});
Loading

0 comments on commit 7e2014f

Please sign in to comment.