[dds/dap] Handle some additional kinds of errors when the VM Service connection shuts down

This handles an RPC Error -32000 with text "Service connection disposed" as reported in https://github.com/dart-lang/sdk/issues/60851. It extracts some of this logic into an extension on RPCError to simplify checking in multiple places.

Unfortunately I can't reproduce the specific conditions that produce this error in a test (it probably requires terminated the VM Service at just the right time during startup or an isolate starting?).

Fixes https://github.com/dart-lang/sdk/issues/60851

Change-Id: Iae945f60290766164d04b91d851fbb8b58f178c6
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/432944
Reviewed-by: Derek Xu <derekx@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
Reviewed-by: Ben Konyi <bkonyi@google.com>
diff --git a/pkg/dds/CHANGELOG.md b/pkg/dds/CHANGELOG.md
index 77f29ff..9344e96 100644
--- a/pkg/dds/CHANGELOG.md
+++ b/pkg/dds/CHANGELOG.md
@@ -1,4 +1,5 @@
 # 5.0.3
+- [DAP] Handle some additional errors if the VM Service is shutting down during an attempt to resume an isolate.
 - [DAP] Stack frames with dots in paths will now be parsed and have locations attached to `OutputEvents`s.
 - [DAP] Responses to `evaluateRequest` that are lists now include `indexedVariables` to allow for client-side paging.
 
diff --git a/pkg/dds/lib/src/dap/adapters/dart.dart b/pkg/dds/lib/src/dap/adapters/dart.dart
index 5f31a8d..5909aa1 100644
--- a/pkg/dds/lib/src/dap/adapters/dart.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart.dart
@@ -15,7 +15,6 @@
 import 'package:vm_service/vm_service.dart' as vm;
 
 import '../../../dds.dart';
-import '../../rpc_error_codes.dart';
 import '../base_debug_adapter.dart';
 import '../isolate_manager.dart';
 import '../logging.dart';
@@ -2838,8 +2837,7 @@
       // outside of the DAP (eg. closing the simulator) so it's possible our
       // requests will fail in this way before we've handled any event to set
       // `isTerminating`.
-      if (e.code == RpcErrorCodes.kServiceDisappeared ||
-          e.code == RpcErrorCodes.kConnectionDisposed) {
+      if (e.isServiceDisposedError) {
         return null;
       }
 
@@ -2853,15 +2851,6 @@
         if (isTerminating) {
           return null;
         }
-
-        // Always ignore "client is closed" and "closed with pending request"
-        // errors because these can always occur during shutdown if we were
-        // just starting to send (or had just sent) a request.
-        if (e.message.contains("The client is closed") ||
-            e.message.contains("The client closed with pending request") ||
-            e.message.contains("Service connection disposed")) {
-          return null;
-        }
       }
 
       // Otherwise, it's an unexpected/unknown failure and should be rethrown.
diff --git a/pkg/dds/lib/src/dap/isolate_manager.dart b/pkg/dds/lib/src/dap/isolate_manager.dart
index ca7801c..bfc2f49 100644
--- a/pkg/dds/lib/src/dap/isolate_manager.dart
+++ b/pkg/dds/lib/src/dap/isolate_manager.dart
@@ -378,6 +378,9 @@
       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.isServiceDisposedError) {
+        // The VM service connection was terminated, we can silently ignore this
+        // because we're likely shutting down.
       } else if (e.code == RpcErrorCodes.kInternalError &&
           e.message.contains('No running isolate (inspector is not set).')) {
         // TODO(bkonyi): remove once https://github.com/flutter/flutter/issues/156793
@@ -432,6 +435,9 @@
       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.isServiceDisposedError) {
+        // The VM service connection was terminated, we can silently ignore this
+        // because we're likely shutting down.
       } else if (e.code == RpcErrorCodes.kMethodNotFound) {
         // Fallback to a regular resume if the DDS service extension isn't
         // available:
diff --git a/pkg/dds/lib/src/dap/utils.dart b/pkg/dds/lib/src/dap/utils.dart
index 553cbb1..4ff90ea 100644
--- a/pkg/dds/lib/src/dap/utils.dart
+++ b/pkg/dds/lib/src/dap/utils.dart
@@ -5,6 +5,9 @@
 import 'dart:io';
 
 import 'package:path/path.dart' as path;
+import 'package:vm_service/vm_service.dart' as vm;
+
+import '../rpc_error_codes.dart';
 
 /// Returns whether this URI is something that can be resolved to a file-like
 /// URI via the VM Service.
@@ -133,3 +136,25 @@
 }
 
 typedef StackFrameLocation = ({Uri uri, int? line, int? column});
+
+extension RpcErrorExtension on vm.RPCError {
+  /// Whether this [vm.RPCError] is some kind of "VM Service connection has gone"
+  /// error that may occur if the VM is shut down.
+  bool get isServiceDisposedError {
+    if (code == RpcErrorCodes.kServiceDisappeared ||
+        code == RpcErrorCodes.kConnectionDisposed) {
+      return true;
+    }
+
+    if (code == RpcErrorCodes.kExtensionError) {
+      // Always ignore "client is closed" and "closed with pending request"
+      // errors because these can always occur during shutdown if we were
+      // just starting to send (or had just sent) a request.
+      return message.contains("The client is closed") ||
+          message.contains("The client closed with pending request") ||
+          message.contains("Service connection disposed");
+    }
+
+    return false;
+  }
+}
diff --git a/pkg/dds/lib/src/rpc_error_codes.dart b/pkg/dds/lib/src/rpc_error_codes.dart
index 56320d7..96a1064 100644
--- a/pkg/dds/lib/src/rpc_error_codes.dart
+++ b/pkg/dds/lib/src/rpc_error_codes.dart
@@ -22,7 +22,7 @@
   static const kInvalidParams = -32602;
   static const kInternalError = -32603;
 
-  // static const kExtensionError = -32000;
+  static const kExtensionError = -32000;
 
   static const kConnectionDisposed = -32010;