Skip to content

Commit 5551015

Browse files
committed
Allow pre-filling launch/attach on command line to adapter
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
1 parent 9b503f4 commit 5551015

File tree

6 files changed

+320
-6
lines changed

6 files changed

+320
-6
lines changed

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,54 @@ Build is pretty simple.
1414
yarn
1515
```
1616

17+
## Running
18+
1719
The entry point for the adapter is `cdtDebugAdapter` for local debugging
1820
and `cdtDebugTargetAdapter` for target (remote) debugging.
1921

22+
### Command line arguments
23+
24+
#### `--server=PORT`
25+
26+
Start the adapter listening on the given port instead of on stdin/stdout.
27+
28+
#### `--config=INITIALCONFIG`
29+
30+
Start the adapter using the given configuration as a starting point for the args in `launch` or `attach` request.
31+
32+
For example, the default GDB can be set like this:
33+
34+
```sh
35+
node debugTargetAdapter.js --config='{"gdb":"arm-none-eabi-gdb"}'
36+
```
37+
38+
The config can be passed on the command line as JSON, or a response file can be used by starting the argument with `@`.
39+
The rest of the argument will be interpreted as a file name to read.
40+
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:
41+
42+
```sh
43+
cat >config.json <<END
44+
{
45+
"processId": 1234
46+
}
47+
END
48+
node debugAdapter.js [email protected]
49+
50+
```
51+
52+
#### `--config-frozen=FROZENCONFIG`
53+
54+
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.
55+
Specifying which type of request is allowed (`launch` or `attach`) can be specified with the `request` field.
56+
When freezing the type of request, regardless of which type of request the user requested, the frozen request type will be used.
57+
58+
For example, the adapter can be configured for program to be frozen to a specific value.
59+
This may be useful for starting adapters in a container and exposing the server port.
60+
61+
```sh
62+
node debugAdapter.js --server=23221 --config-frozen='{"program":"/path/to/my.elf"}'
63+
```
64+
2065
## Testing
2166
2267
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)

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,15 @@
6060
"dependencies": {
6161
"@vscode/debugadapter": "^1.48.0",
6262
"@vscode/debugprotocol": "^1.48.0",
63-
"node-addon-api": "^4.3.0"
63+
"node-addon-api": "^4.3.0",
64+
"tmp": "^0.2.1"
6465
},
6566
"devDependencies": {
6667
"@types/chai": "^4.1.7",
6768
"@types/chai-string": "^1.4.2",
6869
"@types/mocha": "^9.1.0",
6970
"@types/node": "^14.18.17",
71+
"@types/tmp": "^0.2.3",
7072
"@typescript-eslint/eslint-plugin": "^5.10.1",
7173
"@typescript-eslint/parser": "^5.10.1",
7274
"@vscode/debugadapter-testsupport": "^1.37.1",

src/GDBDebugSession.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
*********************************************************************/
1010
import * as os from 'os';
1111
import * as path from 'path';
12+
import * as fs from 'fs';
1213
import {
14+
DebugSession,
1315
Handles,
1416
InitializedEvent,
1517
Logger,
@@ -152,6 +154,18 @@ export function base64ToHex(base64: string): string {
152154
}
153155

154156
export class GDBDebugSession extends LoggingDebugSession {
157+
/**
158+
* Initial (aka default) configuration for launch/attach request
159+
* typically supplied with the --config command line argument.
160+
*/
161+
protected static defaultRequestArguments?: any;
162+
163+
/**
164+
* Frozen configuration for launch/attach request
165+
* typically supplied with the --config-frozen command line argument.
166+
*/
167+
protected static frozenRequestArguments?: { request?: string };
168+
155169
protected gdb: GDBBackend = this.createBackend();
156170
protected isAttach = false;
157171
// isRunning === true means there are no threads stopped.
@@ -184,6 +198,66 @@ export class GDBDebugSession extends LoggingDebugSession {
184198
this.logger = logger;
185199
}
186200

201+
/**
202+
* Main entry point
203+
*/
204+
public static run(debugSession: typeof GDBDebugSession) {
205+
GDBDebugSession.processArgv(process.argv.slice(2));
206+
DebugSession.run(debugSession);
207+
}
208+
209+
/**
210+
* Parse an optional config file which is a JSON string of launch/attach request arguments.
211+
* The config can be a response file by starting with an @.
212+
*/
213+
public static processArgv(args: string[]) {
214+
args.forEach(function (val, _index, _array) {
215+
const configMatch = /^--config(-frozen)?=(.*)$/.exec(val);
216+
if (configMatch) {
217+
let configJson;
218+
const configStr = configMatch[2];
219+
if (configStr.startsWith('@')) {
220+
const configFile = configStr.slice(1);
221+
configJson = JSON.parse(
222+
fs.readFileSync(configFile).toString('utf8')
223+
);
224+
} else {
225+
configJson = JSON.parse(configStr);
226+
}
227+
if (configMatch[1]) {
228+
GDBDebugSession.frozenRequestArguments = configJson;
229+
} else {
230+
GDBDebugSession.defaultRequestArguments = configJson;
231+
}
232+
}
233+
});
234+
}
235+
236+
/**
237+
* Apply the initial and frozen launch/attach request arguments.
238+
* @param request the default request type to return if request type is not frozen
239+
* @param args the arguments from the user to apply initial and frozen arguments to.
240+
* @returns resolved request type and the resolved arguments
241+
*/
242+
protected applyRequestArguments(
243+
request: 'launch' | 'attach',
244+
args: LaunchRequestArguments | AttachRequestArguments
245+
): ['launch' | 'attach', LaunchRequestArguments | AttachRequestArguments] {
246+
const frozenRequest = GDBDebugSession.frozenRequestArguments?.request;
247+
if (frozenRequest === 'launch' || frozenRequest === 'attach') {
248+
request = frozenRequest;
249+
}
250+
251+
return [
252+
request,
253+
{
254+
...GDBDebugSession.defaultRequestArguments,
255+
...args,
256+
...GDBDebugSession.frozenRequestArguments,
257+
},
258+
];
259+
}
260+
187261
protected createBackend(): GDBBackend {
188262
return new GDBBackend();
189263
}
@@ -305,7 +379,11 @@ export class GDBDebugSession extends LoggingDebugSession {
305379
args: AttachRequestArguments
306380
): Promise<void> {
307381
try {
308-
await this.attachOrLaunchRequest(response, 'attach', args);
382+
const [request, resolvedArgs] = this.applyRequestArguments(
383+
'attach',
384+
args
385+
);
386+
await this.attachOrLaunchRequest(response, request, resolvedArgs);
309387
} catch (err) {
310388
this.sendErrorResponse(
311389
response,
@@ -320,7 +398,11 @@ export class GDBDebugSession extends LoggingDebugSession {
320398
args: LaunchRequestArguments
321399
): Promise<void> {
322400
try {
323-
await this.attachOrLaunchRequest(response, 'launch', args);
401+
const [request, resolvedArgs] = this.applyRequestArguments(
402+
'launch',
403+
args
404+
);
405+
await this.attachOrLaunchRequest(response, request, resolvedArgs);
324406
} catch (err) {
325407
this.sendErrorResponse(
326408
response,

src/GDBTargetDebugSession.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,11 @@ export class GDBTargetDebugSession extends GDBDebugSession {
108108
args: TargetLaunchRequestArguments
109109
): Promise<void> {
110110
try {
111-
await this.attachOrLaunchRequest(response, 'launch', args);
111+
const [request, resolvedArgs] = this.applyRequestArguments(
112+
'launch',
113+
args
114+
);
115+
await this.attachOrLaunchRequest(response, request, resolvedArgs);
112116
} catch (err) {
113117
this.sendErrorResponse(
114118
response,
@@ -123,7 +127,11 @@ export class GDBTargetDebugSession extends GDBDebugSession {
123127
args: TargetAttachRequestArguments
124128
): Promise<void> {
125129
try {
126-
await this.attachOrLaunchRequest(response, 'attach', args);
130+
const [request, resolvedArgs] = this.applyRequestArguments(
131+
'attach',
132+
args
133+
);
134+
await this.attachOrLaunchRequest(response, request, resolvedArgs);
127135
} catch (err) {
128136
this.sendErrorResponse(
129137
response,
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*********************************************************************
2+
* Copyright (c) 2023 Kichwa Coders Canada Inc. and others.
3+
*
4+
* This program and the accompanying materials are made
5+
* available under the terms of the Eclipse Public License 2.0
6+
* which is available at https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*********************************************************************/
10+
11+
import * as path from 'path';
12+
import * as tmp from 'tmp';
13+
import * as fs from 'fs';
14+
import {
15+
LaunchRequestArguments,
16+
AttachRequestArguments,
17+
} from '../GDBDebugSession';
18+
import {
19+
debugServerPort,
20+
defaultAdapter,
21+
fillDefaults,
22+
standardBeforeEach,
23+
testProgramsDir,
24+
} from './utils';
25+
26+
describe('config', function () {
27+
const emptyProgram = path.join(testProgramsDir, 'empty');
28+
const emptySrc = path.join(testProgramsDir, 'empty.c');
29+
30+
async function verifyLaunchWorks(
31+
test: Mocha.Context,
32+
commandLine: string[],
33+
requestArgs: LaunchRequestArguments
34+
) {
35+
if (debugServerPort) {
36+
// This test requires launching the adapter to work
37+
test.skip();
38+
}
39+
40+
const dc = await standardBeforeEach(defaultAdapter, commandLine);
41+
42+
try {
43+
await dc.hitBreakpoint(fillDefaults(test.test, requestArgs), {
44+
path: emptySrc,
45+
line: 3,
46+
});
47+
} finally {
48+
await dc.stop();
49+
}
50+
}
51+
52+
it('can specify program via --config=', async function () {
53+
const config = { program: emptyProgram };
54+
await verifyLaunchWorks(
55+
this,
56+
[`--config=${JSON.stringify(config)}`],
57+
{} as LaunchRequestArguments
58+
);
59+
});
60+
61+
it('program via --config= can be overridden', async function () {
62+
const config = { program: '/program/that/does/not/exist' };
63+
await verifyLaunchWorks(this, [`--config=${JSON.stringify(config)}`], {
64+
program: emptyProgram,
65+
} as LaunchRequestArguments);
66+
});
67+
68+
it('can specify program via --config-frozen=', async function () {
69+
const config = { program: emptyProgram };
70+
await verifyLaunchWorks(
71+
this,
72+
[`--config-frozen=${JSON.stringify(config)}`],
73+
{} as LaunchRequestArguments
74+
);
75+
});
76+
77+
it('program via --config-frozen= can not be overridden', async function () {
78+
const config = { program: emptyProgram };
79+
await verifyLaunchWorks(
80+
this,
81+
[`--config-frozen=${JSON.stringify(config)}`],
82+
{
83+
program: '/program/that/does/not/exist',
84+
} as LaunchRequestArguments
85+
);
86+
});
87+
88+
it('can specify program via --config= using response file', async function () {
89+
const config = { program: emptyProgram };
90+
const json = JSON.stringify(config);
91+
const jsonFile = tmp.fileSync();
92+
fs.writeFileSync(jsonFile.fd, json);
93+
fs.closeSync(jsonFile.fd);
94+
95+
await verifyLaunchWorks(
96+
this,
97+
[`--config=@${jsonFile.name}`],
98+
{} as LaunchRequestArguments
99+
);
100+
});
101+
102+
it('can specify program via --config-frozen= using response file', async function () {
103+
const config = { program: emptyProgram };
104+
const json = JSON.stringify(config);
105+
const jsonFile = tmp.fileSync();
106+
fs.writeFileSync(jsonFile.fd, json);
107+
fs.closeSync(jsonFile.fd);
108+
109+
await verifyLaunchWorks(
110+
this,
111+
[`--config-frozen=@${jsonFile.name}`],
112+
{} as LaunchRequestArguments
113+
);
114+
});
115+
116+
// This test most closely models the original design goal
117+
// for the change that added --config and --config-frozen
118+
// as discussed in #227 and #228
119+
// In summary we force a launch request for the given program,
120+
// but the user does not specify the program and specifies
121+
// an attach request
122+
it('config frozen forces specific launch type', async function () {
123+
if (debugServerPort) {
124+
// This test requires launching the adapter to work
125+
this.skip();
126+
}
127+
128+
const config = { request: 'launch', program: emptyProgram };
129+
130+
// Launch the adapter with the frozen config
131+
const dc = await standardBeforeEach(defaultAdapter, [
132+
`--config-frozen=${JSON.stringify(config)}`,
133+
]);
134+
135+
try {
136+
await Promise.all([
137+
// Do an attach request omitting the program that we want
138+
// the adapter to force into a launch request
139+
dc.attachRequest(
140+
fillDefaults(this.test, {} as AttachRequestArguments)
141+
),
142+
143+
// The rest of this code is to ensure we launcher properly by verifying
144+
// we can run to a breakpoint
145+
dc.waitForEvent('initialized').then((_event) => {
146+
return dc
147+
.setBreakpointsRequest({
148+
lines: [3],
149+
breakpoints: [{ line: 3 }],
150+
source: { path: emptySrc },
151+
})
152+
.then((_response) => {
153+
return dc.configurationDoneRequest();
154+
});
155+
}),
156+
dc.assertStoppedLocation('breakpoint', {
157+
path: emptySrc,
158+
line: 3,
159+
}),
160+
]);
161+
} finally {
162+
await dc.stop();
163+
}
164+
});
165+
});

0 commit comments

Comments
 (0)