Skip to content

Commit 6c7bfb3

Browse files
committed
src/debugAdapter: report that next is automatically cancelled if interrupted
In order to set breakpoints, a running program must be halted. When a next, step in, or step out request is interrupted by a halt, the request is automatically cancelled. Unlike the case when continue is interrupted, we should not suppress a stop event and automatically continue. Instead we issue a stopped event and log a warning to stderr. Test that the debug adapter is able to set breakpoint requests while the program is running continue, next, and step out requests. Updates #787 Change-Id: I1b17a65d15fd35c628b1fa2d823c558a24a84727 Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/261078 Trust: Suzy Mueller <suzmue@golang.org> Run-TryBot: Suzy Mueller <suzmue@golang.org> TryBot-Result: kokoro <noreply+kokoro@google.com> Reviewed-by: Polina Sokolova <polina@google.com>
1 parent f38e348 commit 6c7bfb3

File tree

2 files changed

+160
-1
lines changed

2 files changed

+160
-1
lines changed

src/debugAdapter/goDebug.ts

+49-1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ interface DebuggerState {
9090
currentGoroutine: DebugGoroutine;
9191
Running: boolean;
9292
Threads: DebugThread[];
93+
NextInProgress: boolean;
9394
}
9495

9596
export interface PackageBuildInfo {
@@ -857,6 +858,7 @@ export class GoDebugSession extends LoggingDebugSession {
857858
private breakpoints: Map<string, DebugBreakpoint[]>;
858859
// Editing breakpoints requires halting delve, skip sending Stop Event to VS Code in such cases
859860
private skipStopEventOnce: boolean;
861+
private overrideStopReason: string;
860862
private debugState: DebuggerState;
861863
private delve: Delve;
862864
private localPathSeparator: string;
@@ -877,13 +879,16 @@ export class GoDebugSession extends LoggingDebugSession {
877879

878880
private continueEpoch = 0;
879881
private continueRequestRunning = false;
882+
private nextEpoch = 0;
883+
private nextRequestRunning = false;
880884
public constructor(
881885
debuggerLinesStartAt1: boolean,
882886
isServer: boolean = false,
883887
readonly fileSystem = fs) {
884888
super('', debuggerLinesStartAt1, isServer);
885889
this.variableHandles = new Handles<DebugVariable>();
886890
this.skipStopEventOnce = false;
891+
this.overrideStopReason = '';
887892
this.stopOnEntry = false;
888893
this.debugState = null;
889894
this.delve = null;
@@ -1303,11 +1308,25 @@ export class GoDebugSession extends LoggingDebugSession {
13031308
log('Debuggee is not running. Setting breakpoints without halting.');
13041309
await this.setBreakPoints(response, args);
13051310
} else {
1311+
// Skip stop event if a continue request is running.
13061312
this.skipStopEventOnce = this.continueRequestRunning;
1313+
const haltedDuringNext = this.nextRequestRunning;
1314+
if (haltedDuringNext) {
1315+
this.overrideStopReason = 'next cancelled';
1316+
}
1317+
13071318
log(`Halting before setting breakpoints. SkipStopEventOnce is ${this.skipStopEventOnce}.`);
13081319
this.delve.callPromise('Command', [{ name: 'halt' }]).then(
13091320
() => {
13101321
return this.setBreakPoints(response, args).then(() => {
1322+
// We do not want to continue if it was running a next request, since the
1323+
// request was automatically cancelled.
1324+
if (haltedDuringNext) {
1325+
// Send an output event containing a warning that next was cancelled.
1326+
const warning = `Setting breakpoints during 'next', 'step in' or 'step out' halted delve and cancelled the next request`;
1327+
this.sendEvent(new OutputEvent(warning, 'stderr'));
1328+
return;
1329+
}
13111330
return this.continue(true).then(null, (err) => {
13121331
this.logDelveError(err, 'Failed to continue delve after halting it to set breakpoints');
13131332
});
@@ -1687,8 +1706,16 @@ export class GoDebugSession extends LoggingDebugSession {
16871706
}
16881707

16891708
protected nextRequest(response: DebugProtocol.NextResponse): void {
1709+
this.nextEpoch++;
1710+
const closureEpoch = this.nextEpoch;
1711+
this.nextRequestRunning = true;
1712+
16901713
log('NextRequest');
16911714
this.delve.call<DebuggerState | CommandOut>('Command', [{ name: 'next' }], (err, out) => {
1715+
if (closureEpoch === this.continueEpoch) {
1716+
this.nextRequestRunning = false;
1717+
}
1718+
16921719
if (err) {
16931720
this.logDelveError(err, 'Failed to next');
16941721
}
@@ -1702,8 +1729,16 @@ export class GoDebugSession extends LoggingDebugSession {
17021729
}
17031730

17041731
protected stepInRequest(response: DebugProtocol.StepInResponse): void {
1732+
this.nextEpoch++;
1733+
const closureEpoch = this.nextEpoch;
1734+
this.nextRequestRunning = true;
1735+
17051736
log('StepInRequest');
17061737
this.delve.call<DebuggerState | CommandOut>('Command', [{ name: 'step' }], (err, out) => {
1738+
if (closureEpoch === this.continueEpoch) {
1739+
this.nextRequestRunning = false;
1740+
}
1741+
17071742
if (err) {
17081743
this.logDelveError(err, 'Failed to step in');
17091744
}
@@ -1717,8 +1752,16 @@ export class GoDebugSession extends LoggingDebugSession {
17171752
}
17181753

17191754
protected stepOutRequest(response: DebugProtocol.StepOutResponse): void {
1755+
this.nextEpoch++;
1756+
const closureEpoch = this.nextEpoch;
1757+
this.nextRequestRunning = true;
1758+
17201759
log('StepOutRequest');
17211760
this.delve.call<DebuggerState | CommandOut>('Command', [{ name: 'stepOut' }], (err, out) => {
1761+
if (closureEpoch === this.continueEpoch) {
1762+
this.nextRequestRunning = false;
1763+
}
1764+
17221765
if (err) {
17231766
this.logDelveError(err, 'Failed to step out');
17241767
}
@@ -2352,6 +2395,11 @@ export class GoDebugSession extends LoggingDebugSession {
23522395
return;
23532396
}
23542397

2398+
if (this.overrideStopReason?.length > 0) {
2399+
reason = this.overrideStopReason;
2400+
this.overrideStopReason = '';
2401+
}
2402+
23552403
const stoppedEvent = new StoppedEvent(reason, this.debugState.currentGoroutine.id);
23562404
(<any>stoppedEvent.body).allThreadsStopped = true;
23572405
this.sendEvent(stoppedEvent);
@@ -2375,7 +2423,7 @@ export class GoDebugSession extends LoggingDebugSession {
23752423
} catch (error) {
23762424
this.logDelveError(error, 'Failed to get state');
23772425
// Fall back to the internal tracking.
2378-
return this.continueRequestRunning;
2426+
return this.continueRequestRunning || this.nextRequestRunning;
23792427
}
23802428
}
23812429

test/integration/goDebug.test.ts

+111
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,117 @@ suite('Go Debug Adapter', function () {
880880
await killProcessTree(remoteProgram);
881881
await new Promise((resolve) => setTimeout(resolve, 2_000));
882882
});
883+
884+
test('stopped for a breakpoint set during initialization (remote attach)', async () => {
885+
const FILE = path.join(DATA_ROOT, 'helloWorldServer', 'main.go');
886+
const BREAKPOINT_LINE = 29;
887+
const remoteProgram = await setUpRemoteProgram(remoteAttachConfig.port, server);
888+
889+
const breakpointLocation = getBreakpointLocation(FILE, BREAKPOINT_LINE, false);
890+
891+
// Setup attach with a breakpoint.
892+
await setUpRemoteAttach(remoteAttachDebugConfig, [breakpointLocation]);
893+
894+
// Calls the helloworld server to make the breakpoint hit.
895+
await waitForBreakpoint(
896+
() => http.get(`http://localhost:${server}`).on('error', (data) => console.log(data)),
897+
breakpointLocation);
898+
899+
await dc.disconnectRequest({restart: false});
900+
await killProcessTree(remoteProgram);
901+
await new Promise((resolve) => setTimeout(resolve, 2_000));
902+
});
903+
904+
test('should set breakpoints during continue', async () => {
905+
const PROGRAM = path.join(DATA_ROOT, 'sleep');
906+
907+
const FILE = path.join(DATA_ROOT, 'sleep', 'sleep.go');
908+
const HELLO_LINE = 10;
909+
const helloLocation = getBreakpointLocation(FILE, HELLO_LINE);
910+
911+
const config = {
912+
name: 'Launch file',
913+
type: 'go',
914+
request: 'launch',
915+
mode: 'auto',
916+
program: PROGRAM
917+
};
918+
const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
919+
920+
await Promise.all([
921+
dc.configurationSequence(),
922+
dc.launch(debugConfig),
923+
]);
924+
925+
return Promise.all([
926+
dc.setBreakpointsRequest({
927+
lines: [ helloLocation.line ],
928+
breakpoints: [ { line: helloLocation.line, column: 0 } ],
929+
source: { path: helloLocation.path }
930+
}),
931+
dc.assertStoppedLocation('breakpoint', helloLocation)
932+
]);
933+
});
934+
935+
async function setBreakpointsDuringStep(nextFunc: () => void) {
936+
const PROGRAM = path.join(DATA_ROOT, 'sleep');
937+
938+
const FILE = path.join(DATA_ROOT, 'sleep', 'sleep.go');
939+
const SLEEP_LINE = 11;
940+
const setupBreakpoint = getBreakpointLocation(FILE, SLEEP_LINE);
941+
942+
const HELLO_LINE = 10;
943+
const onNextBreakpoint = getBreakpointLocation(FILE, HELLO_LINE);
944+
945+
const config = {
946+
name: 'Launch file',
947+
type: 'go',
948+
request: 'launch',
949+
mode: 'auto',
950+
program: PROGRAM
951+
};
952+
const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
953+
954+
await dc.hitBreakpoint(debugConfig, setupBreakpoint);
955+
956+
// The program is now stopped at the line containing time.Sleep().
957+
// Issue a next request, followed by a setBreakpointsRequest.
958+
nextFunc();
959+
960+
// Note: the current behavior of setting a breakpoint during a next
961+
// request will cause the step to be interrupted, so it may not be
962+
// stopped on the next line.
963+
await Promise.all([
964+
dc.setBreakpointsRequest({
965+
lines: [ onNextBreakpoint.line ],
966+
breakpoints: [ { line: onNextBreakpoint.line, column: 0 } ],
967+
source: { path: onNextBreakpoint.path }
968+
}),
969+
dc.assertStoppedLocation('next cancelled', {})
970+
]);
971+
972+
// Once the 'step' has completed, continue the program and
973+
// make sure the breakpoint set while the program was nexting
974+
// is succesfully hit.
975+
await Promise.all([
976+
dc.continueRequest({threadId: 1}),
977+
dc.assertStoppedLocation('breakpoint', onNextBreakpoint)
978+
]);
979+
}
980+
981+
test('should set breakpoints during next', async () => {
982+
setBreakpointsDuringStep(async () => {
983+
const nextResponse = await dc.nextRequest({threadId: 1});
984+
assert.ok(nextResponse.success);
985+
});
986+
});
987+
988+
test('should set breakpoints during step out', async () => {
989+
setBreakpointsDuringStep(async () => {
990+
const stepOutResponse = await dc.stepOutRequest({threadId: 1});
991+
assert.ok(stepOutResponse.success);
992+
});
993+
});
883994
});
884995

885996
suite('conditionalBreakpoints', () => {

0 commit comments

Comments
 (0)