Ensure attaching to an application with an existing DDS instance is not treated as a fatal error (#70847)

diff --git a/dev/integration_tests/gradle_deprecated_settings/pubspec.yaml b/dev/integration_tests/gradle_deprecated_settings/pubspec.yaml
index 793897e..efb9fdc 100644
--- a/dev/integration_tests/gradle_deprecated_settings/pubspec.yaml
+++ b/dev/integration_tests/gradle_deprecated_settings/pubspec.yaml
@@ -8,7 +8,7 @@
 dependencies:
   flutter:
     sdk: flutter
-  camera: 0.5.8+11
+  camera: 0.5.8+17
 
   characters: 1.1.0-nullsafety.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   collection: 1.15.0-nullsafety.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
@@ -19,4 +19,4 @@
 flutter:
   uses-material-design: true
 
-# PUBSPEC CHECKSUM: aa03
+# PUBSPEC CHECKSUM: 8f09
diff --git a/dev/tools/pubspec.yaml b/dev/tools/pubspec.yaml
index 9cdc948..995e11a 100644
--- a/dev/tools/pubspec.yaml
+++ b/dev/tools/pubspec.yaml
@@ -20,7 +20,7 @@
   analyzer: 0.40.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   async: 2.5.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   boolean_selector: 2.1.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
-  browser_launcher: 0.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  browser_launcher: 0.1.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   built_collection: 4.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   built_value: 7.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   charcode: 1.2.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
@@ -97,4 +97,4 @@
 
   node_preamble: 1.4.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
 
-# PUBSPEC CHECKSUM: bb6f
+# PUBSPEC CHECKSUM: 7770
diff --git a/packages/flutter_tools/lib/src/base/dds.dart b/packages/flutter_tools/lib/src/base/dds.dart
index 7d052ee..a7d6af4 100644
--- a/packages/flutter_tools/lib/src/base/dds.dart
+++ b/packages/flutter_tools/lib/src/base/dds.dart
@@ -19,7 +19,8 @@
   final Logger logger;
   dds.DartDevelopmentService _ddsInstance;
 
-  Uri get uri => _ddsInstance.uri;
+  Uri get uri => _ddsInstance?.uri ?? _existingDdsUri;
+  Uri _existingDdsUri;
 
   Future<void> get done => _completer.future;
   final Completer<void> _completer = Completer<void>();
@@ -57,6 +58,11 @@
       logger.printTrace('DDS is listening at ${_ddsInstance.uri}.');
     } on dds.DartDevelopmentServiceException catch (e) {
       logger.printTrace('Warning: Failed to start DDS: ${e.message}');
+      if (e.errorCode == dds.DartDevelopmentServiceException.existingDdsInstanceError) {
+        _existingDdsUri = Uri.parse(
+          e.message.split(' ').firstWhere((String e) => e.startsWith('http'))
+        );
+      }
       if (!_completer.isCompleted) {
         _completer.complete();
       }
diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart
index d7dc29d..d10a700 100644
--- a/packages/flutter_tools/lib/src/commands/attach.dart
+++ b/packages/flutter_tools/lib/src/commands/attach.dart
@@ -318,7 +318,14 @@
         try {
           app = await daemon.appDomain.launch(
             runner,
-            runner.attach,
+            ({Completer<DebugConnectionInfo> connectionInfoCompleter,
+              Completer<void> appStartedCompleter}) {
+              return runner.attach(
+                connectionInfoCompleter: connectionInfoCompleter,
+                appStartedCompleter: appStartedCompleter,
+                allowExistingDdsInstance: true,
+              );
+            },
             device,
             null,
             true,
@@ -354,6 +361,7 @@
         }));
         result = await runner.attach(
           appStartedCompleter: onAppStart,
+          allowExistingDdsInstance: true,
         );
         if (result != 0) {
           throwToolExit(null, exitCode: result);
diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart
index 7a016ff..c320c3e 100644
--- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart
+++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart
@@ -748,6 +748,7 @@
   Future<int> attach({
     Completer<DebugConnectionInfo> connectionInfoCompleter,
     Completer<void> appStartedCompleter,
+    bool allowExistingDdsInstance = false,
   }) async {
     if (_chromiumLauncher != null) {
       final Chromium chrome = await _chromiumLauncher.connectedInstance;
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 633c63b..55f74fa 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 
+import 'package:dds/dds.dart' as dds;
 import 'package:meta/meta.dart';
 import 'package:package_config/package_config.dart';
 import 'package:vm_service/vm_service.dart' as vm_service;
@@ -221,6 +222,7 @@
     int ddsPort,
     bool disableServiceAuthCodes = false,
     bool disableDds = false,
+    bool allowExistingDdsInstance = false,
     bool ipv6 = false,
   }) {
     final Completer<void> completer = Completer<void>();
@@ -231,8 +233,15 @@
       // FYI, this message is used as a sentinel in tests.
       globals.printTrace('Connecting to service protocol: $observatoryUri');
       isWaitingForVm = true;
+      bool existingDds = false;
       vm_service.VmService service;
       if (!disableDds) {
+        void handleError(Exception e) {
+          globals.printTrace('Fail to connect to service protocol: $observatoryUri: $e');
+          if (!completer.isCompleted) {
+            completer.completeError('failed to connect to $observatoryUri');
+          }
+        }
         // This first try block is meant to catch errors that occur during DDS startup
         // (e.g., failure to bind to a port, failure to connect to the VM service,
         // attaching to a VM service with existing clients, etc.).
@@ -243,11 +252,16 @@
             ipv6,
             disableServiceAuthCodes,
           );
-        } on Exception catch (e) {
-          globals.printTrace('Fail to connect to service protocol: $observatoryUri: $e');
-          if (!completer.isCompleted && !_isListeningForObservatoryUri) {
-            completer.completeError('failed to connect to $observatoryUri');
+        } on dds.DartDevelopmentServiceException catch (e) {
+          if (!allowExistingDdsInstance ||
+              (e.errorCode != dds.DartDevelopmentServiceException.existingDdsInstanceError)) {
+            handleError(e);
+            return;
+          } else {
+            existingDds = true;
           }
+        } on Exception catch (e) {
+          handleError(e);
           return;
         }
       }
@@ -267,7 +281,8 @@
               printStructuredErrorLogMethod: printStructuredErrorLogMethod,
               device: device,
             ),
