Skip to content

Commit fced49e

Browse files
authored
Silently pause target before reset if running (#464)
* Adds pauseIfRunning method which does not expect continue after following debug operation. * Call pauseIfRunning before execution of customResetCommands. * Validate gdbAsync setting as required when using customResetCommands. --------- Signed-off-by: Jens Reinecke <[email protected]>
1 parent 0bb3ae7 commit fced49e

File tree

6 files changed

+237
-41
lines changed

6 files changed

+237
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Fixes [`463`](https://github.com/eclipse-cdt-cloud/cdt-gdb-adapter/issues/463): Cannot use custom reset while CPU is running.
56
- Fixes [`465`](https://github.com/eclipse-cdt-cloud/cdt-gdb-adapter/issues/465): UTF-8 'Failed to decode cstring' errors for GDB with CP1252 support only.
67

78
## 1.4.1

src/desktop/GDBTargetDebugSession.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,15 +187,20 @@ export class GDBTargetDebugSession extends GDBDebugSession {
187187
// Limitations for auxiliary GDB mode
188188
if (args.gdbNonStop) {
189189
throw new Error(
190-
'Cannot use auxiliaryGdb mode with gdbNonStop mode'
190+
"Cannot use setting 'auxiliaryGdb' with 'gdbNonStop' mode"
191191
);
192192
}
193193
if (args.gdbAsync === false) {
194194
throw new Error(
195-
'AuxiliaryGdb mode requires gdbAsync to be active'
195+
"Setting 'auxiliaryGdb' mode requires 'gdbAsync' to be active"
196196
);
197197
}
198198
}
199+
if (args.customResetCommands?.length && args.gdbAsync === false) {
200+
throw new Error(
201+
"Setting 'customResetCommands' requires 'gdbAsync' to be active"
202+
);
203+
}
199204
}
200205

201206
protected override async setupCommonLoggerAndBackends(

src/gdb/GDBDebugSessionBase.ts

Lines changed: 104 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ interface StreamOutput {
8585
category: string;
8686
}
8787

88+
// Interface for a pending pause request, used with pauseIfRunning() logic
89+
interface PendingPauseRequest {
90+
threadId: number | undefined; // All threads if undefined
91+
resolveFunc?: (value: void | PromiseLike<void>) => void;
92+
}
93+
8894
export function hexToBase64(hex: string): string {
8995
// The buffer will ignore incomplete bytes (unpaired digits), so we need to catch that early
9096
if (hex.length % 2 !== 0) {
@@ -153,18 +159,27 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
153159
protected threads: ThreadWithStatus[] = [];
154160
protected missingThreadNames = false;
155161

162+
/**
163+
* State variables for pauseIfNeeded/continueIfNeeded logic, mostly used for
164+
* temporary pause to insert breakpoints while the target is running.
165+
*/
156166
// promise that resolves once the target stops so breakpoints can be inserted
157167
protected waitPausedPromise?: Promise<void>;
158168
// resolve function of waitPausedPromise while waiting, undefined otherwise
159169
protected waitPaused?: (value?: void | PromiseLike<void>) => void;
160170
// the thread id that we were waiting for
161171
protected waitPausedThreadId = 0;
162-
// set to true if the target was interrupted where inteneded, and should
172+
// set to true if the target was interrupted where intended, and should
163173
// therefore be resumed after breakpoints are inserted.
164174
protected waitPausedNeeded = false;
165175
// reference count of operations requiring pausing, to make sure only the
166176
// first of them pauses, and the last to complete resumes
167177
protected pauseCount = 0;
178+
179+
// Pending requests to pause a thread silently (without sending stopped event).
180+
// At the moment only used with pauseIfRunning().
181+
protected silentPauseRequests: PendingPauseRequest[] = [];
182+
168183
// keeps track of where in the configuration phase (between initialize event
169184
// and configurationDone response) we are
170185
protected configuringState: ConfiguringState = ConfiguringState.INITIAL;
@@ -282,16 +297,23 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
282297
*/
283298
protected customResetRequest(response: DebugProtocol.Response) {
284299
if (this.customResetCommands) {
285-
this.gdb
286-
.sendCommands(this.customResetCommands)
287-
.then(() => this.sendResponse(response))
288-
.catch(() =>
300+
this.pauseIfRunning()
301+
.then(() => {
302+
// Behavior after reset very much depends on the commands used.
303+
// So, hard to make assumptions when expected state is reached.
304+
// Hence, implement stop-after-reset behavior unless commands
305+
// set running.
306+
this.gdb.sendCommands(this.customResetCommands).then(() => {
307+
this.sendResponse(response);
308+
});
309+
})
310+
.catch(() => {
289311
this.sendErrorResponse(
290312
response,
291313
1,
292314
'The custom reset command failed'
293-
)
294-
);
315+
);
316+
});
295317
}
296318
}
297319

@@ -628,6 +650,43 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
628650
}
629651
}
630652

653+
/**
654+
* Pause thread if running and suppress stopped event.
655+
* In contrast to pauseIfNeeded(), there is no expectation to resume after a debugger operation.
656+
*
657+
* Notes:
658+
* - NOT to be called during configuration phase.
659+
* - No requireAsync, supposed to be removed from pauseIfNeeded in future.
660+
*/
661+
protected async pauseIfRunning(): Promise<void> {
662+
// Check if running before actual wait promise
663+
if (!this.isRunning) {
664+
// At least one thread is already stopped, good enough
665+
// to execute custom reset commands.
666+
return;
667+
}
668+
return new Promise<void>(async (resolve) => {
669+
// Get current thread ID in non-stop mode
670+
const currentThreadId = this.gdb.isNonStopMode()
671+
? await this.gdb.queryCurrentThreadId()
672+
: undefined;
673+
// Push pause request to be handled silently when stop event arrives.
674+
// threadId = undefined means all threads.
675+
// Note: threadId usage matches continueIfNeeded() behavior.
676+
this.silentPauseRequests.push({
677+
threadId: currentThreadId,
678+
resolveFunc: resolve,
679+
});
680+
if (this.gdb.isNonStopMode()) {
681+
// Send pause command to any thread (use current)
682+
this.gdb.pause(currentThreadId);
683+
} else {
684+
this.gdb.pause();
685+
}
686+
// The promise resolves when pushed resolveFunc gets called.
687+
});
688+
}
689+
631690
protected async dataBreakpointInfoRequest(
632691
response: DebugProtocol.DataBreakpointInfoResponse,
633692
args: DebugProtocol.DataBreakpointInfoArguments
@@ -2402,6 +2461,24 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
24022461
return this.sendContinuedEvent(getThreadId(result));
24032462
}
24042463

2464+
private handleSilentPauseRequests(threadId?: number): boolean {
2465+
// Resolve requests with exact threadId match.
2466+
// Note: Expecting request with threadId=undefined gets satisfied
2467+
// by handling a stopped event for allThreads. If this doesn't hold,
2468+
// below conditions should be changed to resolve all threads, i.e.
2469+
// also those with a defined threadId.
2470+
const requestsToResolve = this.silentPauseRequests.filter(
2471+
(request) => request.threadId === threadId
2472+
);
2473+
this.silentPauseRequests = this.silentPauseRequests.filter(
2474+
(request) => request.threadId !== threadId
2475+
);
2476+
requestsToResolve.forEach((request) => {
2477+
request.resolveFunc?.();
2478+
});
2479+
return requestsToResolve.length > 0;
2480+
}
2481+
24052482
protected handleGDBAsync(resultClass: string, resultData: any) {
24062483
const updateIsRunning = () => {
24072484
this.isRunning = this.threads.length ? true : false;
@@ -2441,24 +2518,32 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
24412518
thread.lastRunToken = undefined;
24422519
}
24432520
}
2444-
if (
2445-
this.waitPaused &&
2446-
resultData.reason === 'signal-received' &&
2447-
(this.waitPausedThreadId === id ||
2448-
this.waitPausedThreadId === -1)
2449-
) {
2450-
suppressHandleGDBStopped = true;
2521+
if (resultData.reason === 'signal-received') {
2522+
if (
2523+
this.waitPaused &&
2524+
(this.waitPausedThreadId === id ||
2525+
this.waitPausedThreadId === -1)
2526+
) {
2527+
suppressHandleGDBStopped = true;
2528+
}
2529+
// Handle separately to avoid short-circuiting effects
2530+
if (this.handleSilentPauseRequests(id)) {
2531+
suppressHandleGDBStopped = true;
2532+
}
24512533
}
24522534
} else {
24532535
for (const thread of this.threads) {
24542536
thread.running = false;
24552537
thread.lastRunToken = undefined;
24562538
}
2457-
if (
2458-
this.waitPaused &&
2459-
resultData.reason === 'signal-received'
2460-
) {
2461-
suppressHandleGDBStopped = true;
2539+
if (resultData.reason === 'signal-received') {
2540+
if (this.waitPaused) {
2541+
suppressHandleGDBStopped = true;
2542+
}
2543+
// Handle separately to avoid short-circuiting effects
2544+
if (this.handleSilentPauseRequests()) {
2545+
suppressHandleGDBStopped = true;
2546+
}
24622547
}
24632548
}
24642549

src/integration-tests/auxiliaryGdb.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ describe('auxiliary gdb configuration', function () {
5757
if (expectToFail) {
5858
// Expecting launch to fail, check for correct error message
5959
const expectedErrorMessage = gdbNonStop
60-
? 'Cannot use auxiliaryGdb mode with gdbNonStop mode'
61-
: 'AuxiliaryGdb mode requires gdbAsync to be active';
60+
? "Cannot use setting 'auxiliaryGdb' with 'gdbNonStop' mode"
61+
: "Setting 'auxiliaryGdb' mode requires 'gdbAsync' to be active";
6262
const rejectError = await expectRejection(
6363
dc.launchRequest(launchArgs)
6464
);

src/integration-tests/custom-reset.spec.ts

Lines changed: 116 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,41 +12,141 @@ import * as path from 'path';
1212
import { TargetLaunchRequestArguments } from '../types/session';
1313
import { CdtDebugClient } from './debugClient';
1414
import {
15+
expectRejection,
1516
fillDefaults,
17+
gdbAsync,
1618
isRemoteTest,
1719
standardBeforeEach,
1820
testProgramsDir,
1921
} from './utils';
22+
import { DebugProtocol } from '@vscode/debugprotocol';
23+
import { Runnable } from 'mocha';
24+
import { expect, use } from 'chai';
25+
import * as chaistring from 'chai-string';
26+
use(chaistring);
27+
28+
const gdbtargetAdapter = 'debugTargetAdapter.js';
29+
const loopForeverProgram = path.join(testProgramsDir, 'loopforever');
30+
const commands = ['print 42'];
31+
const expectedResult = '$1 = 42\n';
32+
const customResetCommandsUnsupported = gdbAsync === false || !isRemoteTest;
33+
34+
describe('custom reset configuration', function () {
35+
let dc: CdtDebugClient;
36+
37+
beforeEach(async function () {
38+
dc = await standardBeforeEach(gdbtargetAdapter);
39+
});
40+
41+
afterEach(async function () {
42+
if (dc) {
43+
await dc.stop();
44+
}
45+
});
46+
47+
const testConnect = async (
48+
launchArgs: TargetLaunchRequestArguments,
49+
expectToFail: boolean
50+
) => {
51+
if (expectToFail) {
52+
// Expecting launch to fail, check for correct error message
53+
const expectedErrorMessage =
54+
"Setting 'customResetCommands' requires 'gdbAsync' to be active";
55+
const rejectError = await expectRejection(
56+
dc.launchRequest(launchArgs)
57+
);
58+
expect(rejectError.message).to.startWith(expectedErrorMessage);
59+
} else {
60+
// Expecting launch to succeed
61+
const launchResponse = (await dc.launchRequest(
62+
launchArgs
63+
)) as DebugProtocol.LaunchResponse;
64+
expect(launchResponse.success).to.be.true;
65+
}
66+
};
67+
68+
it('correctly validates if auxiliary gdb mode can work with other settings', async function () {
69+
if (!isRemoteTest) {
70+
// Only skip remote tests, gdbAsync validation tested here
71+
this.skip();
72+
}
73+
74+
const launchArgs = fillDefaults(this.test, {
75+
program: loopForeverProgram,
76+
customResetCommands: commands,
77+
} as TargetLaunchRequestArguments);
78+
79+
await testConnect(launchArgs, customResetCommandsUnsupported);
80+
});
81+
});
2082

2183
describe('custom reset', function () {
2284
let dc: CdtDebugClient;
23-
const emptyProgram = path.join(testProgramsDir, 'empty');
24-
const commands = ['print 42'];
25-
const expectedResult = '$1 = 42\n';
2685

2786
beforeEach(async function () {
28-
dc = await standardBeforeEach('debugTargetAdapter.js');
29-
await dc.launchRequest(
30-
fillDefaults(this.currentTest, {
31-
program: emptyProgram,
32-
customResetCommands: commands,
33-
} as TargetLaunchRequestArguments)
34-
);
87+
dc = await standardBeforeEach(gdbtargetAdapter);
3588
});
3689

3790
afterEach(async function () {
3891
await dc.stop();
3992
});
4093

94+
const completeStartup = async function (
95+
testContext?: Runnable
96+
): Promise<void> {
97+
// Call here instead of beforeEach so that test can be skipped without
98+
// failing due to argument validation.
99+
await dc.launchRequest(
100+
fillDefaults(testContext, {
101+
program: loopForeverProgram,
102+
customResetCommands: commands,
103+
} as TargetLaunchRequestArguments)
104+
);
105+
};
106+
41107
it('tests sending custom reset commands', async function () {
42-
if (!isRemoteTest) {
43-
// command is implemented in the remote adapter but not in the local adapter
44-
// so skip this test if not running remote
108+
if (customResetCommandsUnsupported) {
109+
// Command is implemented in GDBDebugSessionBase but deliberately documented
110+
// for gdbtarget (remote) adapter only. So skip this test if not running remote
45111
this.skip();
46112
}
47113

48-
const event = dc.waitForOutputEvent('stdout', expectedResult);
49-
await dc.customRequest('cdt-gdb-adapter/customReset');
50-
await event;
114+
await completeStartup(this.test);
115+
116+
await Promise.all([
117+
dc.waitForOutputEvent('stdout', expectedResult),
118+
dc.customRequest('cdt-gdb-adapter/customReset'),
119+
]);
120+
});
121+
122+
it('stops the target if necessary before sending custom reset commands', async function () {
123+
if (customResetCommandsUnsupported) {
124+
// Command is implemented in GDBDebugSessionBase but deliberately documented
125+
// for gdbtarget (remote) adapter only. So skip this test if not running remote.
126+
// Skip if not gdbAsync, pauseIfNeeded will otherwise hang in when fetching `$_gthread`.
127+
this.skip();
128+
}
129+
130+
await completeStartup(this.test);
131+
132+
await dc.setFunctionBreakpointsRequest({
133+
breakpoints: [{ name: 'main' }],
134+
});
135+
const [stoppedEvent] = await Promise.all([
136+
dc.waitForEvent('stopped'),
137+
dc.configurationDoneRequest(),
138+
]);
139+
await dc.setFunctionBreakpointsRequest({ breakpoints: [] }); // remove function breakpoints
140+
141+
// Let the program run
142+
await dc.continueRequest({ threadId: stoppedEvent.body.threadId });
143+
144+
await Promise.all([
145+
dc.waitForOutputEvent('stdout', expectedResult), // wait stdout event
146+
dc.customRequest('cdt-gdb-adapter/customReset'),
147+
]);
148+
149+
// Would throw if it wasn't stopped
150+
await dc.stepInRequest({ threadId: stoppedEvent.body.threadId });
51151
});
52152
});

0 commit comments

Comments
 (0)