Skip to content

Commit 9cc10d4

Browse files
authored
Connect to a chrome.runtime port to keep the service worker alive (#1789)
1 parent 3ec168f commit 9cc10d4

File tree

9 files changed

+386
-83
lines changed

9 files changed

+386
-83
lines changed

dwds/debug_extension_mv3/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ dependencies:
1414

1515
dev_dependencies:
1616
build: ^2.0.0
17-
build_web_compilers: ^3.0.0
1817
build_runner: ^2.0.6
1918
built_value_generator: ^8.3.0
19+
build_web_compilers: ^3.0.0
2020
dwds: ^16.0.0
2121

2222
dependency_overrides:

dwds/debug_extension_mv3/web/background.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
@JS()
66
library background;
77

8+
import 'dart:async';
89
import 'dart:html';
910

1011
import 'package:dwds/data/debug_info.dart';
1112
import 'package:js/js.dart';
1213

1314
import 'chrome_api.dart';
1415
import 'data_types.dart';
16+
import 'lifeline_ports.dart';
1517
import 'messaging.dart';
1618
import 'storage.dart';
1719
import 'web_api.dart';
@@ -22,13 +24,16 @@ void main() {
2224

2325
void _registerListeners() {
2426
chrome.runtime.onMessage.addListener(allowInterop(_handleRuntimeMessages));
27+
chrome.tabs.onRemoved
28+
.addListener(allowInterop((tabId, _) => maybeRemoveLifelinePort(tabId)));
2529

2630
// Detect clicks on the Dart Debug Extension icon.
2731
chrome.action.onClicked.addListener(allowInterop(_startDebugSession));
2832
}
2933

30-
Future<void> _startDebugSession(Tab _) async {
31-
// TODO(elliette): Start a debug session instead.
34+
// TODO(elliette): Start a debug session instead.
35+
Future<void> _startDebugSession(Tab currentTab) async {
36+
maybeCreateLifelinePort(currentTab.id);
3237
final devToolsOpener = await fetchStorageObject<DevToolsOpener>(
3338
type: StorageObject.devToolsOpener);
3439
await _createTab('https://dart.dev/',

dwds/debug_extension_mv3/web/chrome_api.dart

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ external Chrome get chrome;
1212
class Chrome {
1313
external Action get action;
1414
external Runtime get runtime;
15+
external Scripting get scripting;
1516
external Storage get storage;
1617
external Tabs get tabs;
1718
external Windows get windows;
@@ -47,12 +48,37 @@ class IconInfo {
4748
@JS()
4849
@anonymous
4950
class Runtime {
51+
external void connect(String? extensionId, ConnectInfo info);
52+
5053
external void sendMessage(
5154
String? id, Object? message, Object? options, Function? callback);
5255

56+
external ConnectionHandler get onConnect;
57+
5358
external OnMessageHandler get onMessage;
5459
}
5560

61+
@JS()
62+
@anonymous
63+
class ConnectInfo {
64+
external String? get name;
65+
external factory ConnectInfo({String? name});
66+
}
67+
68+
@JS()
69+
@anonymous
70+
class Port {
71+
external String? get name;
72+
external void disconnect();
73+
external ConnectionHandler get onDisconnect;
74+
}
75+
76+
@JS()
77+
@anonymous
78+
class ConnectionHandler {
79+
external void addListener(void Function(Port) callback);
80+
}
81+
5682
@JS()
5783
@anonymous
5884
class OnMessageHandler {
@@ -69,13 +95,46 @@ class MessageSender {
6995
external factory MessageSender({String? id, String? url, Tab? tab});
7096
}
7197

98+
/// chrome.scripting APIs
99+
/// https://developer.chrome.com/docs/extensions/reference/scripting
100+
101+
@JS()
102+
@anonymous
103+
class Scripting {
104+
external executeScript(InjectDetails details, Function? callback);
105+
}
106+
107+
@JS()
108+
@anonymous
109+
class InjectDetails<T, U> {
110+
external Target get target;
111+
external T? get func;
112+
external List<U?>? get args;
113+
external List<String>? get files;
114+
external factory InjectDetails({
115+
Target target,
116+
T? func,
117+
List<U>? args,
118+
List<String>? files,
119+
});
120+
}
121+
122+
@JS()
123+
@anonymous
124+
class Target {
125+
external int get tabId;
126+
external factory Target({int tabId});
127+
}
128+
72129
/// chrome.storage APIs
73130
/// https://developer.chrome.com/docs/extensions/reference/storage
74131
75132
@JS()
76133
@anonymous
77134
class Storage {
78135
external StorageArea get local;
136+
137+
external StorageArea get session;
79138
}
80139

81140
@JS()
@@ -95,6 +154,14 @@ class Tabs {
95154
external Object query(QueryInfo queryInfo);
96155

97156
external Object create(TabInfo tabInfo);
157+
158+
external OnRemovedHandler get onRemoved;
159+
}
160+
161+
@JS()
162+
@anonymous
163+
class OnRemovedHandler {
164+
external void addListener(void Function(int tabId, dynamic info) callback);
98165
}
99166

100167
@JS()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'chrome_api.dart';
6+
import 'web_api.dart';
7+
8+
void main() async {
9+
_connectToLifelinePort();
10+
}
11+
12+
void _connectToLifelinePort() {
13+
console.log(
14+
'[Dart Debug Extension] Connecting to lifeline port at ${_currentTime()}.');
15+
chrome.runtime.connect(
16+
/*extensionId=*/ null,
17+
ConnectInfo(name: 'keepAlive'),
18+
);
19+
}
20+
21+
String _currentTime() {
22+
final date = DateTime.now();
23+
return '${date.hour}:${date.minute}::${date.second}';
24+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// Keeps the background service worker alive for the duration of a Dart debug
6+
// session by using the workaround described in:
7+
// https://bugs.chromium.org/p/chromium/issues/detail?id=1152255#c21
8+
@JS()
9+
library lifeline_ports;
10+
11+
import 'dart:async';
12+
import 'package:js/js.dart';
13+
14+
import 'chrome_api.dart';
15+
import 'web_api.dart';
16+
17+
// Switch to true to enable debug logs.
18+
// TODO(elliette): Enable / disable with flag while building the extension.
19+
final enableDebugLogging = true;
20+
21+
Port? lifelinePort;
22+
int? lifelineTab;
23+
final dartTabs = <int>{};
24+
25+
void maybeCreateLifelinePort(int tabId) {
26+
// Keep track of current Dart tabs that are being debugged. This way if one of
27+
// them is closed, we can reconnect the lifeline port to another one:
28+
dartTabs.add(tabId);
29+
_debugLog('Dart tabs are: $dartTabs');
30+
// Don't create a lifeline port if we already have one (meaning another Dart
31+
// app is currently being debugged):
32+
if (lifelinePort != null) {
33+
_debugWarn('Port already exists.');
34+
return;
35+
}
36+
// Start the keep-alive logic when the port connects:
37+
chrome.runtime.onConnect.addListener(allowInterop(_keepLifelinePortAlive));
38+
// Inject the connection script into the current Dart tab, that way the tab
39+
// will connect to the port:
40+
_debugLog('Creating lifeline port.');
41+
lifelineTab = tabId;
42+
chrome.scripting.executeScript(
43+
InjectDetails(
44+
target: Target(tabId: tabId),
45+
files: ['lifeline_connection.dart.js'],
46+
),
47+
/*callback*/ null,
48+
);
49+
}
50+
51+
void maybeRemoveLifelinePort(int removedTabId) {
52+
final removedDartTab = dartTabs.remove(removedTabId);
53+
// If the removed tab was not a Dart tab, return early.
54+
if (!removedDartTab) return;
55+
_debugLog('Removed tab $removedTabId, Dart tabs are now $dartTabs.');
56+
// If the removed Dart tab hosted the lifeline port connection, see if there
57+
// are any other Dart tabs to connect to. Otherwise disconnect the port.
58+
if (lifelineTab == removedTabId) {
59+
if (dartTabs.isEmpty) {
60+
lifelineTab = null;
61+
_debugLog('No more Dart tabs, disconnecting from lifeline port.');
62+
_disconnectFromLifelinePort();
63+
} else {
64+
lifelineTab = dartTabs.last;
65+
_debugLog('Reconnecting lifeline port to a new Dart tab: $lifelineTab.');
66+
_reconnectToLifelinePort();
67+
}
68+
}
69+
}
70+
71+
void _keepLifelinePortAlive(Port port) {
72+
final portName = port.name ?? '';
73+
if (portName != 'keepAlive') return;
74+
lifelinePort = port;
75+
// Reconnect to the lifeline port every 5 minutes, as per:
76+
// https://bugs.chromium.org/p/chromium/issues/detail?id=1146434#c6
77+
Timer(Duration(minutes: 5), () {
78+
_debugLog('5 minutes have elapsed, therefore reconnecting.');
79+
_reconnectToLifelinePort();
80+
});
81+
}
82+
83+
void _reconnectToLifelinePort() {
84+
_debugLog('Reconnecting...');
85+
if (lifelinePort == null) {
86+
_debugWarn('Could not find a lifeline port.');
87+
return;
88+
}
89+
if (lifelineTab == null) {
90+
_debugWarn('Could not find a lifeline tab.');
91+
return;
92+
}
93+
// Disconnect from the port, and then recreate the connection with the current
94+
// Dart tab:
95+
_disconnectFromLifelinePort();
96+
maybeCreateLifelinePort(lifelineTab!);
97+
_debugLog('Reconnection complete.');
98+
}
99+
100+
void _disconnectFromLifelinePort() {
101+
_debugLog('Disconnecting...');
102+
if (lifelinePort != null) {
103+
lifelinePort!.disconnect();
104+
lifelinePort = null;
105+
_debugLog('Disconnection complete.');
106+
}
107+
}
108+
109+
void _debugLog(String msg) {
110+
if (enableDebugLogging) {
111+
console.log(msg);
112+
}
113+
}
114+
115+
void _debugWarn(String msg) {
116+
if (enableDebugLogging) {
117+
console.warn(msg);
118+
}
119+
}

dwds/debug_extension_mv3/web/manifest.json

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,6 @@
1414
"host_permissions": [
1515
"<all_urls>"
1616
],
17-
"web_accessible_resources": [
18-
{
19-
"matches": [
20-
"<all_urls>"
21-
],
22-
"resources": [
23-
"iframe.html",
24-
"iframe_injector.dart.js"
25-
]
26-
}
27-
],
2817
"background": {
2918
"service_worker": "background.dart.js"
3019
},

0 commit comments

Comments
 (0)