[DAP] Set `requirePermissionToResume` and `requireUserPermissionToResume` for `onPauseStart` and `onPauseExit` so that DDS waits for DAP's permission before resuming the isolate.
Bug: https://github.com/dart-lang/sdk/issues/54843
Change-Id: I12686d6c683983b7af5a3148681410ff9c684343
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/350649
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Elliott Brooks <elliottbrooks@google.com>
diff --git a/pkg/dds/CHANGELOG.md b/pkg/dds/CHANGELOG.md
index 64769fd..a42cda4 100644
--- a/pkg/dds/CHANGELOG.md
+++ b/pkg/dds/CHANGELOG.md
@@ -2,6 +2,7 @@
- [DAP]: Fixed an issue where breakpoint `changed` events might contain incorrect location information when new isolates are created, causing breakpoints to appear to move in the editor.
- [DAP]: For consistency with other values, automatic `toString()` invocations for debugger views no longer expand long strings and instead show truncated values. Full values continue to be returned for evaluation (`context=="repl"`) and when copying to the clipboard (`context=="clipboard"`).
- [DAP]: Improved handling of sentinel responses when building `variables` responses. This prevents entire map/list requests from failing when only some values inside are sentinels.
+- [DAP] Set `requirePermissionToResume` and `requireUserPermissionToResume` for `onPauseStart` and `onPauseExit` so that DDS waits for DAP's permission before resuming the isolate.
# 4.2.0
- [DAP] All `OutputEvent`s are now scanned for stack frames to attach `source` metadata to. The [parseStackFrames] parameter for `sendOutput` is ignored and deprecated.
diff --git a/pkg/dds/lib/src/dap/adapters/dart.dart b/pkg/dds/lib/src/dap/adapters/dart.dart
index ab27a7f..15b231b 100644
--- a/pkg/dds/lib/src/dap/adapters/dart.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart.dart
@@ -8,6 +8,7 @@
import 'package:collection/collection.dart';
import 'package:dap/dap.dart';
+import 'package:dds_service_extensions/dds_service_extensions.dart';
import 'package:json_rpc_2/error_code.dart' as json_rpc_errors;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
@@ -434,12 +435,19 @@
/// vs 'detached').
bool isDetaching = false;
- /// Whether isolates that pause in the PauseExit state should be automatically
- /// resumed after any in-process log events have completed.
+ /// Whether this adapter set the --pause-isolates-on-start flag, specifying
+ /// that isolates should pause on starting.
///
/// Normally this will be true, but it may be set to false if the user
- /// also manually passes pause-isolates-on-exit.
- bool resumeIsolatesAfterPauseExit = true;
+ /// also manually passed the --pause-isolates-on-start flag.
+ bool pauseIsolatesOnStartSetByDap = true;
+
+ /// Whether this adapter set the --pause-isolates-on-exit flag, specifying
+ /// that isolates should pause on exiting.
+ ///
+ /// Normally this will be true, but it may be set to false if the user
+ /// also manually passed the --pause-isolates-on-exit flag.
+ bool pauseIsolatesOnExitSetByDap = true;
/// A [Future] that completes when the last queued OutputEvent has been sent.
///
@@ -559,10 +567,6 @@
isAttach = true;
_subscribeToOutputStreams = true;
- // When attaching to a process, suppress auto-resuming isolates until the
- // first time the user resumes anything.
- isolateManager.autoResumeStartingIsolates = false;
-
// Common setup.
await _prepareForLaunchOrAttach(null);
@@ -702,6 +706,7 @@
// Let the subclass do any existing setup once we have a connection.
await debuggerConnected(vmInfo);
+ await _configureIsolateSettings(vmService);
await _withErrorHandling(
() => _configureExistingIsolates(vmService, vmInfo),
);
@@ -756,6 +761,68 @@
return DapProgressReporter.start(this, id, title, message: message);
}
+ Future<void> _configureIsolateSettings(
+ vm.VmService vmService,
+ ) async {
+ // If this is an attach workflow, check whether pause_isolates_on_start or
+ // pause_isolates_on_exit were already set, and if not set them (note: this
+ // is already done as part of the launch workflow):
+ if (isAttach) {
+ const pauseIsolatesOnStart = 'pause_isolates_on_start';
+ const pauseIsolatesOnExit = 'pause_isolates_on_exit';
+ final flags = (await vmService.getFlagList()).flags ?? <vm.Flag>[];
+ for (final flag in flags) {
+ final flagName = flag.name;
+ final isPauseIsolatesFlag =
+ flagName == pauseIsolatesOnStart || flagName == pauseIsolatesOnExit;
+ if (flagName == null || !isPauseIsolatesFlag) continue;
+
+ if (flag.valueAsString == 'true') {
+ if (flagName == pauseIsolatesOnStart) {
+ pauseIsolatesOnStartSetByDap = false;
+ }
+ if (flagName == pauseIsolatesOnExit) {
+ pauseIsolatesOnExitSetByDap = false;
+ }
+ } else {
+ _setVmFlagTo(vmService, flagName: flagName, valueAsString: 'true');
+ }
+ }
+ }
+
+ try {
+ // Make sure DDS waits for DAP to be ready to resume before forwarding
+ // resume requests to the VM Service:
+ await vmService.requirePermissionToResume(
+ onPauseStart: true,
+ onPauseExit: true,
+ );
+
+ // Specify whether DDS should wait for a user-initiated resume as well as a
+ // DAP-initiated resume:
+ await vmService.requireUserPermissionToResume(
+ onPauseStart: !pauseIsolatesOnStartSetByDap,
+ onPauseExit: !pauseIsolatesOnExitSetByDap,
+ );
+ } catch (e) {
+ // If DDS is not enabled, calling these DDS service extensions will fail.
+ // Therefore catch and log any errors.
+ logger?.call('Failure configuring isolate settings: $e');
+ }
+ }
+
+ Future<void> _setVmFlagTo(
+ vm.VmService vmService, {
+ required String flagName,
+ required String valueAsString,
+ }) async {
+ try {
+ await vmService.setFlag(flagName, valueAsString);
+ } catch (e) {
+ logger?.call('Failed to to set VM flag $flagName to $valueAsString: $e');
+ }
+ }
+
/// Process any existing isolates that may have been created before the
/// streams above were set up.
Future<void> _configureExistingIsolates(
@@ -775,8 +842,7 @@
final pauseEventKind = isolate.runnable ?? false
? vm.EventKind.kIsolateRunnable
: vm.EventKind.kIsolateStart;
- final thread =
- await isolateManager.registerIsolate(isolate, pauseEventKind);
+ await isolateManager.registerIsolate(isolate, pauseEventKind);
// If the Isolate already has a Pause event we can give it to the
// IsolateManager to handle (if it's PausePostStart it will re-configure
@@ -788,13 +854,7 @@
isolate.pauseEvent!,
);
} else if (isolate.runnable == true) {
- // If requested, automatically resume. Otherwise send a Stopped event to
- // inform the client UI the thread is paused.
- if (isolateManager.autoResumeStartingIsolates) {
- await isolateManager.resumeIsolate(isolate);
- } else {
- isolateManager.sendStoppedOnEntryEvent(thread.threadId);
- }
+ await isolateManager.readyToResumeIsolate(isolate);
}
}));
}
@@ -2352,11 +2412,9 @@
// We pause isolates on exit to allow requests for resolving URIs in
// stderr call stacks, so when we see an isolate pause, wait for any
// pending logs and then resume it (so it exits).
- if (resumeIsolatesAfterPauseExit &&
- eventKind == vm.EventKind.kPauseExit &&
- isolate != null) {
+ if (eventKind == vm.EventKind.kPauseExit && isolate != null) {
await _waitForPendingOutputEvents();
- await isolateManager.resumeIsolate(isolate);
+ await isolateManager.readyToResumeIsolate(isolate);
}
}
diff --git a/pkg/dds/lib/src/dap/adapters/dart_cli_adapter.dart b/pkg/dds/lib/src/dap/adapters/dart_cli_adapter.dart
index 77913c1..8c14d7a 100644
--- a/pkg/dds/lib/src/dap/adapters/dart_cli_adapter.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart_cli_adapter.dart
@@ -11,6 +11,7 @@
import 'package:path/path.dart' as path;
import 'package:vm_service/vm_service.dart' as vm;
+import '../utils.dart';
import 'dart.dart';
import 'mixins.dart';
@@ -68,14 +69,6 @@
terminatePids(ProcessSignal.sigkill);
}
- /// Checks whether [flag] is in [args], allowing for both underscore and
- /// dash format.
- bool _containsVmFlag(List<String> args, String flag) {
- final flagUnderscores = flag.replaceAll('-', '_');
- final flagDashes = flag.replaceAll('_', '-');
- return args.contains(flagUnderscores) || args.contains(flagDashes);
- }
-
@override
Future<void> launchImpl() {
throw UnsupportedError(
@@ -115,7 +108,6 @@
...?args.vmAdditionalArgs,
if (debug) ...[
'--enable-vm-service=${args.vmServicePort ?? 0}${ipv6 ? '/::1' : ''}',
- '--pause_isolates_on_start',
if (!enableAuthCodes) '--disable-service-auth-codes'
],
'--disable-dart-dev',
@@ -128,12 +120,20 @@
final toolArgs = args.toolArgs ?? [];
if (debug) {
// If the user has explicitly set pause-isolates-on-exit we need to
- // not add it ourselves, and disable auto-resuming.
- if (_containsVmFlag(toolArgs, '--pause_isolates_on_exit')) {
- resumeIsolatesAfterPauseExit = false;
+ // not add it ourselves, and specify that we didn't set it.
+ if (containsVmFlag(toolArgs, '--pause_isolates_on_exit')) {
+ pauseIsolatesOnExitSetByDap = false;
} else {
vmArgs.add('--pause_isolates_on_exit');
}
+
+ // If the user has explicitly set pause-isolates-on-start we need to
+ // not add it ourselves, and specify that we didn't set it.
+ if (containsVmFlag(toolArgs, '--pause_isolates_on_start')) {
+ pauseIsolatesOnStartSetByDap = false;
+ } else {
+ vmArgs.add('--pause_isolates_on_start');
+ }
}
// Handle customTool and deletion of any arguments for it.
diff --git a/pkg/dds/lib/src/dap/adapters/dart_test_adapter.dart b/pkg/dds/lib/src/dap/adapters/dart_test_adapter.dart
index f9d66c1..f894136 100644
--- a/pkg/dds/lib/src/dap/adapters/dart_test_adapter.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart_test_adapter.dart
@@ -10,6 +10,7 @@
import 'package:vm_service/vm_service.dart' as vm;
import '../stream_transformers.dart';
+import '../utils.dart';
import 'dart.dart';
import 'mixins.dart';
@@ -78,17 +79,32 @@
.then((uri) => connectDebugger(uri)));
}
- final vmArgs = <String>[
+ var vmArgs = <String>[
...?args.vmAdditionalArgs,
if (debug) ...[
'--enable-vm-service=${args.vmServicePort ?? 0}${ipv6 ? '/::1' : ''}',
- '--pause_isolates_on_start',
if (!enableAuthCodes) '--disable-service-auth-codes'
],
if (debug && vmServiceInfoFile != null) ...[
'-DSILENT_VM_SERVICE=true',
'--write-service-info=${Uri.file(vmServiceInfoFile.path)}'
],
+ ];
+
+ final toolArgs = args.toolArgs ?? [];
+ if (debug) {
+ // If the user has explicitly set pause-isolates-on-start we need to
+ // not add it ourselves, and specify that we didn't set it.
+ if (containsVmFlag(toolArgs, '--pause_isolates_on_start') ||
+ containsVmFlag(vmArgs, '--pause_isolates_on_start')) {
+ pauseIsolatesOnStartSetByDap = false;
+ } else {
+ vmArgs.add('--pause_isolates_on_start');
+ }
+ }
+
+ vmArgs = [
+ ...vmArgs,
// TODO(dantup): This should be changed from "dart run test:test" to
// "dart test" once the start-paused flags are working correctly.
// Currently they start paused but do not write the vm-service-info file
diff --git a/pkg/dds/lib/src/dap/isolate_manager.dart b/pkg/dds/lib/src/dap/isolate_manager.dart
index 893f520..8f77086 100644
--- a/pkg/dds/lib/src/dap/isolate_manager.dart
+++ b/pkg/dds/lib/src/dap/isolate_manager.dart
@@ -7,6 +7,7 @@
import 'package:collection/collection.dart';
import 'package:dap/dap.dart';
+import 'package:dds_service_extensions/dds_service_extensions.dart';
import 'package:vm_service/vm_service.dart' as vm;
import '../rpc_error_codes.dart';
@@ -62,16 +63,6 @@
/// [debugExternalPackageLibraries] in one step.
bool debugExternalPackageLibraries = true;
- /// Whether to automatically resume new isolates after configuring them.
- ///
- /// This setting is almost always `true` because isolates are paused only so
- /// we can configure them (send breakpoints, pause-on-exceptions,
- /// setLibraryDebuggables) without races. It is set to `false` during the
- /// initial connection of an `attachRequest` to allow paused isolates to
- /// remain paused. In this case, it will be automatically re-set to `true` the
- /// first time the user resumes.
- bool autoResumeStartingIsolates = true;
-
/// Tracks breakpoints last provided by the client so they can be sent to new
/// isolates that appear after initial breakpoints were sent.
final Map<String, List<ClientBreakpoint>> _clientBreakpointsByUri = {};
@@ -277,6 +268,17 @@
await resumeThread(thread.threadId);
}
+ Future<void> readyToResumeIsolate(vm.IsolateRef isolateRef) async {
+ final isolateId = isolateRef.id!;
+
+ final thread = _threadsByIsolateId[isolateId];
+ if (thread == null) {
+ return;
+ }
+
+ await readyToResumeThread(thread.threadId);
+ }
+
/// Resumes (or steps) an isolate using its client [threadId].
///
/// If the isolate is not paused, or already has a pending resume request
@@ -289,6 +291,16 @@
await _resume(threadId, resumeType: resumeType);
}
+ /// Resumes an isolate using its client [threadId].
+ ///
+ /// CAUTION: This should only be used for a tool-initiated resume, not a user-
+ /// initiated resume.
+ ///
+ /// See: https://pub.dev/documentation/dds_service_extensions/latest/dds_service_extensions/DdsExtension/readyToResume.html
+ Future<void> readyToResumeThread(int threadId) async {
+ await _readyToResume(threadId);
+ }
+
/// Rewinds an isolate to an earlier frame using its client [threadId].
///
/// If the isolate is not paused, or already has a pending resume request
@@ -301,7 +313,8 @@
);
}
- /// Resumes (or steps) an isolate using its client [threadId].
+ /// Resumes (or steps) an isolate using its client [threadId] on behalf
+ /// of the user.
///
/// If the isolate is not paused, or already has a pending resume request
/// in-flight, a request will not be sent.
@@ -316,11 +329,6 @@
String? resumeType,
int? frameIndex,
}) async {
- // The first time a user resumes a thread is our signal that the app is now
- // "running" and future isolates can be auto-resumed. This only affects
- // attach, as it's already `true` for launch requests.
- autoResumeStartingIsolates = true;
-
final thread = _threadsByThreadId[threadId];
if (thread == null) {
if (isInvalidThreadId(threadId)) {
@@ -336,7 +344,7 @@
// Check this thread hasn't already been resumed by another handler in the
// meantime (for example if the user performs a hot restart or something
// while we processing some previous events).
- if (!thread.paused || thread.hasPendingResume) {
+ if (!thread.paused || thread.hasPendingUserResume) {
return;
}
@@ -350,7 +358,7 @@
// we can drop them to save memory.
thread.clearStoredData();
- thread.hasPendingResume = true;
+ thread.hasPendingUserResume = true;
try {
await _adapter.vmService?.resume(
thread.isolate.id!,
@@ -369,7 +377,57 @@
rethrow;
}
} finally {
- thread.hasPendingResume = false;
+ thread.hasPendingUserResume = false;
+ }
+ }
+
+ /// Resumes an isolate using its client [threadId].
+ ///
+ /// CAUTION: This should only be used for a tool-initiated resume, not a user-
+ /// initiated resume.
+ ///
+ /// See: https://pub.dev/documentation/dds_service_extensions/latest/dds_service_extensions/DdsExtension/readyToResume.html
+ Future<void> _readyToResume(int threadId) async {
+ final thread = _threadsByThreadId[threadId];
+ if (thread == null) {
+ if (isInvalidThreadId(threadId)) {
+ throw DebugAdapterException('Thread $threadId was not found');
+ } else {
+ // Otherwise, this thread has exited and we don't need to do anything.
+ // It's possible another debugger unpaused or we're shutting down and
+ // the VM has terminated it.
+ return;
+ }
+ }
+
+ // When we're resuming, all stored objects become invalid and we can drop
+ // to save memory.
+ thread.clearStoredData();
+
+ try {
+ thread.hasPendingDapResume = true;
+ await _adapter.vmService?.readyToResume(thread.isolate.id!);
+ } on UnimplementedError {
+ // Fallback to a regular resume if the DDS version doesn't support
+ // `readyToResume`:
+ return _resume(threadId);
+ } on vm.SentinelException {
+ // It's possible during these async requests that the isolate went away
+ // (for example a shutdown/restart) and we no longer care about
+ // resuming it.
+ } on vm.RPCError catch (e) {
+ if (e.code == RpcErrorCodes.kIsolateMustBePaused) {
+ // It's possible something else resumed the thread (such as if another
+ // debugger is attached), we can just continue.
+ } else if (e.code == RpcErrorCodes.kMethodNotFound) {
+ // Fallback to a regular resume if the DDS service extension isn't
+ // available:
+ return _resume(threadId);
+ } else {
+ rethrow;
+ }
+ } finally {
+ thread.hasPendingDapResume = false;
}
}
@@ -406,8 +464,9 @@
bool isInvalidThreadId(int threadId) => threadId >= _nextThreadNumber;
/// Sends an event informing the client that a thread is stopped at entry.
- void sendStoppedOnEntryEvent(int threadId) {
- _adapter.sendEvent(StoppedEventBody(reason: 'entry', threadId: threadId));
+ void sendStoppedOnEntryEvent(ThreadInfo thread) {
+ _adapter.sendEvent(
+ StoppedEventBody(reason: 'entry', threadId: thread.threadId));
}
/// Records breakpoints for [uri].
@@ -557,11 +616,9 @@
///
/// For [vm.EventKind.kPausePostRequest] which occurs after a restart, the
/// isolate will be re-configured (pause-exception behaviour, debuggable
- /// libraries, breakpoints) and then (if [autoResumeStartingIsolates] is
- /// `true`) resumed.
+ /// libraries, breakpoints) and we'll declare we are ready to resume.
///
- /// For [vm.EventKind.kPauseStart] and [autoResumeStartingIsolates] is `true`,
- /// the isolate will be resumed.
+ /// For [vm.EventKind.kPauseStart] we'll declare we are ready to resume.
///
/// For breakpoints with conditions that are not met and for logpoints, the
/// isolate will be automatically resumed.
@@ -586,21 +643,17 @@
// after a hot restart.
if (eventKind == vm.EventKind.kPausePostRequest) {
await _configureIsolate(thread);
- if (autoResumeStartingIsolates) {
- await resumeThread(thread.threadId);
- }
+ await readyToResumeThread(thread.threadId);
} else if (eventKind == vm.EventKind.kPauseStart) {
// Don't resume from a PauseStart if this has already happened (see
// comments on [thread.hasBeenStarted]).
if (!thread.startupHandled) {
thread.startupHandled = true;
- // If requested, automatically resume. Otherwise send a Stopped event to
- // inform the client UI the thread is paused.
- if (autoResumeStartingIsolates) {
- await resumeThread(thread.threadId);
- } else {
- sendStoppedOnEntryEvent(thread.threadId);
- }
+ // Send a Stopped event to inform the client UI the thread is paused and
+ // declare that we are ready to resume (which might result in an
+ // immediate resume).
+ sendStoppedOnEntryEvent(thread);
+ await readyToResumeThread(thread.threadId);
}
} else {
// PauseExit, PauseBreakpoint, PauseInterrupted, PauseException
@@ -643,6 +696,8 @@
reason = 'step';
} else if (eventKind == vm.EventKind.kPauseException) {
reason = 'exception';
+ } else if (eventKind == vm.EventKind.kPauseExit) {
+ reason = 'exit';
}
// If we stopped at an exception, capture the exception instance so we
@@ -671,6 +726,11 @@
final isolate = event.isolate!;
final thread = _threadsByIsolateId[isolate.id!];
if (thread != null) {
+ // When a thread is resumed, we must inform the client. This is not
+ // necessary when the user has clicked Continue because it is implied.
+ // However, resume events can now be triggered by other things (eg. other
+ // in other IDEs or DevTools) so we must notify the client.
+ _adapter.sendEvent(ContinuedEventBody(threadId: thread.threadId));
thread.paused = false;
thread.pauseEvent = null;
thread.exceptionReference = null;
@@ -1057,6 +1117,11 @@
var runnable = false;
var atAsyncSuspension = false;
int? exceptionReference;
+
+ /// Whether this thread is currently known to be paused in the VM.
+ ///
+ /// Because requests are async, this is not guaranteed to be always correct
+ /// but should represent the state based on the latest VM events.
var paused = false;
/// Tracks whether an isolates startup routine has been handled.
@@ -1096,9 +1161,13 @@
/// Values are file-like URIs (file: or similar, such as dart-macro+file:).
final _resolvedPaths = <String, Future<Uri?>>{};
- /// Whether this isolate has an in-flight resume request that has not yet
- /// been responded to.
- var hasPendingResume = false;
+ /// Whether this isolate has an in-flight user-initiated resume request that
+ /// has not yet been responded to.
+ var hasPendingUserResume = false;
+
+ /// Whether this isolate has an in-flight DAP (readyToResume) resume request
+ /// that has not yet been responded to.
+ var hasPendingDapResume = false;
ThreadInfo(this._manager, this.threadId, this.isolate);
diff --git a/pkg/dds/lib/src/dap/utils.dart b/pkg/dds/lib/src/dap/utils.dart
index d50a06a..5536345 100644
--- a/pkg/dds/lib/src/dap/utils.dart
+++ b/pkg/dds/lib/src/dap/utils.dart
@@ -98,3 +98,11 @@
tryParseFrame((line) => stack.Frame.parseIE(line)) ??
tryParseFrame((line) => stack.Frame.parseFriendly(line));
}
+
+/// Checks whether [flag] is in [args], allowing for both underscore and
+/// dash format.
+bool containsVmFlag(List<String> args, String flag) {
+ final flagUnderscores = flag.replaceAll('-', '_');
+ final flagDashes = flag.replaceAll('_', '-');
+ return args.contains(flagUnderscores) || args.contains(flagDashes);
+}
diff --git a/pkg/dds/test/dap/integration/dart_test_test.dart b/pkg/dds/test/dap/integration/dart_test_test.dart
index 263aa9c..3d15b82 100644
--- a/pkg/dds/test/dap/integration/dart_test_test.dart
+++ b/pkg/dds/test/dap/integration/dart_test_test.dart
@@ -298,7 +298,8 @@
final outputEvents = await client.collectTestOutput(
launch: () => client.attach(
vmServiceUri: 'ws://bogus.local/',
- autoResume: false,
+ autoResumeOnEntry: false,
+ autoResumeOnExit: false,
),
);
diff --git a/pkg/dds/test/dap/integration/debug_attach_test.dart b/pkg/dds/test/dap/integration/debug_attach_test.dart
index b378dd8..e3e2dcc 100644
--- a/pkg/dds/test/dap/integration/debug_attach_test.dart
+++ b/pkg/dds/test/dap/integration/debug_attach_test.dart
@@ -25,13 +25,15 @@
testFile.path,
args,
cwd: dap.testAppDir.path,
+ pauseOnExit: true, // To ensure we capture all output
);
final vmServiceUri = await waitForStdoutVmServiceBanner(proc);
final outputEvents = await dap.client.collectOutput(
launch: () => dap.client.attach(
vmServiceUri: vmServiceUri.toString(),
- autoResume: true,
+ autoResumeOnEntry: true,
+ autoResumeOnExit: true,
cwd: dap.testAppDir.path,
),
);
@@ -78,11 +80,13 @@
['one', 'two'],
cwd: dap.testAppDir.path,
vmArgs: ['--write-service-info=${Uri.file(vmServiceInfoFilePath)}'],
+ pauseOnExit: true, // To ensure we capture all output
);
final outputEvents = await dap.client.collectOutput(
launch: () => dap.client.attach(
vmServiceInfoFile: vmServiceInfoFilePath,
- autoResume: true,
+ autoResumeOnEntry: true,
+ autoResumeOnExit: true,
cwd: dap.testAppDir.path,
),
);
@@ -125,6 +129,11 @@
testFile.path,
[],
cwd: dap.testAppDir.path,
+ // Disable user-pause-on-exit because we're checking DAP resumes and
+ // if the VM waits for user-resume, we won't complete. We don't want to
+ // send an explicit user-resume because that would force resume,
+ // invalidating this test that we did a DAP resume.
+ pauseOnExit: false,
);
final vmServiceUri = await waitForStdoutVmServiceBanner(proc);
@@ -135,7 +144,8 @@
client.start(
launch: () => client.attach(
vmServiceUri: vmServiceUri.toString(),
- autoResume: false,
+ autoResumeOnEntry: false,
+ autoResumeOnExit: false,
cwd: dap.testAppDir.path,
),
),
diff --git a/pkg/dds/test/dap/integration/debug_breakpoints_test.dart b/pkg/dds/test/dap/integration/debug_breakpoints_test.dart
index 6e4c49e..5207121 100644
--- a/pkg/dds/test/dap/integration/debug_breakpoints_test.dart
+++ b/pkg/dds/test/dap/integration/debug_breakpoints_test.dart
@@ -646,7 +646,8 @@
client2.start(
launch: () => client2.attach(
vmServiceUri: vmServiceUri.toString(),
- autoResume: false,
+ autoResumeOnEntry: false,
+ autoResumeOnExit: false,
cwd: dap.testAppDir.path,
),
),
diff --git a/pkg/dds/test/dap/integration/debug_test.dart b/pkg/dds/test/dap/integration/debug_test.dart
index 33f3ea9..3167b5b 100644
--- a/pkg/dds/test/dap/integration/debug_test.dart
+++ b/pkg/dds/test/dap/integration/debug_test.dart
@@ -161,7 +161,7 @@
final testFile = dap.createTestFile(simpleArgPrintingProgram);
// Run the script, expecting a Stopped event.
- final stop = dap.client.expectStop('pause');
+ final stop = dap.client.expectStop('exit');
await Future.wait([
stop,
dap.client.initialize(),
@@ -176,6 +176,97 @@
], eagerError: true);
});
+ test('does not resume isolates if user passes --pause-isolates-on-start',
+ () async {
+ // Internally we always pass --pause-isolates-on-start and resume the
+ // isolates after setting any breakpoints.
+ //
+ // However if a user passes this flag explicitly, we should not
+ // auto-resume because they might be trying to debug something.
+ final testFile = dap.createTestFile(simpleArgPrintingProgram);
+
+ // Run the script, expecting a Stopped event.
+ final stop = dap.client.expectStop('entry');
+ await Future.wait([
+ stop,
+ dap.client.initialize(),
+ dap.client.launch(
+ testFile.path,
+ toolArgs: ["--pause-isolates-on-start"],
+ ),
+ ], eagerError: true);
+
+ // Resume and expect termination.
+ await Future.wait([
+ dap.client.event('terminated'),
+ dap.client.continue_((await stop).threadId!),
+ ], eagerError: true);
+ });
+
+ test('receives thread, stopped, continued events during pause/resume',
+ () async {
+ final client = dap.client;
+ final testFile = dap.createTestFile(debuggerPauseAndPrintManyProgram);
+
+ // Collect interesting events that we want to verify exist and in the
+ // right order.
+ final interestingEvents = const {
+ 'thread',
+ 'stopped',
+ 'continued',
+ 'terminated'
+ };
+ final eventsFuture = client.allEvents
+ .where((e) => interestingEvents.contains(e.event))
+ .map((e) {
+ // Map onto a descriptive string for verifying later.
+ final reason = (e.body as Map<String, Object?>)['reason'] as String?;
+ return reason != null ? '${e.event} ($reason)' : e.event;
+ }).toList();
+
+ // Start the program and wait to pause on `debugger()`.
+ final stoppedFuture = client.expectStop('step');
+ await client.start(file: testFile);
+ final threadId = (await stoppedFuture).threadId!;
+
+ // Step 3 times and wait for the corresponding stop.
+ for (var i = 0; i < 3; i++) {
+ client.next(threadId);
+ await client.stoppedEvents.first;
+ }
+
+ // Resume to run to end.
+ client.continue_(threadId);
+
+ // Verify we had the expected events.
+ expect(
+ await eventsFuture,
+ [
+ 'thread (started)',
+ 'stopped (entry)',
+ 'continued',
+ // stop on debugger()
+ 'stopped (step)',
+ // step 1
+ 'continued',
+ 'stopped (step)',
+ // step 2
+ 'continued',
+ 'stopped (step)',
+ // step 3
+ 'continued',
+ 'stopped (step)',
+ // continue
+ 'continued',
+ // pause-on-exit to drain stdout and handle looking up URIs
+ 'stopped (exit)',
+ // finished
+ 'thread (exited)',
+ 'terminated',
+ ],
+ );
+ });
+
for (final outputKind in ['stdout', 'stderr']) {
test('sends $outputKind output events in the correct order', () async {
// Output events that have their URIs mapped will be processed slowly due
@@ -760,18 +851,21 @@
}, timeout: Timeout.none);
group('debug mode', () {
- test('can run without DDS', () async {
- final dap = await DapTestSession.setUp(additionalArgs: ['--no-dds']);
- addTearDown(dap.tearDown);
+ test(
+ 'can run without DDS',
+ () async {
+ final dap = await DapTestSession.setUp(additionalArgs: ['--no-dds']);
+ addTearDown(dap.tearDown);
- final client = dap.client;
- final testFile = dap.createTestFile(simpleBreakpointProgram);
- final breakpointLine = lineWith(testFile, breakpointMarker);
+ final client = dap.client;
+ final testFile = dap.createTestFile(simpleBreakpointProgram);
+ final breakpointLine = lineWith(testFile, breakpointMarker);
- await client.hitBreakpoint(testFile, breakpointLine);
+ await client.hitBreakpoint(testFile, breakpointLine);
- expect(await client.ddsAvailable, isFalse);
- });
+ expect(await client.ddsAvailable, isFalse);
+ },
+ );
test('can run without auth codes', () async {
final dap =
diff --git a/pkg/dds/test/dap/integration/test_client.dart b/pkg/dds/test/dap/integration/test_client.dart
index 936a6d3..1a09111 100644
--- a/pkg/dds/test/dap/integration/test_client.dart
+++ b/pkg/dds/test/dap/integration/test_client.dart
@@ -135,7 +135,8 @@
/// Send an attachRequest to the server, asking it to attach to an existing
/// Dart program.
Future<Response> attach({
- required bool autoResume,
+ required bool autoResumeOnEntry,
+ required bool autoResumeOnExit,
String? vmServiceUri,
String? vmServiceInfoFile,
String? cwd,
@@ -154,10 +155,19 @@
// When attaching, the paused VM will not be automatically unpaused, but
// instead send a Stopped(reason: 'entry') event. Respond to this by
// resuming (if requested).
- final resumeFuture = autoResume
+ final resumeFuture = autoResumeOnEntry
? expectStop('entry').then((event) => continue_(event.threadId!))
: null;
+ // Also handle resuming on exit. This should be done if the test has
+ // started the app with a user-provided pause-on-exit but wants to
+ // simulate the user resuming after the exit pause.
+ if (autoResumeOnExit) {
+ stoppedEvents
+ .firstWhere((e) => e.reason == 'exit')
+ .then((event) => continue_(event.threadId!));
+ }
+
cwd ??= defaultCwd;
final attachResponse = sendRequest(
DartAttachRequestArguments(
@@ -227,13 +237,22 @@
/// Returns a Future that completes with the next [event] event.
Future<Event> event(String event) => _logIfSlow(
'Event "$event"',
- _eventController.stream.firstWhere((e) => e.event == event,
+ allEvents.firstWhere((e) => e.event == event,
orElse: () =>
throw 'Did not receive $event event before stream closed'));
/// Returns a stream for [event] events.
Stream<Event> events(String event) {
- return _eventController.stream.where((e) => e.event == event);
+ return allEvents.where((e) => e.event == event);
+ }
+
+ Stream<Event> get allEvents => _eventController.stream;
+
+ /// Returns a stream for 'stopped' events.
+ Stream<StoppedEventBody> get stoppedEvents {
+ return allEvents
+ .where((e) => e.event == 'stopped')
+ .map((e) => StoppedEventBody.fromJson(e.body as Map<String, Object?>));
}
/// Returns a stream for standard progress events.
@@ -243,8 +262,7 @@
'progressUpdate',
'progressEnd'
};
- return _eventController.stream
- .where((e) => standardProgressEvents.contains(e.event));
+ return allEvents.where((e) => standardProgressEvents.contains(e.event));
}
/// Returns a stream for custom Dart progress events.
@@ -254,8 +272,7 @@
'dart.progressUpdate',
'dart.progressEnd'
};
- return _eventController.stream
- .where((e) => customProgressEvents.contains(e.event));
+ return allEvents.where((e) => customProgressEvents.contains(e.event));
}
/// Records a handler for when the server sends a [request] request.
@@ -902,14 +919,26 @@
///
/// If [file] or [line] are provided, they will be checked against the stop
/// location for the top stack frame.
+ ///
+ /// Stopped-on-entry events will be automatically skipped unless
+ /// [skipFirstStopOnEntry] is `false` or [reason] is `"entry"`.
Future<StoppedEventBody> expectStop(
String reason, {
File? file,
int? line,
String? sourceName,
+ bool? skipFirstStopOnEntry,
}) async {
- final e = await event('stopped');
- final stop = StoppedEventBody.fromJson(e.body as Map<String, Object?>);
+ skipFirstStopOnEntry ??= reason != 'entry';
+ assert(skipFirstStopOnEntry != (reason == 'entry'));
+
+ // Unless we're specifically waiting for stop-on-entry, skip over those
+ // events because they can be emitted at during startup because now we use
+ // readyToResume we don't know if the isolate will be immediately unpaused
+ // and the client needs to have a consistent view of threads.
+ final stop = skipFirstStopOnEntry
+ ? await stoppedEvents.skipWhile((e) => e.reason == 'entry').first
+ : await stoppedEvents.first;
expect(stop.reason, equals(reason));
final result =
diff --git a/pkg/dds/test/dap/integration/test_scripts.dart b/pkg/dds/test/dap/integration/test_scripts.dart
index dc32ca8..ed265669 100644
--- a/pkg/dds/test/dap/integration/test_scripts.dart
+++ b/pkg/dds/test/dap/integration/test_scripts.dart
@@ -227,6 +227,23 @@
}
''';
+/// A program that immediately pauses with debugger() and then has 5 print
+/// statements.
+///
+/// Used for verifying breaking and stepping behaviour.
+const debuggerPauseAndPrintManyProgram = '''
+ import 'dart:developer';
+
+ void main(List<String> args) async {
+ debugger();
+ print('1');
+ print('2');
+ print('3');
+ print('4');
+ print('5');
+ }
+''';
+
final simpleBreakpointProgramWith50ExtraLines = '''
void main(List<String> args) async {
print('Hello!'); $breakpointMarker
diff --git a/pkg/dds/test/dap/integration/test_support.dart b/pkg/dds/test/dap/integration/test_support.dart
index 031cbcd..15ba49c 100644
--- a/pkg/dds/test/dap/integration/test_support.dart
+++ b/pkg/dds/test/dap/integration/test_support.dart
@@ -88,6 +88,7 @@
List<String> args, {
required String cwd,
List<String>? vmArgs,
+ required bool pauseOnExit,
}) async {
final vmPath = Platform.resolvedExecutable;
vmArgs ??= [];
@@ -95,7 +96,7 @@
'--enable-vm-service=0',
'--pause_isolates_on_start',
// Use pause-on-exit so we don't lose async output events in attach tests.
- '--pause_isolates_on_exit',
+ if (pauseOnExit) '--pause_isolates_on_exit',
]);
final processArgs = [
...vmArgs,