-            device.dds.done.whenComplete(() => throw Exception('DDS shut down too early')),
+            if (!existingDds)
+              device.dds.done.whenComplete(() => throw Exception('DDS shut down too early')),
           ]
         ) as vm_service.VmService;
       } on Exception catch (exception) {
@@ -849,6 +864,7 @@
   Future<int> attach({
     Completer<DebugConnectionInfo> connectionInfoCompleter,
     Completer<void> appStartedCompleter,
+    bool allowExistingDdsInstance = false,
   });
 
   bool get supportsRestart => false;
@@ -1201,6 +1217,7 @@
     Restart restart,
     CompileExpression compileExpression,
     GetSkSLMethod getSkSLMethod,
+    bool allowExistingDdsInstance,
   }) async {
     if (!debuggingOptions.debuggingEnabled) {
       throw 'The service protocol is not enabled.';
@@ -1214,6 +1231,7 @@
         compileExpression: compileExpression,
         disableDds: debuggingOptions.disableDds,
         ddsPort: debuggingOptions.ddsPort,
+        allowExistingDdsInstance: allowExistingDdsInstance,
         hostVmServicePort: debuggingOptions.hostVmServicePort,
         getSkSLMethod: getSkSLMethod,
         printStructuredErrorLogMethod: printStructuredErrorLog,
diff --git a/packages/flutter_tools/lib/src/run_cold.dart b/packages/flutter_tools/lib/src/run_cold.dart
index 7576240..6ce44a1 100644
--- a/packages/flutter_tools/lib/src/run_cold.dart
+++ b/packages/flutter_tools/lib/src/run_cold.dart
@@ -128,11 +128,13 @@
   Future<int> attach({
     Completer<DebugConnectionInfo> connectionInfoCompleter,
     Completer<void> appStartedCompleter,
+    bool allowExistingDdsInstance = false,
   }) async {
     _didAttach = true;
     try {
       await connectToServiceProtocol(
         getSkSLMethod: writeSkSL,
+        allowExistingDdsInstance: allowExistingDdsInstance,
       );
     } on Exception catch (error) {
       globals.printError('Error connecting to the service protocol: $error');
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index f81e1d6..fbb6cff 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -171,6 +171,7 @@
   Future<int> attach({
     Completer<DebugConnectionInfo> connectionInfoCompleter,
     Completer<void> appStartedCompleter,
+    bool allowExistingDdsInstance = false,
   }) async {
     _didAttach = true;
     try {
@@ -179,6 +180,7 @@
         restart: _restartService,
         compileExpression: _compileExpressionService,
         getSkSLMethod: writeSkSL,
+        allowExistingDdsInstance: allowExistingDdsInstance,
       );
     // Catches all exceptions, non-Exception objects are rethrown.
     } catch (error) { // ignore: avoid_catches_without_on_clauses
diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml
index 31c3939..ab33da8 100644
--- a/packages/flutter_tools/pubspec.yaml
+++ b/packages/flutter_tools/pubspec.yaml
@@ -62,7 +62,7 @@
   _fe_analyzer_shared: 12.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   analyzer: 0.40.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   boolean_selector: 2.1.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
-  browser_launcher: 0.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  browser_launcher: 0.1.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   built_collection: 4.3.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   built_value: 7.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
   charcode: 1.2.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
@@ -111,4 +111,4 @@
   # Exclude this package from the hosted API docs.
   nodoc: true
 
-# PUBSPEC CHECKSUM: 2f49
+# PUBSPEC CHECKSUM: 8b4a
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
index 725cf8b..1b69f13 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
@@ -179,7 +179,7 @@
         const String outputDill = '/tmp/output.dill';
 
         final MockHotRunner mockHotRunner = MockHotRunner();
-        when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter')))
+        when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter'), allowExistingDdsInstance: true))
             .thenAnswer((_) async => 0);
         when(mockHotRunner.exited).thenReturn(false);
         when(mockHotRunner.isWaitingForObservatory).thenReturn(false);
@@ -311,7 +311,7 @@
         .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
       when(portForwarder.unforward(any))
         .thenAnswer((_) async {});
-      when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter')))
+      when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter'), allowExistingDdsInstance: true))
         .thenAnswer((_) async => 0);
       when(mockHotRunnerFactory.build(
         any,
@@ -395,7 +395,7 @@
         .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
       when(portForwarder.unforward(any))
         .thenAnswer((_) async {});
-      when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter')))
+      when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter'), allowExistingDdsInstance: true))
         .thenAnswer((_) async => 0);
       when(mockHotRunnerFactory.build(
         any,
diff --git a/packages/flutter_tools/test/general.shard/cold_test.dart b/packages/flutter_tools/test/general.shard/cold_test.dart
index 96afacc..81a2494 100644
--- a/packages/flutter_tools/test/general.shard/cold_test.dart
+++ b/packages/flutter_tools/test/general.shard/cold_test.dart
@@ -129,6 +129,7 @@
     int hostVmServicePort,
     int ddsPort,
     bool ipv6 = false,
+    bool allowExistingDdsInstance = false,
   }) async {
     throw exception;
   }
diff --git a/packages/flutter_tools/test/general.shard/hot_test.dart b/packages/flutter_tools/test/general.shard/hot_test.dart
index 4afe0ce..a18097f 100644
--- a/packages/flutter_tools/test/general.shard/hot_test.dart
+++ b/packages/flutter_tools/test/general.shard/hot_test.dart
@@ -598,6 +598,7 @@
     bool ipv6 = false,
     int hostVmServicePort,
     int ddsPort,
+    bool allowExistingDdsInstance = false,
   }) async {
     throw exception;
   }
diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
index a9fa3c1..aaaab19 100644
--- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
@@ -10,6 +10,7 @@
 import 'package:meta/meta.dart';
 import 'package:package_config/package_config.dart';
 import 'package:vm_service/vm_service.dart' as vm_service;
+import 'package:dds/dds.dart' as dds;
 import 'package:file/memory.dart';
 import 'package:file_testing/file_testing.dart';
 import 'package:flutter_tools/src/artifacts.dart';
@@ -2673,6 +2674,36 @@
     }) async => mockVMService,
   }));
 
