Skip to content

Commit 55dc847

Browse files
authored
Handle page refreshes and opening in new tabs (#222)
Fixes #202 Note that I added an app instance id - this allows us to unambiguously identify the correct tab for a given instance of an app, separate from the app id which is consistent across all instances. We cache the debug services per app ID and keep them running, but we only allow debugging one instance of a given app at a time. The rough logic here is as follows: - When a new client connects - if we have cached debug services for the app - if the old tab connection is still connected to the correct tab (page refresh case) - reconnect and create the new isolate - else - don't do anything - else - don't do anything - When a client disconnects (from the SSE connection) - destroy the isolate (but leave the proxy services running). - When a client sends a DevTools request - if we have an existing debug service for that app - if it is for the same app _instance_ we are already debugging - open devtools tab pointing at the running debug services - else - alert the user that they can't debug the same app in multiple windows - else - create a new debug service for the app
1 parent 4ffdc4d commit 55dc847

File tree

15 files changed

+8049
-6233
lines changed

15 files changed

+8049
-6233
lines changed

dwds/example/hello_world/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<script defer src="main.dart.js"></script>
55
<script>
66
window.$dartAppId = 'id-for-testing';
7+
window.$dartAppInstanceId = 'instance-id-for-testing';
78
</script>
89
</head>
910

dwds/lib/service.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@ class DebugService {
5757

5858
String get wsUri => 'ws://$hostname:$port';
5959

60-
/// [appId] is a unique String embedded in the application available through
61-
/// `window.$dartAppId`.
60+
/// [appInstanceId] is a unique String embedded in the instance of the
61+
/// application available through `window.$dartAppInstanceId`.
6262
static Future<DebugService> start(
6363
String hostname,
6464
ChromeConnection chromeConnection,
6565
Future<String> Function(String) assetHandler,
66-
String appId) async {
67-
var chromeProxyService =
68-
await ChromeProxyService.create(chromeConnection, assetHandler, appId);
66+
String appInstanceId) async {
67+
var chromeProxyService = await ChromeProxyService.create(
68+
chromeConnection, assetHandler, appInstanceId);
6969
var serviceExtensionRegistry = ServiceExtensionRegistry();
7070
var cascade = Cascade().add(webSocketHandler(_createNewConnectionHandler(
7171
chromeProxyService, serviceExtensionRegistry)));

dwds/lib/src/chrome_proxy_service.dart

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:async';
66
import 'dart:convert';
77
import 'dart:io';
88

9+
import 'package:pedantic/pedantic.dart';
910
import 'package:pub_semver/pub_semver.dart' as semver;
1011
import 'package:vm_service_lib/vm_service_lib.dart';
1112
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
@@ -49,21 +50,25 @@ class ChromeProxyService implements VmServiceInterface {
4950
ChromeProxyService._(
5051
this._vm, this._tab, this.tabConnection, this._assetHandler);
5152

52-
static Future<ChromeProxyService> create(ChromeConnection chromeConnection,
53-
Future<String> Function(String) assetHandler, String appId) async {
53+
static Future<ChromeProxyService> create(
54+
ChromeConnection chromeConnection,
55+
Future<String> Function(String) assetHandler,
56+
String appInstanceId) async {
5457
ChromeTab appTab;
5558
for (var tab in await chromeConnection.getTabs()) {
5659
if (tab.url.startsWith('chrome-extensions:')) continue;
5760
var tabConnection = await tab.connect();
58-
var result = await tabConnection.runtime.sendCommand('Runtime.evaluate',
59-
params: {'expression': r'window.$dartAppId;', 'awaitPromise': true});
60-
if (result.result['result']['value'] == appId) {
61+
var result = await tabConnection.runtime
62+
.evaluate(r'window["$dartAppInstanceId"];');
63+
if (result.value == appInstanceId) {
6164
appTab = tab;
6265
break;
6366
}
67+
unawaited(tabConnection.close());
6468
}
6569
if (appTab == null) {
66-
throw StateError('Could not connect to application with appId: $appId');
70+
throw StateError('Could not connect to application with appInstanceId: '
71+
'$appInstanceId');
6772
}
6873
var tabConnection = await appTab.connect();
6974
await tabConnection.debugger.enable();
@@ -142,6 +147,7 @@ class ChromeProxyService implements VmServiceInterface {
142147
// Listen for `registerExtension` and `postEvent` calls.
143148
_consoleSubscription = tabConnection.runtime.onConsoleAPICalled
144149
.listen((ConsoleAPIEvent event) {
150+
if (_isolate == null) return;
145151
if (event.type != 'debug') return;
146152
var firstArgValue = event.args[0].value as String;
147153
switch (firstArgValue) {
@@ -152,7 +158,8 @@ class ChromeProxyService implements VmServiceInterface {
152158
'Isolate',
153159
Event()
154160
..kind = EventKind.kServiceExtensionAdded
155-
..extensionRPC = service);
161+
..extensionRPC = service
162+
..isolate = isolateRef);
156163
break;
157164
case 'dart.developer.postEvent':
158165
_streamNotify(
@@ -161,7 +168,8 @@ class ChromeProxyService implements VmServiceInterface {
161168
..kind = EventKind.kExtension
162169
..extensionKind = event.args[1].value as String
163170
..extensionData = ExtensionData.parse(
164-
jsonDecode(event.args[2].value as String) as Map));
171+
jsonDecode(event.args[2].value as String) as Map)
172+
..isolate = isolateRef);
165173
break;
166174
case 'dart.developer.inspect':
167175
// All inspected objects should be real objects.
@@ -178,7 +186,8 @@ class ChromeProxyService implements VmServiceInterface {
178186
Event()
179187
..kind = EventKind.kInspect
180188
..inspectee = inspectee
181-
..timestamp = event.timestamp.toInt());
189+
..timestamp = event.timestamp.toInt()
190+
..isolate = isolateRef);
182191
break;
183192
default:
184193
break;
@@ -198,12 +207,25 @@ class ChromeProxyService implements VmServiceInterface {
198207
Event()
199208
..kind = EventKind.kIsolateRunnable
200209
..isolate = isolateRef);
210+
211+
// TODO: We shouldn't need to fire these events since they exist on the
212+
// isolate, but devtools doesn't recognize extensions after a page refresh
213+
// otherwise.
214+
for (var extensionRpc in isolate.extensionRPCs) {
215+
_streamNotify(
216+
'Isolate',
217+
Event()
218+
..kind = EventKind.kServiceExtensionAdded
219+
..extensionRPC = extensionRpc
220+
..isolate = isolateRef);
221+
}
201222
}
202223

203224
/// Should be called when there is a hot restart or full page refresh.
204225
///
205226
/// Clears out [_isolate] and all related cached information.
206227
void destroyIsolate() {
228+
if (_isolate == null) return;
207229
_streamNotify(
208230
'Isolate',
209231
Event()
@@ -354,7 +376,7 @@ require("dart_sdk").developer.invokeExtension(
354376
/// Sync version of [getIsolate] for internal use, also has stronger typing
355377
/// than the public one which has to be dynamic.
356378
Isolate _getIsolate(String isolateId) {
357-
if (_isolate.id == isolateId) return _isolate;
379+
if (_isolate?.id == isolateId) return _isolate;
358380
throw ArgumentError.value(
359381
isolateId, 'isolateId', 'Unrecognized isolate id');
360382
}
@@ -439,7 +461,7 @@ require("dart_sdk").developer.invokeExtension(
439461
}
440462

441463
Future<Library> _getLibrary(String isolateId, String objectId) async {
442-
if (isolateId != _isolate.id) return null;
464+
if (isolateId != _isolate?.id) return null;
443465
var libraryRef = _libraryRefs[objectId];
444466
if (libraryRef == null) return null;
445467
var library = _libraries[objectId];
@@ -670,6 +692,7 @@ require("dart_sdk").developer.invokeExtension(
670692
}, onListen: () {
671693
chromeConsoleSubscription =
672694
tabConnection.runtime.onConsoleAPICalled.listen((e) {
695+
if (_isolate == null) return;
673696
if (!filter(e)) return;
674697
var args = e.params['args'] as List;
675698
var item = args[0] as Map;
@@ -683,6 +706,7 @@ require("dart_sdk").developer.invokeExtension(
683706
if (includeExceptions) {
684707
exceptionsSubscription =
685708
tabConnection.runtime.onExceptionThrown.listen((e) {
709+
if (_isolate == null) return;
686710
controller.add(Event()
687711
..kind = EventKind.kWriteEvent
688712
..isolate = toIsolateRef(_isolate)
@@ -703,6 +727,7 @@ require("dart_sdk").developer.invokeExtension(
703727
StreamSubscription resumeSubscription;
704728
return StreamController<Event>.broadcast(onListen: () {
705729
pauseSubscription = tabConnection.debugger.onPaused.listen((e) {
730+
if (_isolate == null) return;
706731
var event = Event()..isolate = toIsolateRef(_isolate);
707732
var params = e.params;
708733
var breakpoints = params['hitBreakpoints'] as List;
@@ -717,6 +742,7 @@ require("dart_sdk").developer.invokeExtension(
717742
_streamNotify('Debug', event);
718743
});
719744
resumeSubscription = tabConnection.debugger.onResumed.listen((e) {
745+
if (_isolate == null) return;
720746
_streamNotify(
721747
'Debug',
722748
Event()

dwds/test/chrome_proxy_service_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ void main() {
8080
connection,
8181
assetHandler,
8282
// Provided in the example index.html.
83-
'id-for-testing',
83+
'instance-id-for-testing',
8484
);
8585
});
8686

webdev/lib/src/daemon/app_domain.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,17 @@ class AppDomain extends Domain {
2828
void _initialize(ServerManager serverManager) async {
2929
var devHandler = serverManager.servers.first.devHandler;
3030
// The connection is established right before `main()` is called.
31-
_appId = await devHandler.connectedApps.first;
31+
var request = await devHandler.connectedApps.first;
32+
_appId = request.appId;
3233
sendEvent('app.start', {
3334
'appId': _appId,
3435
'directory': Directory.current.path,
3536
'deviceId': 'chrome',
3637
'launchMode': 'run'
3738
});
3839
var chrome = await Chrome.connectedInstance;
39-
_debugService =
40-
await devHandler.startDebugService(chrome.chromeConnection, _appId);
40+
_debugService = await devHandler.startDebugService(
41+
chrome.chromeConnection, request.instanceId);
4142
_webdevVmClient = await WebdevVmClient.create(_debugService);
4243
_vmService = _webdevVmClient.client;
4344
sendEvent('app.started', {

webdev/lib/src/serve/data/connect_request.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,9 @@ abstract class ConnectRequest
1717

1818
ConnectRequest._();
1919

20+
/// Identifies a given application, across tabs/windows.
2021
String get appId;
22+
23+
/// Identifies a given instance of an application, unique per tab/window.
24+
String get instanceId;
2125
}

webdev/lib/src/serve/data/connect_request.g.dart

Lines changed: 27 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webdev/lib/src/serve/data/devtools_request.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,26 @@ abstract class DevToolsRequest
1818

1919
DevToolsRequest._();
2020

21+
/// Identifies a given application, across tabs/windows.
2122
String get appId;
23+
24+
/// Identifies a given instance of an application, unique per tab/window.
25+
String get instanceId;
26+
}
27+
28+
/// A response to a [DevToolsRequest].
29+
abstract class DevToolsResponse
30+
implements Built<DevToolsResponse, DevToolsResponseBuilder> {
31+
static Serializer<DevToolsResponse> get serializer =>
32+
_$devToolsResponseSerializer;
33+
34+
factory DevToolsResponse([updates(DevToolsResponseBuilder b)]) =
35+
_$DevToolsResponse;
36+
37+
DevToolsResponse._();
38+
39+
bool get success;
40+
41+
@nullable
42+
String get error;
2243
}

0 commit comments

Comments
 (0)