Skip to content

Commit 6e3ffd3

Browse files
committed
[ DWDS ] Reestablish WS connections with app and Chrome debugger
When the computer enters sleep mode, Chrome does not attempt to reestablish websocket connections when the computer wakes up. This would break debugging of web applications when DWDS is configured to communicate with the Chrome debugger or the debugged application is using web sockets to communicate with DWDS. This change adds some retry logic to reestablish these web socket connections from the injected client and to the Chrome debugger. Related to flutter/flutter#179886
1 parent c820e2a commit 6e3ffd3

File tree

13 files changed

+1139
-794
lines changed

13 files changed

+1139
-794
lines changed

dwds/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
## 26.2.3-wip
1+
## 26.2.3
22

33
- Bump `build_web_compilers` to ^4.4.1.
44
- Remove unused `clientFuture` arg from `DwdsVmClient` methods.
55
- Fix pausing starting of `main` after the hot restart.
66
- Updating bootstrapper for DDC library bundler module format + Frontend Server.
77
- Fix setting up breakpoints when handling in-app restarts with attached debugger.
8+
- Fix issue where the web socket connections with the target application and Chrome debugger close when the computer sleeps.
89

910
## 26.2.2
1011

dwds/lib/src/debugging/debugger.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ class Debugger {
228228
skipLists,
229229
root,
230230
);
231+
remoteDebugger.onReconnect = debugger._initialize;
231232
await debugger._initialize();
232233
return debugger;
233234
}

