Skip to content

Commit 2ceef5b

Browse files
committed
Allow pre-filling launch/attach on command line to adapter
#### `--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. 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 c653a9a commit 2ceef5b

File tree

7 files changed

+313
-12
lines changed

7 files changed

+313
-12
lines changed

README.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,52 @@ Build is pretty simple.
1414
yarn
1515
```
1616

17+
## Running
18+
1719
The entry point for the adapter is `out/debugAdapter.js` for local debugging
18-
and `out/debugTargetAdapter.js` for target (remote) debugging.g
20+
and `out/debugTargetAdapter.js` for target (remote) debugging.
21+
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+
57+
For example, the adapter can be configured for program to be frozen to a specific value.
58+
This may be useful for starting adapters in a container and exposing the server port.
59+
60+
```sh
61+
node debugAdapter.js --server=23221 --config-frozen='{"program":"/path/to/my.elf"}'
62+
```
1963
2064
## Testing
2165

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,15 @@
5656
"dependencies": {
5757
"@vscode/debugadapter": "^1.48.0",
5858
"@vscode/debugprotocol": "^1.48.0",
59-
"node-addon-api": "^4.3.0"
59+
"node-addon-api": "^4.3.0",
60+
"tmp": "^0.2.1"
6061
},
6162
"devDependencies": {
6263
"@types/chai": "^4.1.7",
6364
"@types/chai-string": "^1.4.2",
6465
"@types/mocha": "^9.1.0",
6566
"@types/node": "^14.18.17",
67+
"@types/tmp": "^0.2.3",
6668
"@typescript-eslint/eslint-plugin": "^5.10.1",
6769
"@typescript-eslint/parser": "^5.10.1",
6870
"@vscode/debugadapter-testsupport": "^1.37.1",

src/GDBDebugSession.ts

Lines changed: 86 additions & 0 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,68 @@ 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 type of request to test against the frozen request type
239+
* @param args the arguments from the user to apply initial and frozen arguments to.
240+
* @returns new arguments
241+
* @throws Error if request is not permitted by frozen config
242+
*/
243+
protected applyRequestArguments(
244+
request: 'launch' | 'attach',
245+
args: LaunchRequestArguments | AttachRequestArguments
246+
): LaunchRequestArguments | AttachRequestArguments {
247+
const frozenRequest = GDBDebugSession.frozenRequestArguments?.request;
248+
if (frozenRequest) {
249+
if (frozenRequest !== request) {
250+
const message = `only ${frozenRequest} requests are permitted`;
251+
logger.error(message);
252+
throw new Error(message);
253+
}
254+
}
255+
256+
return {
257+
...GDBDebugSession.defaultRequestArguments,
258+
...args,
259+
...GDBDebugSession.frozenRequestArguments,
260+
};
261+
}
262+
187263
protected createBackend(): GDBBackend {
188264
return new GDBBackend();
189265
}
@@ -247,6 +323,11 @@ export class GDBDebugSession extends LoggingDebugSession {
247323
args: AttachRequestArguments
248324
): Promise<void> {
249325
try {
326+
args = this.applyRequestArguments(
327+
'attach',
328+
args
329+
) as AttachRequestArguments;
330+
250331
logger.setup(
251332
args.verbose ? Logger.LogLevel.Verbose : Logger.LogLevel.Warn,
252333
args.logFile || false
@@ -290,6 +371,11 @@ export class GDBDebugSession extends LoggingDebugSession {
290371
args: LaunchRequestArguments
291372
): Promise<void> {
292373
try {
374+
args = this.applyRequestArguments(
375+
'launch',
376+
args
377+
) as LaunchRequestArguments;
378+
293379
logger.setup(
294380
args.verbose ? Logger.LogLevel.Verbose : Logger.LogLevel.Warn,
295381
args.logFile || false

src/GDBTargetDebugSession.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ export class GDBTargetDebugSession extends GDBDebugSession {
8282
args: TargetLaunchRequestArguments
8383
): Promise<void> {
8484
try {
85+
args = this.applyRequestArguments(
86+
'launch',
87+
args
88+
) as TargetLaunchRequestArguments;
89+
8590
this.setupCommonLoggerAndHandlers(args);
8691
await this.startGDBServer(args);
8792
await this.startGDBAndAttachToTarget(response, args);
@@ -99,6 +104,11 @@ export class GDBTargetDebugSession extends GDBDebugSession {
99104
args: TargetAttachRequestArguments
100105
): Promise<void> {
101106
try {
107+
args = this.applyRequestArguments(
108+
'attach',
109+
args
110+
) as TargetAttachRequestArguments;
111+
102112
this.setupCommonLoggerAndHandlers(args);
103113
await this.startGDBAndAttachToTarget(response, args);
104114
} catch (err) {
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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 { LaunchRequestArguments } from '../GDBDebugSession';
15+
import {
16+
debugServerPort,
17+
defaultAdapter,
18+
fillDefaults,
19+
standardBeforeEach,
20+
testProgramsDir,
21+
} from './utils';
22+
import { expect } from 'chai';
23+
24+
describe('config', function () {
25+
const emptyProgram = path.join(testProgramsDir, 'empty');
26+
const emptySrc = path.join(testProgramsDir, 'empty.c');
27+
28+
async function verifyLaunchWorks(
29+
test: Mocha.Context,
30+
commandLine: string,
31+
requestArgs: LaunchRequestArguments
32+
) {
33+
if (debugServerPort) {
34+
// This test requires launching the adapter to work
35+
test.skip();
36+
}
37+
38+
const dc = await standardBeforeEach(defaultAdapter, commandLine);
39+
40+
try {
41+
await dc.hitBreakpoint(fillDefaults(test.test, requestArgs), {
42+
path: emptySrc,
43+
line: 3,
44+
});
45+
} finally {
46+
await dc.stop();
47+
}
48+
}
49+
50+
it('can specify program via --config=', async function () {
51+
await verifyLaunchWorks(
52+
this,
53+
`'--config={"program":"${emptyProgram}"}'`,
54+
{} as LaunchRequestArguments
55+
);
56+
});
57+
58+
it('program via --config= can be overridden', async function () {
59+
await verifyLaunchWorks(
60+
this,
61+
`'--config={"program":"/program/that/does/not/exist"}'`,
62+
{
63+
program: emptyProgram,
64+
} as LaunchRequestArguments
65+
);
66+
});
67+
68+
it('can specify program via --config-frozen=', async function () {
69+
await verifyLaunchWorks(
70+
this,
71+
`'--config-frozen={"program":"${emptyProgram}"}'`,
72+
{} as LaunchRequestArguments
73+
);
74+
});
75+
76+
it('program via --config-frozen= can not be overridden', async function () {
77+
await verifyLaunchWorks(
78+
this,
79+
`'--config-frozen={"program":"${emptyProgram}"}'`,
80+
{
81+
program: '/program/that/does/not/exist',
82+
} as LaunchRequestArguments
83+
);
84+
});
85+
86+
it('can specify program via --config= using response file', async function () {
87+
const json = `{"program":"${emptyProgram}"}`;
88+
const jsonFile = tmp.fileSync();
89+
fs.writeFileSync(jsonFile.fd, json);
90+
fs.closeSync(jsonFile.fd);
91+
92+
await verifyLaunchWorks(
93+
this,
94+
`'--config=@${jsonFile.name}'`,
95+
{} as LaunchRequestArguments
96+
);
97+
});
98+
99+
it('can specify program via --config-frozen= using response file', async function () {
100+
const json = `{"program":"${emptyProgram}"}`;
101+
const jsonFile = tmp.fileSync();
102+
fs.writeFileSync(jsonFile.fd, json);
103+
fs.closeSync(jsonFile.fd);
104+
105+
await verifyLaunchWorks(
106+
this,
107+
`'--config-frozen=@${jsonFile.name}'`,
108+
{} as LaunchRequestArguments
109+
);
110+
});
111+
112+
it('config frozen can allow correct launch type', async function () {
113+
await verifyLaunchWorks(
114+
this,
115+
`'--config-frozen={"request":"launch"}'`,
116+
{
117+
program: emptyProgram,
118+
} as LaunchRequestArguments
119+
);
120+
});
121+
122+
it('config frozen can prevent incorrect launch type', async function () {
123+
if (debugServerPort) {
124+
// This test requires launching the adapter to work
125+
this.skip();
126+
}
127+
128+
const dc = await standardBeforeEach(
129+
defaultAdapter,
130+
`'--config-frozen={"request":"attach"}'`
131+
);
132+
133+
const errorMessage = await new Promise<Error>((resolve, reject) => {
134+
dc.launchRequest(
135+
fillDefaults(this.test, {
136+
program: emptyProgram,
137+
} as LaunchRequestArguments)
138+
)
139+
.then(reject)
140+
.catch(resolve);
141+
});
142+
143+
expect(errorMessage.message).to.satisfy((msg: string) =>
144+
msg.includes('only attach requests are permitted')
145+
);
146+
});
147+
});

src/integration-tests/utils.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -191,16 +191,16 @@ function getAdapterAndArgs(adapter?: string): string {
191191
}
192192

193193
export async function standardBeforeEach(
194-
adapter?: string
194+
adapter?: string,
195+
extraArgs?: string
195196
): Promise<CdtDebugClient> {
196-
const dc: CdtDebugClient = new CdtDebugClient(
197-
'node',
198-
getAdapterAndArgs(adapter),
199-
'cppdbg',
200-
{
201-
shell: true,
202-
}
203-
);
197+
let args = getAdapterAndArgs(adapter);
198+
if (extraArgs) {
199+
args = `${args} ${extraArgs}`;
200+
}
201+
const dc: CdtDebugClient = new CdtDebugClient('node', args, 'cppdbg', {
202+
shell: true,
203+
});
204204
await dc.start(debugServerPort);
205205
await dc.initializeRequest();
206206

0 commit comments

Comments
 (0)