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

Possible fix for https://github.com/flutter/flutter/issues/161466
This commit is contained in:
Ben Konyi
2025-03-26 12:50:07 -04:00
committed by GitHub
parent 6b02598b80
commit 8297e44993
5 changed files with 188 additions and 13 deletions

View File

@@ -645,7 +645,7 @@ class HotRunner extends ResidentRunner {
);
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 @@ class HotRunner extends ResidentRunner {
// 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 @@ class HotRunner extends ResidentRunner {
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.

View File

@@ -239,6 +239,14 @@ void main() {
args: <String, Object?>{'isolateId': fakeUnpausedIsolate.id},
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(),
@@ -785,6 +793,14 @@ void main() {
args: <String, Object?>{'isolateId': fakeUnpausedIsolate.id},
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(),
@@ -851,19 +867,29 @@ void main() {
args: <String, Object?>{'isolateId': fakeUnpausedIsolate.id},
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',
@@ -913,6 +939,14 @@ void main() {
args: <String, Object?>{'isolateId': fakeUnpausedIsolate.id},
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(),
@@ -940,6 +974,14 @@ void main() {
args: <String, Object?>{'isolateId': fakeUnpausedIsolate.id},
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(),
@@ -967,6 +1009,14 @@ void main() {
args: <String, Object?>{'isolateId': fakeUnpausedIsolate.id},
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(),

View File

@@ -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();
});
}

View File

@@ -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();
});
}
''';
}

View File

@@ -148,7 +148,9 @@ abstract final class FlutterTestDriver {
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.