Skip to content

Commit 9b091dd

Browse files
authored
Fix missing thread names when attaching to a non-stopping target (#439)
* Add tests for thread names after attaching, currently failing Right after attaching to a target that does not stop on attaching, the threads request currently fails to return the correct thread names, because they are not known to the adapter yet and are not retrieved from the target until it stops. The tests are only enabled under those circumstances where the problem can easily be fixed: local non-stop, local all-stop async, remote non-stop. Signed-off-by: Christian Walther <walther@indel.ch> * Fix missing thread names when attaching to a non-stopping target Right after attaching to a target that does not stop on attaching, the threads request would fail to return the correct thread names, because they were not known to the adapter yet and were not retrieved from the target until it stops. At that point, the adapter only knew about the threads from their `thread-created` notifications, which do not include the name. Doing the required -thread-info command not only when the target is stopped, but additionally under those circumstances where it is allowed while the target is running (local non-stop, local all-stop async, remote non-stop), fixes that under those circumstances. Under the remaining circumstances (local all-stop sync, remote all-stop async, remote all-stop sync) the behavior is unmodified, as explicitly stopping the target every time to allow the -thread-info command is probably undesired. This makes the tests from the previous commit pass. Signed-off-by: Christian Walther <walther@indel.ch> --------- Signed-off-by: Christian Walther <walther@indel.ch>
1 parent b892d37 commit 9b091dd

File tree

6 files changed

+125
-1
lines changed

6 files changed

+125
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Change Log
22

3+
## Unreleased
4+
5+
- Fixes [`#439`](https://github.com/eclipse-cdt-cloud/cdt-gdb-adapter/pull/439): missing thread names when attaching to targets that don’t stop on attach.
6+
37
## 1.3.0
48

59
- Implements [`#422`](https://github.com/eclipse-cdt-cloud/cdt-gdb-adapter/issues/422): Initial support for data breakpoints.

src/desktop/GDBTargetDebugSession.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export class GDBTargetDebugSession extends GDBDebugSession {
109109
super(backendFactory || new GDBBackendFactory());
110110
this.gdbserverFactory = gdbserverFactory || new GDBServerFactory();
111111
this.logger = logger;
112+
this.isRemote = true;
112113
}
113114

114115
protected logGDBRemote(message: string, level = LogLevel.Verbose) {

src/gdb/GDBDebugSessionBase.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
123123

124124
protected gdb!: IGDBBackend;
125125
protected isAttach = false;
126+
/**
127+
* True if GDB is using the "remote" or "extended-remote" target. In
128+
* currently supported cases, always for type="gdbtarget"
129+
* (GDBTargetDebugSession) and never for type="gdb" (GDBDebugSession).
130+
*/
131+
protected isRemote = false;
126132
// isRunning === true means there are no threads stopped.
127133
protected isRunning = false;
128134

@@ -138,6 +144,7 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
138144
protected logPointMessages: { [key: string]: string } = {};
139145

140146
protected threads: ThreadWithStatus[] = [];
147+
protected missingThreadNames = false;
141148

142149
// promise that resolves once the target stops so breakpoints can be inserted
143150
protected waitPausedPromise?: Promise<void>;
@@ -1266,11 +1273,29 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
12661273
response: DebugProtocol.ThreadsResponse
12671274
): Promise<void> {
12681275
try {
1269-
if (!this.isRunning) {
1276+
// The -thread-info command is always allowed in non-stop mode or on
1277+
// local targets in async all-stop mode. In sync all-stop mode or
1278+
// over remote connections in all-stop mode, it is only allowed when
1279+
// the target is stopped. GDB docs
1280+
// (https://sourceware.org/gdb/current/onlinedocs/gdb.html/Asynchronous-and-non_002dstop-modes.html)
1281+
// seem to indicate it should also always be allowed in async
1282+
// all-stop mode (irrespective of remote or local), but the source
1283+
// code (remote.c remote_target::putpkt_binary) says otherwise.
1284+
// The effect in situations excluded by this logic is that missing
1285+
// thread names are not filled in until the client issues this
1286+
// request at a time when the target is stopped.
1287+
const threadInfoAllowedWhenRunning =
1288+
(!this.isRemote && this.gdb.getAsyncMode()) ||
1289+
this.gdb.isNonStopMode();
1290+
if (
1291+
!this.isRunning ||
1292+
(this.missingThreadNames && threadInfoAllowedWhenRunning)
1293+
) {
12701294
const result = await mi.sendThreadInfoRequest(this.gdb, {});
12711295
this.threads = result.threads
12721296
.map((thread) => this.convertThread(thread))
12731297
.sort((a, b) => a.id - b.id);
1298+
this.missingThreadNames = false;
12741299
}
12751300

12761301
response.body = {
@@ -2305,6 +2330,7 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
23052330
switch (notifyClass) {
23062331
case 'thread-created':
23072332
this.threads.push(this.convertThread(notifyData));
2333+
this.missingThreadNames = true;
23082334
break;
23092335
case 'thread-exited': {
23102336
const thread: mi.MIThreadInfo = notifyData;

src/integration-tests/attach.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@
1010

1111
import * as cp from 'child_process';
1212
import * as path from 'path';
13+
import * as os from 'os';
1314
import { AttachRequestArguments } from '../types/session';
1415
import { CdtDebugClient } from './debugClient';
1516
import {
1617
fillDefaults,
18+
gdbAsync,
19+
gdbNonStop,
1720
isRemoteTest,
1821
standardBeforeEach,
1922
testProgramsDir,
2023
} from './utils';
2124
import { expect } from 'chai';
25+
import { DebugProtocol } from '@vscode/debugprotocol';
2226

2327
describe('attach', function () {
2428
let dc: CdtDebugClient;
@@ -64,4 +68,54 @@ describe('attach', function () {
6468
await dc.attachHitBreakpoint(attachArgs, { line: 25, path: src });
6569
expect(await dc.evaluate('argv[1]')).to.contain('running-from-spawn');
6670
});
71+
72+
it('can attach to a non-stopping target and has thread names from the beginning', async function () {
73+
if (isRemoteTest) {
74+
// attachRemote.spec.ts is the test for when isRemoteTest
75+
this.skip();
76+
}
77+
if ((!gdbAsync && !gdbNonStop) || os.platform() === 'win32') {
78+
// This functionality is currently only available in async (incl.
79+
// non-stop) mode.
80+
// Windows always belongs in this case because native debugging does
81+
// not support async mode there in GDB < 13, so we are actually in
82+
// sync even when we requested async.
83+
this.skip();
84+
}
85+
86+
const attachArgs = fillDefaults(this.test, {
87+
processId: `${inferior.pid}`,
88+
initCommands: [
89+
'thread name mythreadname',
90+
// Simulate a target that does not stop on attaching, unlike
91+
// what gdbserver does when attaching to a Unix process.
92+
'-exec-continue --all',
93+
],
94+
} as AttachRequestArguments);
95+
96+
await Promise.all([
97+
dc
98+
.waitForEvent('initialized')
99+
.then(() => dc.configurationDoneRequest()),
100+
dc.initializeRequest().then(() => dc.attachRequest(attachArgs)),
101+
]);
102+
103+
try {
104+
const threadsResponse = await dc.threadsRequest();
105+
expect(threadsResponse.success).to.be.true;
106+
expect(threadsResponse.body.threads)
107+
.to.be.an('array')
108+
.that.satisfies((threads: DebugProtocol.Thread[]) =>
109+
threads.some((t) => t.name === 'mythreadname')
110+
);
111+
} finally {
112+
// This is redundant as long as this case is skipped above, but
113+
// becomes necessary if and when that restriction is lifted.
114+
if (!gdbAsync) {
115+
// In sync mode we need to stop the program again, otherwise
116+
// afterEach cannot send any commands and gets stuck.
117+
dc.pauseRequest({ threadId: -1 });
118+
}
119+
}
120+
});
67121
});

src/integration-tests/attachRemote.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import {
2222
gdbServerPath,
2323
gdbAsync,
2424
fillDefaults,
25+
gdbNonStop,
2526
} from './utils';
2627
import { expect } from 'chai';
28+
import { DebugProtocol } from '@vscode/debugprotocol';
2729

2830
describe('attach remote', function () {
2931
let dc: CdtDebugClient;
@@ -146,6 +148,42 @@ describe('attach remote', function () {
146148
expect(await dc.evaluate('argv[1]')).to.contain('running-from-spawn');
147149
});
148150

151+
it('can attach to a non-stopping target and has thread names from the beginning', async function () {
152+
if (!gdbNonStop) {
153+
// Over a remote connection, this functionality is currently only
154+
// available in non-stop mode.
155+
this.skip();
156+
}
157+
const attachArgs = fillDefaults(this.test, {
158+
program: program,
159+
target: {
160+
type: 'remote',
161+
parameters: [`localhost:${port}`],
162+
} as TargetAttachArguments,
163+
initCommands: [
164+
'thread name mythreadname',
165+
// Simulate a target that does not stop on attaching, unlike
166+
// what gdbserver does when attaching to a Unix process.
167+
'-exec-continue --all',
168+
],
169+
} as TargetAttachRequestArguments);
170+
171+
await Promise.all([
172+
dc
173+
.waitForEvent('initialized')
174+
.then(() => dc.configurationDoneRequest()),
175+
dc.initializeRequest().then(() => dc.attachRequest(attachArgs)),
176+
]);
177+
178+
const threadsResponse = await dc.threadsRequest();
179+
expect(threadsResponse.success).to.be.true;
180+
expect(threadsResponse.body.threads)
181+
.to.be.an('array')
182+
.that.satisfies((threads: DebugProtocol.Thread[]) =>
183+
threads.some((t) => t.name === 'mythreadname')
184+
);
185+
});
186+
149187
it('can detach from a running program', async function () {
150188
const attachArgs = fillDefaults(this.test, {
151189
program: program,

src/web/GDBTargetDebugSession.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export class GDBTargetDebugSession extends GDBDebugSession {
9797
super(backendFactory);
9898
this.gdbserverFactory = gdbserverFactory;
9999
this.logger = logger;
100+
this.isRemote = true;
100101
}
101102

102103
protected logGDBRemote(message: string, level = LogLevel.Verbose) {

0 commit comments

Comments
 (0)