Skip to content

Commit f19b06c

Browse files
committed
[ Tool ] Fix hot restart hanging when target application is in an infinite loop
Partial fix to #22214
1 parent b3cd06d commit f19b06c

4 files changed

Lines changed: 78 additions & 60 deletions

File tree

‎packages/flutter_tools/lib/src/run_hot.dart‎

Lines changed: 3 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -625,68 +625,12 @@ class HotRunner extends ResidentRunner {
625625
device.generator!.accept();
626626
}
627627
}
628-
// Check if the isolate is paused and resume it.
629628
final List<Future<void>> operations = <Future<void>>[];
630629
for (final FlutterDevice? device in flutterDevices) {
631-
final Set<String?> uiIsolatesIds = <String?>{};
632-
final List<FlutterView> views = await device!.vmService!.getFlutterViews();
633-
for (final FlutterView view in views) {
634-
if (view.uiIsolate == null) {
635-
continue;
636-
}
637-
uiIsolatesIds.add(view.uiIsolate!.id);
638-
// Reload the isolate.
639-
final Future<vm_service.Isolate?> reloadIsolate = device.vmService!.getIsolateOrNull(
640-
view.uiIsolate!.id!,
641-
);
642-
operations.add(
643-
reloadIsolate.then((vm_service.Isolate? isolate) async {
644-
if (isolate != null) {
645-
// The embedder requires that the isolate is unpaused, because the
646-
// runInView method requires interaction with dart engine APIs that
647-
// are not thread-safe, and thus must be run on the same thread that
648-
// would be blocked by the pause. Simply un-pausing is not sufficient,
649-
// because this does not prevent the isolate from immediately hitting
650-
// a breakpoint (for example if the breakpoint was placed in a loop
651-
// or in a frequently called method) or an exception. Instead, all
652-
// breakpoints are first disabled and exception pause mode set to
653-
// None, and then the isolate resumed.
654-
// These settings do not need restoring as Hot Restart results in
655-
// new isolates, which will be configured by the editor as they are
656-
// started.
657-
final List<Future<void>> breakpointAndExceptionRemoval = <Future<void>>[
658-
device.vmService!.service.setIsolatePauseMode(
659-
isolate.id!,
660-
exceptionPauseMode: vm_service.ExceptionPauseMode.kNone,
661-
),
662-
for (final vm_service.Breakpoint breakpoint in isolate.breakpoints!)
663-
device.vmService!.service.removeBreakpoint(isolate.id!, breakpoint.id!),
664-
];
665-
await Future.wait(breakpointAndExceptionRemoval);
666-
if (isPauseEvent(isolate.pauseEvent!.kind!)) {
667-
await device.vmService!.service.resume(view.uiIsolate!.id!);
668-
}
669-
}
670-
}),
671-
);
672-
}
673-
674-
// Wait for the UI isolates to have their breakpoints removed and exception pause mode
675-
// cleared while also ensuring the isolate's are no longer paused. If we don't clear
676-
// the exception pause mode before we start killing child isolates, it's possible that
677-
// any UI isolate waiting on a result from a child isolate could throw an unhandled
678-
// exception and re-pause the isolate, causing hot restart to hang.
679-
await Future.wait(operations);
680-
operations.clear();
681-
682-
// The engine handles killing and recreating isolates that it has spawned
683-
// ("uiIsolates"). The isolates that were spawned from these uiIsolates
684-
// will not be restarted, and so they must be manually killed.
685-
final vm_service.VM vm = await device.vmService!.service.getVM();
630+
// Explicitly kill all the isolates in the application, otherwise we need to account for
631+
// special cases that the engine can't handle (e.g., paused isolates, infinite loops, etc.).
632+
final vm_service.VM vm = await device!.vmService!.service.getVM();
686633
for (final vm_service.IsolateRef isolateRef in vm.isolates!) {
687-
if (uiIsolatesIds.contains(isolateRef.id)) {
688-
continue;
689-
}
690634
operations.add(
691635
device.vmService!.service
692636
.kill(isolateRef.id!)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:file/file.dart';
6+
7+
import '../src/common.dart';
8+
import 'test_data/infinite_loop_project.dart';
9+
import 'test_driver.dart';
10+
import 'test_utils.dart';
11+
12+
void main() {
13+
late Directory tempDir;
14+
final InfiniteLoopProject project = InfiniteLoopProject();
15+
late FlutterRunTestDriver flutter;
16+
17+
setUp(() async {
18+
tempDir = createResolvedTempDirectorySync('hot_restart_test.');
19+
await project.setUpIn(tempDir);
20+
flutter = FlutterRunTestDriver(tempDir);
21+
});
22+
23+
tearDown(() async {
24+
await flutter.stop();
25+
tryToDelete(tempDir);
26+
});
27+
28+
// Possible regression test for https://github.com/flutter/flutter/issues/161466
29+
testWithoutContext("Hot restart doesn't hang when UI isolate is in an infinite loop", () async {
30+
await flutter.run(withDebugger: true);
31+
32+
// This call will fail to return if the hot restart hangs.
33+
await flutter.hotRestart();
34+
});
35+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'project.dart';
6+
7+
// Reproduction case from
8+
// https://github.com/flutter/flutter/issues/161466#issuecomment-2743309718.
9+
class InfiniteLoopProject extends Project {
10+
@override
11+
final String pubspec = '''
12+
name: test
13+
environment:
14+
sdk: ^3.7.0-0
15+
16+
dependencies:
17+
flutter:
18+
sdk: flutter
19+
''';
20+
21+
@override
22+
final String main = r'''
23+
import 'dart:async';
24+
import 'dart:developer';
25+
import 'dart:isolate';
26+
27+
import 'package:flutter/material.dart';
28+
29+
void main() {
30+
runApp(
31+
Builder(builder: (context) {
32+
while (true) {
33+
// Loop forever.
34+
}
35+
}),
36+
);
37+
}
38+
''';
39+
}

‎packages/flutter_tools/test/integration.shard/test_utils.dart‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const FileSystem fileSystem = LocalFileSystem();
1919
final String flutterBin = fileSystem.path.join(
2020
getFlutterRoot(),
2121
'bin',
22-
platform.isWindows ? 'flutter.bat' : 'flutter',
22+
platform.isWindows ? 'flutter-dev.bat' : 'flutter-dev',
2323
);
2424

2525
/// The [Platform] for the integration test environment.

0 commit comments

Comments
 (0)