+  testUsingContext('FlutterDevice handles existing DDS instance', () => testbed.run(() async {
+    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
+    final MockDevice mockDevice = MockDevice();
+    final MockDartDevelopmentService mockDds = MockDartDevelopmentService();
+    final MockDeviceLogReader mockLogReader = MockDeviceLogReader();
+    final Completer<void> noopCompleter = Completer<void>();
+    when(mockDevice.getLogReader(app: anyNamed('app'))).thenReturn(mockLogReader);
+    when(mockDevice.dds).thenReturn(mockDds);
+    when(mockDds.startDartDevelopmentService(any, any, any, any)).thenThrow(FakeDartDevelopmentServiceException());
+    when(mockDds.done).thenAnswer((_) => noopCompleter.future);
+
+    final TestFlutterDevice flutterDevice = TestFlutterDevice(
+      mockDevice,
+      observatoryUris: Stream<Uri>.value(testUri),
+    );
+    await flutterDevice.connect(allowExistingDdsInstance: true);
+    verify(mockLogReader.connectedVMService = mockVMService);
+  }, overrides: <Type, Generator>{
+    VMServiceConnector: () => (Uri httpUri, {
+      ReloadSources reloadSources,
+      Restart restart,
+      CompileExpression compileExpression,
+      GetSkSLMethod getSkSLMethod,
+      PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
+      io.CompressionOptions compression,
+      Device device,
+    }) async => mockVMService,
+  }));
+
+
   testUsingContext('nextPlatform moves through expected platforms', () {
     expect(nextPlatform('android', TestFeatureFlags()), 'iOS');
     expect(nextPlatform('iOS', TestFeatureFlags()), 'fuchsia');
@@ -2694,6 +2725,14 @@
 class MockProcessManager extends Mock implements ProcessManager {}
 class MockResidentCompiler extends Mock implements ResidentCompiler {}
 
+class FakeDartDevelopmentServiceException implements dds.DartDevelopmentServiceException {
+  @override
+  final int errorCode = dds.DartDevelopmentServiceException.existingDdsInstanceError;
+
+  @override
+  final String message = 'A DDS instance is already connected at http://localhost:8181';
+}
+
 class TestFlutterDevice extends FlutterDevice {
   TestFlutterDevice(Device device, { Stream<Uri> observatoryUris })
     : super(device, buildInfo: BuildInfo.debug) {
@@ -2737,6 +2776,7 @@
     int hostVmServicePort,
     int ddsPort,
     PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
+    bool allowExistingDdsInstance = false,
   }) async { }
 
 
diff --git a/packages/flutter_tools/test/general.shard/terminal_handler_test.dart b/packages/flutter_tools/test/general.shard/terminal_handler_test.dart
index 1183d55..0710fb8 100644
--- a/packages/flutter_tools/test/general.shard/terminal_handler_test.dart
+++ b/packages/flutter_tools/test/general.shard/terminal_handler_test.dart
@@ -353,5 +353,6 @@
   Future<int> attach({
     Completer<DebugConnectionInfo> connectionInfoCompleter,
     Completer<void> appStartedCompleter,
+    bool allowExistingDdsInstance = false,
   }) async => null;
 }