Skip to content

Commit 037ca7f

Browse files
author
Anna Gringauze
authored
Fix hot restart and breakpoints races (#1384)
* Fix hot restart and breakpoints races Multiple breakpoints restored ad the isolate start can cause the app to crash in chrome, serialize them using a mutex to prevent the crash. Hot restart runs main immediately, followed by createIsolate that restores breakpoints, sometimes too late, causing non- removable breakpoints and failures in the UI. Make the dwds run main after createIsolate is done, to make sure breakpoints are all set before the run. Hot restart tries to resume the exiting isolate, and checks if it resumed before that, which causes various races or chrome requiring to be in focus, depending on the way the check is done. Remove the check and just catch exception in case the isolate is already running. * Use local dwds in webdev, update changelog * Rebase, update version and build * Increase timeout for failing tests * Update frontend_sever_client_test.dart revert timeout on frontend_server_client tests, it is fixed in another PR. * Remove no running main on hot restart * Addressed CR comments
1 parent 8b7cb48 commit 037ca7f

File tree

10 files changed

+84
-23
lines changed

10 files changed

+84
-23
lines changed

dwds/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 11.2.3-dev
2+
3+
- Fix race causing intermittent `Aww, snap` errors on starting debugger
4+
with multiple breakpoints in source.
5+
- Fix needing chrome to be focus in order to wait for the isolate to
6+
exit on hot restart.
7+
18
## 11.2.2
29

310
- Depend on `dds` version `2.1.1`.

dwds/lib/src/debugging/debugger.dart

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'dart:math' as math;
99

1010
import 'package:logging/logging.dart';
1111
import 'package:meta/meta.dart';
12+
import 'package:pool/pool.dart';
1213
import 'package:vm_service/vm_service.dart';
1314
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
1415
hide StackTrace;
@@ -187,6 +188,7 @@ class Debugger extends Domain {
187188
runZonedGuarded(() {
188189
_remoteDebugger?.onPaused?.listen(_pauseHandler);
189190
_remoteDebugger?.onResumed?.listen(_resumeHandler);
191+
_remoteDebugger?.onTargetCrashed?.listen(_crashHandler);
190192
}, (e, StackTrace s) {
191193
logger.warning('Error handling Chrome event', e, s);
192194
});
@@ -580,6 +582,23 @@ class Debugger extends Domain {
580582
_streamNotify('Debug', event);
581583
}
582584

585+
/// Handles targetCrashed events coming from the Chrome connection.
586+
Future<void> _crashHandler(TargetCrashedEvent _) async {
587+
// We can receive a resume event in the middle of a reload which will result
588+
// in a null isolate.
589+
var isolate = inspector?.isolate;
590+
if (isolate == null) return;
591+
592+
stackComputer = null;
593+
var event = Event(
594+
kind: EventKind.kIsolateExit,
595+
timestamp: DateTime.now().millisecondsSinceEpoch,
596+
isolate: inspector.isolateRef);
597+
isolate.pauseEvent = event;
598+
_streamNotify('Isolate', event);
599+
logger.severe('Target crashed!');
600+
}
601+
583602
/// Evaluate [expression] by calling Chrome's Runtime.evaluate
584603
Future<RemoteObject> evaluate(String expression) async {
585604
try {
@@ -655,6 +674,8 @@ class _Breakpoints extends Domain {
655674

656675
final _bpByDartId = <String, Future<Breakpoint>>{};
657676

677+
final _pool = Pool(1);
678+
658679
final Locations locations;
659680
final RemoteDebugger remoteDebugger;
660681

@@ -722,12 +743,16 @@ class _Breakpoints extends Domain {
722743

723744
// The module can be loaded from a nested path and contain an ETAG suffix.
724745
var urlRegex = '.*${location.jsLocation.module}.*';
725-
var response = await remoteDebugger
726-
.sendCommand('Debugger.setBreakpointByUrl', params: {
727-
'urlRegex': urlRegex,
728-
'lineNumber': location.jsLocation.line - 1,
746+
// Prevent `Aww, snap!` errors when setting multiple breakpoints
747+
// simultaneously by serializing the requests.
748+
return _pool.withResource(() async {
749+
var response = await remoteDebugger
750+
.sendCommand('Debugger.setBreakpointByUrl', params: {
751+
'urlRegex': urlRegex,
752+
'lineNumber': location.jsLocation.line - 1,
753+
});
754+
return response.result['breakpointId'] as String;
729755
});
730-
return response.result['breakpointId'] as String;
731756
}
732757

733758
/// Records the internal Dart <=> JS breakpoint id mapping and adds the

dwds/lib/src/debugging/remote_debugger.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
88

9+
class TargetCrashedEvent extends WipEvent {
10+
TargetCrashedEvent(Map<String, dynamic> json) : super(json);
11+
}
12+
913
/// A generic debugger used in remote debugging.
1014
abstract class RemoteDebugger {
1115
Stream<ConsoleAPIEvent> get onConsoleAPICalled;
@@ -20,6 +24,8 @@ abstract class RemoteDebugger {
2024

2125
Stream<ScriptParsedEvent> get onScriptParsed;
2226

27+
Stream<TargetCrashedEvent> get onTargetCrashed;
28+
2329
Map<String, WipScript> get scripts;
2430

2531
Stream<void> get onClose;

dwds/lib/src/debugging/webkit_debugger.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ class WebkitDebugger implements RemoteDebugger {
112112
@override
113113
Stream<ScriptParsedEvent> get onScriptParsed => _wipDebugger.onScriptParsed;
114114

115+
@override
116+
Stream<TargetCrashedEvent> get onTargetCrashed => _wipDebugger.eventStream(
117+
'Inspector.targetCrashed',
118+
(WipEvent event) => TargetCrashedEvent(event.json));
119+
115120
@override
116121
Map<String, WipScript> get scripts => _wipDebugger.scripts;
117122

dwds/lib/src/dwds_vm_client.dart

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -194,22 +194,28 @@ Future<void> _disableBreakpointsAndResume(
194194
if (vm.isolates.isEmpty) throw StateError('No active isolate to resume.');
195195
var isolateRef = vm.isolates.first;
196196

197-
// Pause the app to prevent it from hitting a breakpoint
198-
// during hot restart and stalling hot restart execution.
199-
// Then wait for the app to pause or to hit a breakpoint.
200-
var debug = chromeProxyService.onEvent('Debug').firstWhere((event) =>
201-
event.kind == EventKind.kPauseInterrupted ||
202-
event.kind == EventKind.kPauseBreakpoint);
203-
204-
await client.pause(isolateRef.id);
205-
206-
var isolate = await client.getIsolate(isolateRef.id);
207-
if (isolate.pauseEvent.kind != EventKind.kPauseInterrupted &&
208-
isolate.pauseEvent.kind != EventKind.kPauseBreakpoint) {
209-
await debug;
210-
}
211-
212197
await chromeProxyService.disableBreakpoints();
213-
await client.resume(isolateRef.id);
198+
try {
199+
// Any checks for paused status result in race conditions or hangs
200+
// at this point:
201+
//
202+
// - `getIsolate()` and check for status:
203+
// the app migth still pause on existing breakpoint.
204+
//
205+
// - `pause()` and wait for `Debug.paused` event:
206+
// chrome does not send the `Debug.Paused `notification
207+
// without shifting focus to chrome.
208+
//
209+
// Instead, just try resuming and
210+
// ignore failures indicating that the app is already running:
211+
//
212+
// WipError -32000 Can only perform operation while paused.
213+
await client.resume(isolateRef.id);
214+
} on RPCError catch (e, s) {
215+
if (!e.message.contains('Can only perform operation while paused')) {
216+
_logger.severe('Hot restart failed to resume exiting isolate', e, s);
217+
rethrow;
218+
}
219+
}
214220
_logger.info('Successfully disabled breakpoints and resumed the isolate');
215221
}

dwds/lib/src/handlers/dev_handler.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ class DevHandler {
142142
tabConnection.onReceive.listen((message) {
143143
_log(' wip', '<== $message');
144144
});
145+
tabConnection.onNotification.listen((message) {
146+
_log(' wip', '<== $message');
147+
});
145148
}
146149
var contextIds = tabConnection.runtime.onExecutionContextCreated
147150
.map((context) => context.id)

dwds/lib/src/servers/extension_debugger.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,11 @@ class ExtensionDebugger implements RemoteDebugger {
279279
'Debugger.scriptParsed',
280280
(WipEvent event) => ScriptParsedEvent(event.json));
281281

282+
@override
283+
Stream<TargetCrashedEvent> get onTargetCrashed => eventStream(
284+
'Inspector.targetCrashed',
285+
(WipEvent event) => TargetCrashedEvent(event.json));
286+
282287
@override
283288
Map<String, WipScript> get scripts => UnmodifiableMapView(_scripts);
284289

dwds/lib/src/version.dart

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dwds/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: dwds
22
# Every time this changes you need to run `pub run build_runner build`.
3-
version: 11.2.2
3+
version: 11.2.3-dev
44
homepage: https://github.com/dart-lang/webdev/tree/master/dwds
55
description: >-
66
A service that proxies between the Chrome debug protocol and the Dart VM

dwds/test/fixtures/fakes.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:dwds/src/debugging/execution_context.dart';
1111
import 'package:dwds/src/debugging/inspector.dart';
1212
import 'package:dwds/src/debugging/instance.dart';
1313
import 'package:dwds/src/debugging/modules.dart';
14+
import 'package:dwds/src/debugging/remote_debugger.dart';
1415
import 'package:dwds/src/debugging/webkit_debugger.dart';
1516
import 'package:dwds/src/loaders/strategy.dart';
1617
import 'package:dwds/src/utilities/domain.dart';
@@ -174,6 +175,9 @@ class FakeWebkitDebugger implements WebkitDebugger {
174175
@override
175176
Stream<ScriptParsedEvent> get onScriptParsed => null;
176177

178+
@override
179+
Stream<TargetCrashedEvent> get onTargetCrashed => null;
180+
177181
@override
178182
Future<WipResponse> pause() => null;
179183

0 commit comments

Comments
 (0)