Disable pause-on-exceptions for (outgoing) isolates during hot restart (#93411)

This commit is contained in:
Danny Tuppeny 2021-11-19 06:23:09 +00:00 committed by GitHub
parent e4b78ffc43
commit 99e85b1c5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 120 additions and 8 deletions

View file

@ -603,14 +603,19 @@ class HotRunner extends ResidentRunner {
// are not thread-safe, and thus must be run on the same thread that
// would be blocked by the pause. Simply un-pausing is not sufficient,
// because this does not prevent the isolate from immediately hitting
// a breakpoint, for example if the breakpoint was placed in a loop
// or in a frequently called method. Instead, all breakpoints are first
// disabled and then the isolate resumed.
final List<Future<void>> breakpointRemoval = <Future<void>>[
// a breakpoint (for example if the breakpoint was placed in a loop
// 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
// new isolates, which will be configured by the editor as they are
// started.
final List<Future<void>> breakpointAndExceptionRemoval = <Future<void>>[
device.vmService.service.setExceptionPauseMode(isolate.id, 'None'),
for (final vm_service.Breakpoint breakpoint in isolate.breakpoints)
device.vmService.service.removeBreakpoint(isolate.id, breakpoint.id)
];
await Future.wait(breakpointRemoval);
await Future.wait(breakpointAndExceptionRemoval);
await device.vmService.service.resume(view.uiIsolate.id);
}
}));

View file

@ -906,7 +906,7 @@ void main() {
Usage: () => TestUsage(),
}));
testUsingContext('ResidentRunner can remove breakpoints from paused isolate during hot restart', () => testbed.run(() async {
testUsingContext('ResidentRunner can remove breakpoints and exception-pause-mode from paused isolate during hot restart', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
listViews,
@ -922,6 +922,13 @@ void main() {
method: 'getVM',
jsonResponse: vm_service.VM.parse(<String, Object>{}).toJson(),
),
const FakeVmServiceRequest(
method: 'setExceptionPauseMode',
args: <String, String>{
'isolateId': '1',
'mode': 'None',
}
),
const FakeVmServiceRequest(
method: 'removeBreakpoint',
args: <String, String>{

View file

@ -187,6 +187,39 @@ void main() {
await dap.client.terminate();
});
testWithoutContext('can hot restart when exceptions occur on outgoing isolates', () async {
final BasicProjectThatThrows _project = BasicProjectThatThrows();
await _project.setUpIn(tempDir);
// Launch the app and wait for it to stop at an exception.
int originalThreadId, newThreadId;
await Future.wait(<Future<Object>>[
// Capture the thread ID of the stopped thread.
dap.client.stoppedEvents.first.then((StoppedEventBody event) => originalThreadId = event.threadId),
dap.client.start(
exceptionPauseMode: 'All', // Ensure we stop on all exceptions
launch: () => dap.client.launch(
cwd: _project.dir.path,
toolArgs: <String>['-d', 'flutter-tester'],
),
),
], eagerError: true);
// Hot restart, ensuring it completes and capturing the ID of the new thread
// to pause.
await Future.wait(<Future<Object>>[
// Capture the thread ID of the newly stopped thread.
dap.client.stoppedEvents.first.then((StoppedEventBody event) => newThreadId = event.threadId),
dap.client.hotRestart(),
], eagerError: true);
// We should not have stopped on the original thread, but the new thread
// from after the restart.
expect(newThreadId, isNot(equals(originalThreadId)));
await dap.client.terminate();
});
}
/// Extracts the output from a set of [OutputEventBody], removing any

View file

@ -56,6 +56,10 @@ class DapTestClient {
Stream<OutputEventBody> get outputEvents => events('output')
.map((Event e) => OutputEventBody.fromJson(e.body! as Map<String, Object?>));
/// Returns a stream of [StoppedEventBody] events.
Stream<StoppedEventBody> get stoppedEvents => events('stopped')
.map((Event e) => StoppedEventBody.fromJson(e.body! as Map<String, Object?>));
/// Returns a stream of the string output from [OutputEventBody] events.
Stream<String> get output => outputEvents.map((OutputEventBody output) => output.output);
@ -172,10 +176,11 @@ class DapTestClient {
Future<void> start({
String? program,
String? cwd,
String exceptionPauseMode = 'None',
Future<Object?> Function()? launch,
}) {
return Future.wait(<Future<Object?>>[
initialize(),
initialize(exceptionPauseMode: exceptionPauseMode),
launch?.call() ?? this.launch(program: program, cwd: cwd),
], eagerError: true);
}
@ -201,7 +206,7 @@ class DapTestClient {
} else {
completer.completeError(message);
}
} else if (message is Event) {
} else if (message is Event && !_eventController.isClosed) {
_eventController.add(message);
// When we see a terminated event, close the event stream so if any

View file

@ -53,6 +53,68 @@ class BasicProject extends Project {
int get topLevelFunctionBreakpointLine => lineContaining(main, '// TOP LEVEL BREAKPOINT');
}
/// A project that throws multiple exceptions during Widget builds.
///
/// A repro for the issue at https://github.com/Dart-Code/Dart-Code/issues/3448
/// where Hot Restart could become stuck on exceptions and never complete.
class BasicProjectThatThrows extends Project {
@override
final String pubspec = '''
name: test
environment:
sdk: ">=2.12.0-0 <3.0.0"
dependencies:
flutter:
sdk: flutter
''';
@override
final String main = r'''
import 'package:flutter/material.dart';
void a() {
throw Exception('a');
}
void b() {
try {
a();
} catch (e) {
throw Exception('b');
}
}
void c() {
try {
b();
} catch (e) {
throw Exception('c');
}
}
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
c();
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Study Flutter',
theme: ThemeData(
primarySwatch: Colors.green,
),
home: Container(),
);
}
}
''';
}
class BasicProjectWithTimelineTraces extends Project {
@override
final String pubspec = '''