[CP-stable][ Hot Restart ] Fix possible hang due to unhandled exception in UI isolates on hot restart (#166064)

This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request)
Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request.

### Issue Link:

https://github.com/flutter/flutter/issues/161466

### Changelog Description:

[flutter/161466](https://github.com/flutter/flutter/issues/161466): Fixed issue where hot restart could hang indefinitely if "Pause on Unhandled Exceptions" was enabled and a call to `Isolate.run` had not completed.

### Impact Description:

Hot restart (and the Dart-Code extension) could end up in a bad state where hot restart never completes and interacting with the application via the Dart-Code extension doesn't work as expected. The application becomes unresponsive and must be fully restarted to continue development.

`Isolate.run` is used to load license files in the background, meaning that users don't need to explicitly be spawning isolates to encounter this issue.

### Workaround:
Is there a workaround for this issue?

Explicitly disable "Pause on Unhandled Exceptions", which is typically enabled by default.

### Risk:
What is the risk level of this cherry-pick?

### Test Coverage:
Are you confident that your fix is well-tested by automated tests?

### Validation Steps:
What are the steps to validate that this fix works?

1. Create a Flutter project with the following `main.dart`:

```dart

  import 'dart:async';
  import 'dart:developer';
  import 'dart:isolate';

  import 'package:flutter/material.dart';

  void main() {
    WidgetsFlutterBinding.ensureInitialized().platformDispatcher.onError = (Object error, StackTrace? stack) {
      return true;
    };
    runApp(
      const Center(
        child: Text(
          'Hello, world!',
          key: Key('title'),
          textDirection: TextDirection.ltr,
        ),
      ),
    );

    Isolate.run(() {
      print('COMPUTING');
      debugger();
    });
  }
```

2. Run the application in debug mode and perform a hot restart once `COMPUTING` appears on `stdout`. Hot restart should complete successfully.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 599b13b..dbee234 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,7 @@
 - [flutter/165166](https://github.com/flutter/flutter/pull/165166) - Impeller, All platforms, Text that is scaled over 48x renders incorrectly.
 - [flutter/163627](https://github.com/flutter/flutter/pull/163627) - Fix issue where placeholder types in ARB localizations weren't used for type inference, causing a possible type mismatch with the placeholder field defined in the template.
 - [flutter/165166](https://github.com/flutter/flutter/pull/165166) - Update CI configurations and tests to use Xcode 16 and iOS 18 simulator.
+- [flutter/161466](https://github.com/flutter/flutter/pull/161466) - Hot restart can hang on all platforms if "Pause on Unhandled Exceptions" is enabled by the debugger and a call to `compute` or `Isolate.run` has not completed.
 
 ### [3.29.2](https://github.com/flutter/flutter/releases/tag/3.29.2)
 
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 011c6e5..e463502 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -645,7 +645,7 @@
         );
         operations.add(
           reloadIsolate.then((vm_service.Isolate? isolate) async {
-            if ((isolate != null) && isPauseEvent(isolate.pauseEvent!.kind!)) {
+            if (isolate != null) {
               // The embedder requires that the isolate is unpaused, because the
               // runInView method requires interaction with dart engine APIs that
               // are not thread-safe, and thus must be run on the same thread that
@@ -655,7 +655,7 @@
               // or in a frequently called method) or an exception. Instead, all
               // breakpoints are first disabled and exception pause mode set to
               // None, and then the isolate resumed.
-              // These settings to not need restoring as Hot Restart results in
+              // These settings do not need restoring as Hot Restart results in
               // new isolates, which will be configured by the editor as they are
               // started.
               final List<Future<void>> breakpointAndExceptionRemoval = <Future<void>>[
@@ -667,12 +667,22 @@
                   device.vmService!.service.removeBreakpoint(isolate.id!, breakpoint.id!),
               ];
               await Future.wait(breakpointAndExceptionRemoval);
-              await device.vmService!.service.resume(view.uiIsolate!.id!);
+              if (isPauseEvent(isolate.pauseEvent!.kind!)) {
+                await device.vmService!.service.resume(view.uiIsolate!.id!);
+              }
             }
           }),
         );
       }
 
+      // Wait for the UI isolates to have their breakpoints removed and exception pause mode
+      // cleared while also ensuring the isolate's are no longer paused. If we don't clear
+      // the exception pause mode before we start killing child isolates, it's possible that
+      // any UI isolate waiting on a result from a child isolate could throw an unhandled
+      // exception and re-pause the isolate, causing hot restart to hang.
+      await Future.wait(operations);
+      operations.clear();
+
       // The engine handles killing and recreating isolates that it has spawned
       // ("uiIsolates"). The isolates that were spawned from these uiIsolates
       // will not be restarted, and so they must be manually killed.
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 2f49b42..2f4dd0a 100644
--- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
@@ -241,6 +241,14 @@
             jsonResponse: fakeUnpausedIsolate.toJson(),
           ),
           FakeVmServiceRequest(
+            method: 'setIsolatePauseMode',
+            args: <String, Object?>{
+              'isolateId': fakeUnpausedIsolate.id,
+              'exceptionPauseMode': vm_service.ExceptionPauseMode.kNone,
+            },
+            jsonResponse: vm_service.Success().toJson(),
+          ),
+          FakeVmServiceRequest(
             method: 'getVM',
             jsonResponse: vm_service.VM.parse(<String, Object>{})!.toJson(),
           ),
@@ -840,6 +848,14 @@
             jsonResponse: fakeUnpausedIsolate.toJson(),
           ),
           FakeVmServiceRequest(
+            method: 'setIsolatePauseMode',
+            args: <String, Object?>{
+              'isolateId': fakeUnpausedIsolate.id,
+              'exceptionPauseMode': vm_service.ExceptionPauseMode.kNone,
+            },
+            jsonResponse: vm_service.Success().toJson(),
+          ),
+          FakeVmServiceRequest(
             method: 'getVM',
             jsonResponse: vm_service.VM.parse(<String, Object>{})!.toJson(),
           ),
@@ -916,18 +932,28 @@
             jsonResponse: fakePausedIsolate.toJson(),
           ),
           FakeVmServiceRequest(
+            method: 'setIsolatePauseMode',
+            args: <String, Object?>{
+              'isolateId': fakeUnpausedIsolate.id,
+              'exceptionPauseMode': vm_service.ExceptionPauseMode.kNone,
+            },
+            jsonResponse: vm_service.Success().toJson(),
+          ),
+          FakeVmServiceRequest(
+            method: 'removeBreakpoint',
+            args: <String, Object?>{
+              'isolateId': fakeUnpausedIsolate.id,
+              'breakpointId': 'test-breakpoint',
+            },
+          ),
+          FakeVmServiceRequest(
+            method: 'resume',
+            args: <String, Object?>{'isolateId': fakeUnpausedIsolate.id},
+          ),
+          FakeVmServiceRequest(
             method: 'getVM',
             jsonResponse: vm_service.VM.parse(<String, Object>{})!.toJson(),
           ),
-          const FakeVmServiceRequest(
-            method: 'setIsolatePauseMode',
-            args: <String, String>{'isolateId': '1', 'exceptionPauseMode': 'None'},
-          ),
-          const FakeVmServiceRequest(
-            method: 'removeBreakpoint',
-            args: <String, String>{'isolateId': '1', 'breakpointId': 'test-breakpoint'},
-          ),
-          const FakeVmServiceRequest(method: 'resume', args: <String, String>{'isolateId': '1'}),
           listViews,
           const FakeVmServiceRequest(
             method: 'streamListen',
@@ -978,6 +1004,14 @@
             jsonResponse: fakeUnpausedIsolate.toJson(),
           ),
           FakeVmServiceRequest(
+            method: 'setIsolatePauseMode',
+            args: <String, Object?>{
+              'isolateId': fakeUnpausedIsolate.id,
+              'exceptionPauseMode': vm_service.ExceptionPauseMode.kNone,
+            },
+            jsonResponse: vm_service.Success().toJson(),
+          ),
+          FakeVmServiceRequest(
             method: 'getVM',
             jsonResponse: vm_service.VM.parse(<String, Object>{})!.toJson(),
           ),
@@ -1005,6 +1039,14 @@
             jsonResponse: fakeUnpausedIsolate.toJson(),
           ),
           FakeVmServiceRequest(
+            method: 'setIsolatePauseMode',
+            args: <String, Object?>{
+              'isolateId': fakeUnpausedIsolate.id,
+              'exceptionPauseMode': vm_service.ExceptionPauseMode.kNone,
+            },
+            jsonResponse: vm_service.Success().toJson(),
+          ),
+          FakeVmServiceRequest(
             method: 'getVM',
             jsonResponse: vm_service.VM.parse(<String, Object>{})!.toJson(),
           ),
