[dds] Improve suppression of auto-resuming threads when attaching to processes

Hopefully fixes https://github.com/dart-lang/sdk/issues/48274.

Change-Id: I893a1f5dee54410986644a52b7c2bb406d4e51f3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/237683
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
diff --git a/pkg/dds/lib/src/dap/adapters/dart.dart b/pkg/dds/lib/src/dap/adapters/dart.dart
index 83c9d9d..ab9d049 100644
--- a/pkg/dds/lib/src/dap/adapters/dart.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart.dart
@@ -482,6 +482,10 @@
     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);
 
@@ -538,13 +542,11 @@
   /// The URI protocol will be changed to ws/wss but otherwise not normalised.
   /// The caller should handle any other normalisation (such as adding /ws to
   /// the end if required).
-  ///
-  /// If [resumeIfStarting] is true, isolates waiting to start will
-  /// automatically be resumed. This is usually desired in launch requests, but
-  /// not when attaching.
   Future<void> connectDebugger(
     Uri uri, {
-    required bool resumeIfStarting,
+    // TODO(dantup): Remove this after parameter after updating the Flutter
+    //   DAP to not pass it.
+    bool? resumeIfStarting,
   }) async {
     // Start up a DDS instance for this VM.
     if (enableDds) {
@@ -622,7 +624,7 @@
     await debuggerConnected(vmInfo);
 
     await _withErrorHandling(
-      () => _configureExistingIsolates(vmService, vmInfo, resumeIfStarting),
+      () => _configureExistingIsolates(vmService, vmInfo),
     );
 
     _debuggerInitializedCompleter.complete();
@@ -633,7 +635,6 @@
   Future<void> _configureExistingIsolates(
     vm.VmService vmService,
     vm.VM vmInfo,
-    bool resumeIfStarting,
   ) async {
     final existingIsolateRefs = vmInfo.isolates;
     final existingIsolates = existingIsolateRefs != null
@@ -659,12 +660,11 @@
       if (isolate.pauseEvent?.kind?.startsWith('Pause') ?? false) {
         await _isolateManager.handleEvent(
           isolate.pauseEvent!,
-          resumeIfStarting: resumeIfStarting,
         );
       } else if (isolate.runnable == true) {
         // If requested, automatically resume. Otherwise send a Stopped event to
         // inform the client UI the thread is paused.
-        if (resumeIfStarting) {
+        if (_isolateManager.autoResumeStartingIsolates) {
           await _isolateManager.resumeIsolate(isolate);
         } else {
           _isolateManager.sendStoppedOnEntryEvent(thread.threadId);
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 06def69..1c8be40 100644
--- a/pkg/dds/lib/src/dap/adapters/dart_cli_adapter.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart_cli_adapter.dart
@@ -94,7 +94,7 @@
     if (debug) {
       vmServiceInfoFile = generateVmServiceInfoFile();
       unawaited(waitForVmServiceInfoFile(logger, vmServiceInfoFile)
-          .then((uri) => connectDebugger(uri, resumeIfStarting: true)));
+          .then((uri) => connectDebugger(uri)));
     }
 
     final vmArgs = <String>[
@@ -191,7 +191,7 @@
         ? Uri.parse(vmServiceUri)
         : await waitForVmServiceInfoFile(logger, File(vmServiceInfoFile!));
 
-    unawaited(connectDebugger(uri, resumeIfStarting: false));
+    unawaited(connectDebugger(uri));
   }
 
   /// Calls the client (via a `runInTerminal` request) to spawn the process so
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 716fcfe..0a2b63c 100644
--- a/pkg/dds/lib/src/dap/adapters/dart_test_adapter.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart_test_adapter.dart
@@ -79,7 +79,7 @@
     if (debug) {
       vmServiceInfoFile = generateVmServiceInfoFile();
       unawaited(waitForVmServiceInfoFile(logger, vmServiceInfoFile)
-          .then((uri) => connectDebugger(uri, resumeIfStarting: true)));
+          .then((uri) => connectDebugger(uri)));
     }
 
     final vmArgs = <String>[
diff --git a/pkg/dds/lib/src/dap/isolate_manager.dart b/pkg/dds/lib/src/dap/isolate_manager.dart
index d13e8fa..df7dd76 100644
--- a/pkg/dds/lib/src/dap/isolate_manager.dart
+++ b/pkg/dds/lib/src/dap/isolate_manager.dart
@@ -56,6 +56,16 @@
   /// [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;
+
   /// The root of the Dart SDK containing the VM running the debug adapter.
   late final String sdkRoot;
 
@@ -138,13 +148,7 @@
   ThreadInfo? getThread(int threadId) => _threadsByThreadId[threadId];
 
   /// Handles Isolate and Debug events.
-  ///
-  /// If [resumeIfStarting] is `true`, PauseStart/PausePostStart events will be
-  /// automatically resumed from.
-  Future<void> handleEvent(
-    vm.Event event, {
-    bool resumeIfStarting = true,
-  }) async {
+  Future<void> handleEvent(vm.Event event) async {
     final isolateId = event.isolate?.id!;
 
     final eventKind = event.kind;
@@ -163,7 +167,7 @@
     if (eventKind == vm.EventKind.kIsolateExit) {
       _handleExit(event);
     } else if (eventKind?.startsWith('Pause') ?? false) {
-      await _handlePause(event, resumeIfStarting: resumeIfStarting);
+      await _handlePause(event);
     } else if (eventKind == vm.EventKind.kResume) {
       _handleResumed(event);
     }
@@ -236,6 +240,11 @@
   /// [vm.StepOption.kOver], a [StepOption.kOverAsyncSuspension] step will be
   /// sent instead.
   Future<void> resumeThread(int threadId, [String? resumeType]) 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) {
       throw DebugAdapterException('Thread $threadId was not found');
@@ -408,21 +417,18 @@
   ///
   /// For [vm.EventKind.kPausePostRequest] which occurs after a restart, the
   /// isolate will be re-configured (pause-exception behaviour, debuggable
-  /// libraries, breakpoints) and then (if [resumeIfStarting] is `true`)
-  /// resumed.
+  /// libraries, breakpoints) and then (if [autoResumeStartingIsolates] is
+  /// `true`) resumed.
   ///
-  /// For [vm.EventKind.kPauseStart] and [resumeIfStarting] is `true`, the
-  /// isolate will be resumed.
+  /// For [vm.EventKind.kPauseStart] and [autoResumeStartingIsolates] is `true`,
+  /// the isolate will be resumed.
   ///
   /// For breakpoints with conditions that are not met and for logpoints, the
   /// isolate will be automatically resumed.
   ///
   /// For all other pause types, the isolate will remain paused and a
   /// corresponding "Stopped" event sent to the editor.
-  Future<void> _handlePause(
-    vm.Event event, {
-    bool resumeIfStarting = true,
-  }) async {
+  Future<void> _handlePause(vm.Event event) async {
     final eventKind = event.kind;
     final isolate = event.isolate!;
     final thread = _threadsByIsolateId[isolate.id!];
@@ -439,7 +445,7 @@
     // after a hot restart.
     if (eventKind == vm.EventKind.kPausePostRequest) {
       await _configureIsolate(thread);
-      if (resumeIfStarting) {
+      if (autoResumeStartingIsolates) {
         await resumeThread(thread.threadId);
       }
     } else if (eventKind == vm.EventKind.kPauseStart) {
@@ -449,7 +455,7 @@
         thread.startupHandled = true;
         // If requested, automatically resume. Otherwise send a Stopped event to
         // inform the client UI the thread is paused.
-        if (resumeIfStarting) {
+        if (autoResumeStartingIsolates) {
           await resumeThread(thread.threadId);
         } else {
           sendStoppedOnEntryEvent(thread.threadId);