Skip to content
134 changes: 106 additions & 28 deletions src/gdb/GDBDebugSessionBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
ObjectVariableReference,
MemoryRequestArguments,
CDTDisassembleArguments,
RequestArgRun,
} from '../types/session';
import { IGDBBackend, IGDBBackendFactory } from '../types/gdb';
import { getInstructions } from '../util/disassembly';
Expand Down Expand Up @@ -154,7 +155,9 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
* (GDBTargetDebugSession) and never for type="gdb" (GDBDebugSession).
*/
protected isRemote = false;
// isRunning === true means there are no threads stopped.
// isRunning === true means there are threads and none are stopped.
// When uncertain because we have not received the status of the newest
// threads yet, this is the last certain value.
protected isRunning = false;

protected supportsRunInTerminalRequest = false;
Expand All @@ -175,6 +178,7 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {

protected updateThreadInfo: 'missing' | 'when-requested' | 'never' =
'missing';
protected runAfterConfiguration: RequestArgRun = RequestArgRun.ALWAYS;

/**
* State variables for pauseIfNeeded/continueIfNeeded logic, mostly used for
Expand Down Expand Up @@ -208,7 +212,9 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
// keeps track of where in the configuration phase (between initialize event
// and configurationDone response) we are
protected configuringState: ConfiguringState = ConfiguringState.INITIAL;
protected isInitialized = false;
protected isInitialized = false; // unused here but kept for compatibility
protected deferredStopEvents: any[] = [];
protected firstContinueIsRun = false;

/**
* customResetCommands from launch.json
Expand Down Expand Up @@ -348,6 +354,16 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
"Valid values are: 'missing', 'when-requested', or 'never'."
);
}
if (
args.run !== undefined &&
args.run !== RequestArgRun.ALWAYS &&
args.run !== RequestArgRun.PRESERVE
) {
throw new Error(
`Invalid value for 'run': '${args.run}'. ` +
"Valid values are: 'always', 'preserve'."
);
}
}

/**
Expand All @@ -361,6 +377,7 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
this.steppingResponseTimeout =
args.steppingResponseTimeout ?? DEFAULT_STEPPING_RESPONSE_TIMEOUT;
this.updateThreadInfo = args.updateThreadInfo ?? 'missing';
this.runAfterConfiguration = args.run ?? RequestArgRun.ALWAYS;
}

/**
Expand Down Expand Up @@ -758,17 +775,35 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
this.waitPaused = undefined;
}

protected setConfiguringStateDone(): void {
this.configuringState = ConfiguringState.DONE;
for (const resultData of this.deferredStopEvents) {
this.handleGDBStopped(resultData);
}
this.deferredStopEvents.splice(0);
}

protected async continueIfNeeded(): Promise<void> {
if (this.pauseCount > 0) {
this.pauseCount--;
if (this.pauseCount === 0) {
if (this.configuringState === ConfiguringState.FINISHING) {
this.configuringState = ConfiguringState.DONE;
if (this.isAttach) {
await mi.sendExecContinue(this.gdb);
} else {
await mi.sendExecRun(this.gdb);
if (
this.runAfterConfiguration == RequestArgRun.ALWAYS ||
this.waitPausedNeeded
) {
if (this.isAttach) {
await mi.sendExecContinue(this.gdb);
} else {
await mi.sendExecRun(this.gdb);
}
} else if (!this.isAttach) {
// We would have sent a 'run' rather than a 'continue',
// but since the user didn't want us to, let them do it
// manually using a continue request.
this.firstContinueIsRun = true;
}
this.setConfiguringStateDone();
} else if (this.waitPausedNeeded) {
if (this.gdb.isNonStopMode()) {
await mi.sendExecContinue(
Expand Down Expand Up @@ -1706,7 +1741,7 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
this.configuringState = ConfiguringState.FINISHING;
await this.continueIfNeeded();
} else {
this.configuringState = ConfiguringState.DONE;
this.setConfiguringStateDone();
}
this.sendResponse(response);
} catch (err) {
Expand All @@ -1725,7 +1760,14 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
name += ` (${thread.details})`;
}

const running = thread.state === 'running';
// 'thread-created' notifications don't include a thread state, it comes
// later via a 'stopped' or 'running' notification.
const running =
thread.state === 'running'
? true
: thread.state === 'stopped'
? false
: undefined;

return new ThreadWithStatus(parseInt(thread.id, 10), name, running);
}
Expand Down Expand Up @@ -2014,7 +2056,12 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
}

try {
await mi.sendExecContinue(this.gdb, args.threadId);
if (this.firstContinueIsRun) {
this.firstContinueIsRun = false;
await mi.sendExecRun(this.gdb);
} else {
await mi.sendExecContinue(this.gdb, args.threadId);
}
let isAllThreadsContinued;
if (this.gdb.isNonStopMode()) {
isAllThreadsContinued = args.threadId ? false : true;
Expand Down Expand Up @@ -2338,15 +2385,16 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
/**
* Send command to backend.
* @param expression Command to be executed
* @returns Promise for: - MI response as JS object if MI command, - `undefined` if CLI command
*/
protected async sendCommandToGdb(
gdb: IGDBBackend,
expression: string,
frameRef: FrameReference | undefined
): Promise<void> {
): Promise<any> {
if (expression.startsWith('-')) {
// GDB/MI command
await gdb.sendCommand(expression);
return await gdb.sendCommand(expression);
} else {
// GDB CLI command
await mi.sendInterpreterExecConsole(gdb, {
Expand Down Expand Up @@ -2397,9 +2445,13 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
frameRef: FrameReference | undefined
): Promise<void> {
const trimmedExpression = expression.trim();
await this.sendCommandToGdb(this.gdb, trimmedExpression, frameRef);
const result = await this.sendCommandToGdb(
this.gdb,
trimmedExpression,
frameRef
);
response.body = {
result: '\r',
result: result ? JSON.stringify(result) : '\r',
variablesReference: 0,
};
await this.sendCommandToOtherGdbs(
Expand Down Expand Up @@ -2455,7 +2507,7 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
response: DebugProtocol.EvaluateResponse,
args: DebugProtocol.EvaluateArguments
): Promise<void> {
return this.doEvaluateRequest(response, args, false);
return this.doEvaluateRequest(response, args, true);
}

private extractExpressionFormat(
Expand Down Expand Up @@ -2499,7 +2551,7 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
protected async doEvaluateRequest(
response: DebugProtocol.EvaluateResponse,
args: DebugProtocol.EvaluateArguments,
alwaysAllowCliCommand: boolean // if true, allows evaluation of expression without a frameId
alwaysAllowCliCommand: boolean // if true, allows evaluation of expression without a frameId (kept for compatibility)
): Promise<void> {
response.body = {
result: 'Error: could not evaluate expression',
Expand Down Expand Up @@ -3148,15 +3200,19 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
return requestsToResolve.length > 0;
}

private updateIsRunning() {
const newIsRunning = this.threads.some((t) => t.running === false) // have stopped
? false
: this.threads.some((t) => t.running === undefined) // have unknown
? undefined
: this.threads.some((t) => t.running === true); // have running
if (newIsRunning !== undefined) {
this.isRunning = newIsRunning;
}
// else leave this.isRunning at its previous known value
}

protected handleGDBAsync(resultClass: string, resultData: any) {
const updateIsRunning = () => {
this.isRunning = this.threads.length ? true : false;
for (const thread of this.threads) {
if (!thread.running) {
this.isRunning = false;
}
}
};
switch (resultClass) {
case 'running':
if (this.gdb.isNonStopMode()) {
Expand All @@ -3172,13 +3228,28 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
thread.running = true;
}
}
updateIsRunning();
if (this.isInitialized) {
this.updateIsRunning();
if (this.configuringState == ConfiguringState.DONE) {
this.handleGDBResume(resultData);
} else {
// while we are deferring events, cancel previous stop events for the same thread
if (this.deferredStopEvents.length > 0) {
if (resultData['thread-id'] === 'all') {
this.deferredStopEvents.splice(0);
} else if (resultData['thread-id'] !== undefined) {
this.deferredStopEvents =
this.deferredStopEvents.filter(
(stopResultData) =>
stopResultData['thread-id'] !==
resultData['thread-id']
);
}
}
}
break;
case 'stopped': {
let suppressHandleGDBStopped = false;
let newThreadsConfirmed = false;
if (this.gdb.isNonStopMode()) {
const id = parseInt(resultData['thread-id'], 10);
for (const thread of this.threads) {
Expand All @@ -3202,6 +3273,9 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
}
} else {
for (const thread of this.threads) {
if (thread.running === undefined) {
newThreadsConfirmed = true;
}
thread.running = false;
thread.lastRunToken = undefined;
}
Expand All @@ -3227,14 +3301,17 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
}

const wasRunning = this.isRunning;
updateIsRunning();
this.updateIsRunning();
if (
!suppressHandleGDBStopped &&
(this.gdb.isNonStopMode() ||
newThreadsConfirmed ||
(wasRunning && !this.isRunning))
) {
if (this.isInitialized) {
if (this.configuringState == ConfiguringState.DONE) {
this.handleGDBStopped(resultData);
} else {
this.deferredStopEvents.push(resultData);
}
}
break;
Expand Down Expand Up @@ -3267,6 +3344,7 @@ export abstract class GDBDebugSessionBase extends LoggingDebugSession {
switch (notifyClass) {
case 'thread-created':
this.threads.push(this.convertThread(notifyData));
this.updateIsRunning();
this.missingThreadNames = true;
break;
case 'thread-exited': {
Expand Down
4 changes: 2 additions & 2 deletions src/gdb/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { DebugProtocol } from '@vscode/debugprotocol';
export class ThreadWithStatus implements DebugProtocol.Thread {
id: number;
name: string;
running: boolean;
running?: boolean;
lastRunToken: string | undefined;
constructor(id: number, name: string, running: boolean) {
constructor(id: number, name: string, running?: boolean) {
this.id = id;
this.name = name;
this.running = running;
Expand Down
Loading
Loading