@@ -1032,6 +1074,14 @@
             jsonResponse: fakeUnpausedIsolate.toJson(),
           ),
           FakeVmServiceRequest(
+            method: 'setIsolatePauseMode',
+            args: <String, Object?>{
+              'isolateId': fakeUnpausedIsolate.id,
+              'exceptionPauseMode': vm_service.ExceptionPauseMode.kNone,
+            },
+            jsonResponse: vm_service.Success().toJson(),
+          ),
+          FakeVmServiceRequest(
             method: 'getVM',
             jsonResponse: vm_service.VM.parse(<String, Object>{})!.toJson(),
           ),
diff --git a/packages/flutter_tools/test/integration.shard/hot_restart_with_unhandled_exception_test.dart b/packages/flutter_tools/test/integration.shard/hot_restart_with_unhandled_exception_test.dart
new file mode 100644
index 0000000..181ada6
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/hot_restart_with_unhandled_exception_test.dart
@@ -0,0 +1,63 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:file/file.dart';
+import 'package:vm_service/vm_service.dart';
+import 'package:vm_service/vm_service_io.dart';
+
+import '../src/common.dart';
+import 'test_data/hot_restart_with_paused_child_isolate_project.dart';
+import 'test_driver.dart';
+import 'test_utils.dart';
+
+void main() {
+  late Directory tempDir;
+  final HotRestartWithPausedChildIsolateProject project = HotRestartWithPausedChildIsolateProject();
+  late FlutterRunTestDriver flutter;
+
+  setUp(() async {
+    tempDir = createResolvedTempDirectorySync('hot_restart_test.');
+    await project.setUpIn(tempDir);
+    flutter = FlutterRunTestDriver(tempDir);
+  });
+
+  tearDown(() async {
+    await flutter.stop();
+    tryToDelete(tempDir);
+  });
+
+  // Possible regression test for https://github.com/flutter/flutter/issues/161466
+  testWithoutContext("Hot restart doesn't hang when an unhandled exception is "
+      'thrown in the UI isolate', () async {
+    await flutter.run(withDebugger: true, startPaused: true, pauseOnExceptions: true);
+    final VmService vmService = await vmServiceConnectUri(flutter.vmServiceWsUri.toString());
+    final Isolate root = await flutter.getFlutterIsolate();
+
+    // The UI isolate has already started paused. Setup a listener for the
+    // child isolate that will spawn when the isolate resumes. Resume the
+    // spawned child which will pause on start, and then wait for it to execute
+    // the `debugger()` call.
+    final Completer<void> childIsolatePausedCompleter = Completer<void>();
+    vmService.onDebugEvent.listen((Event event) async {
+      if (event.kind == EventKind.kPauseStart) {
+        await vmService.resume(event.isolate!.id!);
+      } else if (event.kind == EventKind.kPauseBreakpoint) {
+        if (!childIsolatePausedCompleter.isCompleted) {
+          await vmService.streamCancel(EventStreams.kDebug);
+          childIsolatePausedCompleter.complete();
+        }
+      }
+    });
+    await vmService.streamListen(EventStreams.kDebug);
+
+    await vmService.resume(root.id!);
+    await childIsolatePausedCompleter.future;
+
+    // This call will fail to return if the UI isolate pauses on an unhandled
+    // exception due to the isolate spawned by `Isolate.run` not completing.
+    await flutter.hotRestart();
+  });
+}
diff --git a/packages/flutter_tools/test/integration.shard/test_data/hot_restart_with_paused_child_isolate_project.dart b/packages/flutter_tools/test/integration.shard/test_data/hot_restart_with_paused_child_isolate_project.dart
new file mode 100644
index 0000000..df057e7
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/test_data/hot_restart_with_paused_child_isolate_project.dart
@@ -0,0 +1,50 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'project.dart';
+
+// Reproduction case from
+// https://github.com/flutter/flutter/issues/161466#issuecomment-2743309718.
+class HotRestartWithPausedChildIsolateProject extends Project {
+  @override
+  final String pubspec = '''
+  name: test
+  environment:
+    sdk: ^3.7.0-0
+
+  dependencies:
+    flutter:
+      sdk: flutter
+  ''';
+
+  @override
+  final String main = r'''
+  import 'dart:async';
+  import 'dart:developer';
+  import 'dart:isolate';
+
+  import 'package:flutter/material.dart';
+
+  void main() {
+    WidgetsFlutterBinding.ensureInitialized().platformDispatcher.onError = (Object error, StackTrace? stack) {
+      print('HERE');
+      return true;
+    };
+    runApp(
+      const Center(
+        child: Text(
+          'Hello, world!',
+          key: Key('title'),
+          textDirection: TextDirection.ltr,
+        ),
+      ),
+    );
+
+    Isolate.run(() {
+      print('COMPUTING');
+      debugger();
+    });
+  }
+  ''';
+}
diff --git a/packages/flutter_tools/test/integration.shard/test_driver.dart b/packages/flutter_tools/test/integration.shard/test_driver.dart
index 778bfe4..0dd6ab3 100644
--- a/packages/flutter_tools/test/integration.shard/test_driver.dart
+++ b/packages/flutter_tools/test/integration.shard/test_driver.dart
@@ -148,7 +148,9 @@
     final Completer<void> isolateStarted = Completer<void>();
     _vmService!.onIsolateEvent.listen((Event event) {
       if (event.kind == EventKind.kIsolateStart) {
-        isolateStarted.complete();
+        if (!isolateStarted.isCompleted) {
+          isolateStarted.complete();
+        }
       } else if (event.kind == EventKind.kIsolateExit && event.isolate?.id == _flutterIsolateId) {
         // Hot restarts cause all the isolates to exit, so we need to refresh
         // our idea of what the Flutter isolate ID is.