forked from firka/flutter
[ 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:
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
''';
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user