dwds/lib/src/debugging/remote_debugger.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ class TargetCrashedEvent extends WipEvent {
1010

1111
/// A generic debugger used in remote debugging.
1212
abstract class RemoteDebugger {
13+
/// An optional callback that's invoked in the case where the debugger
14+
/// connection needs to be reinitialized.
15+
Future<void> Function()? onReconnect;
16+
1317
Stream<ConsoleAPIEvent> get onConsoleAPICalled;
1418

1519
Stream<ExceptionThrownEvent> get onExceptionThrown;

dwds/lib/src/debugging/webkit_debugger.dart

Lines changed: 123 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,126 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:async';
6+
57
import 'package:dwds/src/debugging/remote_debugger.dart';
68
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
79

10+
typedef _EventStreamState = ({
11+
StreamController controller,
12+
WipEventTransformer transformer,
13+
});
14+
815
/// A remote debugger with a Webkit Inspection Protocol connection.
916
class WebkitDebugger implements RemoteDebugger {
10-
final WipDebugger _wipDebugger;
17+
WipDebugger _wipDebugger;
1118

1219
/// Null until [close] is called.
1320
///
1421
/// All subsequent calls to [close] will return this future.
1522
Future<void>? _closed;
1623

17-
WebkitDebugger(this._wipDebugger);
24+
// We need to forward the events from the [WipDebugger] instance rather than
25+
// directly expose its streams as it may need to be recreated due to an
26+
// unexpected loss in connection to the debugger.
27+
final _onClosedController = StreamController<WipConnection>.broadcast();
28+
final _onConsoleAPICalledController =
29+
StreamController<ConsoleAPIEvent>.broadcast();
30+
final _onExceptionThrownController =
31+
StreamController<ExceptionThrownEvent>.broadcast();
32+
final _onGlobalObjectClearedController =
33+
StreamController<GlobalObjectClearedEvent>.broadcast();
34+
final _onPausedController = StreamController<DebuggerPausedEvent>.broadcast();
35+
final _onResumedController =
36+
StreamController<DebuggerResumedEvent>.broadcast();
37+
final _onScriptParsedController =
38+
StreamController<ScriptParsedEvent>.broadcast();
39+
final _onTargetCrashedController =
40+
StreamController<TargetCrashedEvent>.broadcast();
41+
42+
/// Tracks and manages all subscriptions to streams created with
43+
/// `eventStream`.
44+
final _eventStreams = <String, _EventStreamState>{};
45+
46+
late final _controllers = <StreamController>[
47+
_onClosedController,
48+
_onConsoleAPICalledController,
49+
_onExceptionThrownController,
50+
_onGlobalObjectClearedController,
51+
_onPausedController,
52+
_onResumedController,
53+
_onScriptParsedController,
54+
_onTargetCrashedController,
55+
];
56+
57+
@override
58+
Future<void> Function()? onReconnect;
59+
60+
WebkitDebugger(this._wipDebugger) {
61+
_initialize();
62+
}
63+
64+
void _initialize() {
65+
late StreamSubscription sub;
66+
sub = _wipDebugger.connection.onClose.listen((connection) async {
67+
await sub.cancel();
68+
if (_closed != null) {
69+
// The connection closing is expected.
70+
_onClosedController.add(connection);
71+
await Future.wait([
72+
for (final controller in _controllers) controller.close(),
73+
for (final MapEntry(value: (:controller, transformer: _))
74+
in _eventStreams.entries)
75+
controller.close(),
76+
]);
77+
return;
78+
}
79+
var retry = false;
80+
var retryCount = 0;
81+
const maxAttempts = 5;
82+
do {
83+
retry = false;
84+
try {
85+
_wipDebugger = WipDebugger(
86+
await WipConnection.connect(connection.url),
87+
);
88+
await onReconnect?.call();
89+
} on Exception {
90+
await Future.delayed(Duration(milliseconds: 25 << retryCount));
91+
retry = true;
92+
retryCount++;
93+
}
94+
} while (retry && retryCount <= maxAttempts);
95+
_initialize();
96+
});
97+
98+
_wipDebugger.connection.runtime
99+
..onConsoleAPICalled.listen(_onConsoleAPICalledController.add)
100+
..onExceptionThrown.listen(_onExceptionThrownController.add);
101+
_wipDebugger
102+
..onGlobalObjectCleared.listen(_onGlobalObjectClearedController.add)
103+
..onPaused.listen(_onPausedController.add)
104+
..onResumed.listen(_onResumedController.add)
105+
..onScriptParsed.listen(_onScriptParsedController.add)
106+
..eventStream(
107+
'Inspector.targetCrashed',
108+
(WipEvent event) => TargetCrashedEvent(event.json),
109+
).listen(_onTargetCrashedController.add);
110+
111+
for (final MapEntry(:key, value: (:controller, :transformer))
112+
in _eventStreams.entries) {
113+
final stream = _wipDebugger.eventStream(key, transformer);
114+
stream.listen(controller.add, onError: controller.addError);
115+
}
116+
}
18117

19118
@override
20119
Stream<ConsoleAPIEvent> get onConsoleAPICalled =>
21-
_wipDebugger.connection.runtime.onConsoleAPICalled;
120+
_onConsoleAPICalledController.stream;
22121

23122
@override
24123
Stream<ExceptionThrownEvent> get onExceptionThrown =>
25-
_wipDebugger.connection.runtime.onExceptionThrown;
124+
_onExceptionThrownController.stream;
26125

27126
@override
28127
Future<WipResponse> sendCommand(
@@ -103,31 +202,40 @@ class WebkitDebugger implements RemoteDebugger {
103202
}
104203

105204
@override
106-
Stream<T> eventStream<T>(String method, WipEventTransformer<T> transformer) =>
107-
_wipDebugger.eventStream(method, transformer);
205+
Stream<T> eventStream<T>(String method, WipEventTransformer<T> transformer) {
206+
return _eventStreams
207+
.putIfAbsent(method, () {
208+
final controller = StreamController<T>();
209+
final stream = _wipDebugger.eventStream(method, transformer);
210+
stream.listen(controller.add, onError: controller.addError);
211+
return (controller: controller, transformer: transformer);
212+
})
213+
.controller
214+
.stream
215+
.cast<T>();
216+
}
108217

109218
@override
110219
Stream<GlobalObjectClearedEvent> get onGlobalObjectCleared =>
111-
_wipDebugger.onGlobalObjectCleared;
220+
_onGlobalObjectClearedController.stream;
112221

113222
@override
114-
Stream<DebuggerPausedEvent> get onPaused => _wipDebugger.onPaused;
223+
Stream<DebuggerPausedEvent> get onPaused => _onPausedController.stream;
115224

116225
@override
117-
Stream<DebuggerResumedEvent> get onResumed => _wipDebugger.onResumed;
226+
Stream<DebuggerResumedEvent> get onResumed => _onResumedController.stream;
118227

119228
@override
120-
Stream<ScriptParsedEvent> get onScriptParsed => _wipDebugger.onScriptParsed;
229+
Stream<ScriptParsedEvent> get onScriptParsed =>
230+
_onScriptParsedController.stream;
121231

122232
@override
123-
Stream<TargetCrashedEvent> get onTargetCrashed => _wipDebugger.eventStream(
124-
'Inspector.targetCrashed',
125-
(WipEvent event) => TargetCrashedEvent(event.json),
126-
);
233+
Stream<TargetCrashedEvent> get onTargetCrashed =>
234+
_onTargetCrashedController.stream;
127235

128236
@override
129237
Map<String, WipScript> get scripts => _wipDebugger.scripts;
130238

131239
@override
132-
Stream<WipConnection> get onClose => _wipDebugger.connection.onClose;
240+
Stream<WipConnection> get onClose => _onClosedController.stream;
133241
}

dwds/lib/src/handlers/dev_handler.dart

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,13 @@ class DevHandler {
171171
) async {
172172
ChromeTab? appTab;
173173
ExecutionContext? executionContext;
174-
WipConnection? tabConnection;
174+
WipConnection? connection;
175+
late final debugger = WebkitDebugger(WipDebugger(connection!));
175176
final appInstanceId = appConnection.request.instanceId;
176177
for (final tab in await chromeConnection.getTabs()) {
177178
if (tab.isChromeExtension || tab.isBackgroundPage) continue;
178179

179-
final connection = tabConnection = await tab.connect();
180+
connection = await tab.connect();
180181
if (_enableLogging) {
181182
connection.onSend.listen((message) {
182183
_log(' wip', '==> $message');
@@ -208,30 +209,28 @@ class DevHandler {
208209
appTab = tab;
209210
executionContext = RemoteDebuggerExecutionContext(
210211
contextId,
211-
WebkitDebugger(WipDebugger(connection)),
212+
debugger,
212213
);
213214
break;
214215
}
215216
}
216217
if (appTab != null) break;
217218
safeUnawaited(connection.close());
218219
}
219-
if (appTab == null || tabConnection == null || executionContext == null) {
220+
if (appTab == null || connection == null || executionContext == null) {
220221
throw AppConnectionException(
221222
'Could not connect to application with appInstanceId: '
222223
'$appInstanceId',
223224
);
224225
}
225226

226-
final webkitDebugger = WebkitDebugger(WipDebugger(tabConnection));
227-
228227
return ChromeDebugService.start(
229228
// We assume the user will connect to the debug service on the same
230229
// machine. This allows consumers of DWDS to provide a `hostname` for
231230
// debugging through the Dart Debug Extension without impacting the local
232231
// debug workflow.
233232
hostname: 'localhost',
234-
remoteDebugger: webkitDebugger,
233+
remoteDebugger: debugger,
235234
executionContext: executionContext,
236235
assetReader: _assetReader,
237236
appConnection: appConnection,

0 commit comments

Comments
 (0)