Skip to content

Connect to a chrome.runtime port to keep the service worker alive #1789

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Nov 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dwds/debug_extension_mv3/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ dependencies:

dev_dependencies:
build: ^2.0.0
build_web_compilers: ^3.0.0
build_runner: ^2.0.6
built_value_generator: ^8.3.0
build_web_compilers: ^3.0.0
dwds: ^16.0.0

dependency_overrides:
Expand Down
9 changes: 7 additions & 2 deletions dwds/debug_extension_mv3/web/background.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
@JS()
library background;

import 'dart:async';
import 'dart:html';

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

import 'chrome_api.dart';
import 'data_types.dart';
import 'lifeline_ports.dart';
import 'messaging.dart';
import 'storage.dart';
import 'web_api.dart';
Expand All @@ -22,13 +24,16 @@ void main() {

void _registerListeners() {
chrome.runtime.onMessage.addListener(allowInterop(_handleRuntimeMessages));
chrome.tabs.onRemoved
.addListener(allowInterop((tabId, _) => maybeRemoveLifelinePort(tabId)));

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

Future<void> _startDebugSession(Tab _) async {
// TODO(elliette): Start a debug session instead.
// TODO(elliette): Start a debug session instead.
Future<void> _startDebugSession(Tab currentTab) async {
maybeCreateLifelinePort(currentTab.id);
final devToolsOpener = await fetchStorageObject<DevToolsOpener>(
type: StorageObject.devToolsOpener);
await _createTab('https://dart.dev/',
Expand Down
67 changes: 67 additions & 0 deletions dwds/debug_extension_mv3/web/chrome_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ external Chrome get chrome;
class Chrome {
external Action get action;
external Runtime get runtime;
external Scripting get scripting;
external Storage get storage;
external Tabs get tabs;
external Windows get windows;
Expand Down Expand Up @@ -47,12 +48,37 @@ class IconInfo {
@JS()
@anonymous
class Runtime {
external void connect(String? extensionId, ConnectInfo info);

external void sendMessage(
String? id, Object? message, Object? options, Function? callback);

external ConnectionHandler get onConnect;

external OnMessageHandler get onMessage;
}

@JS()
@anonymous
class ConnectInfo {
external String? get name;
external factory ConnectInfo({String? name});
}

@JS()
@anonymous
class Port {
external String? get name;
external void disconnect();
external ConnectionHandler get onDisconnect;
}

@JS()
@anonymous
class ConnectionHandler {
external void addListener(void Function(Port) callback);
}

@JS()
@anonymous
class OnMessageHandler {
Expand All @@ -69,13 +95,46 @@ class MessageSender {
external factory MessageSender({String? id, String? url, Tab? tab});
}

/// chrome.scripting APIs
/// https://developer.chrome.com/docs/extensions/reference/scripting

@JS()
@anonymous
class Scripting {
external executeScript(InjectDetails details, Function? callback);
}

@JS()
@anonymous
class InjectDetails<T, U> {
external Target get target;
external T? get func;
external List<U?>? get args;
external List<String>? get files;
external factory InjectDetails({
Target target,
T? func,
List<U>? args,
List<String>? files,
});
}

@JS()
@anonymous
class Target {
external int get tabId;
external factory Target({int tabId});
}

/// chrome.storage APIs
/// https://developer.chrome.com/docs/extensions/reference/storage

@JS()
@anonymous
class Storage {
external StorageArea get local;

external StorageArea get session;
}

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

external Object create(TabInfo tabInfo);

external OnRemovedHandler get onRemoved;
}

@JS()
@anonymous
class OnRemovedHandler {
external void addListener(void Function(int tabId, dynamic info) callback);
}

@JS()
Expand Down
24 changes: 24 additions & 0 deletions dwds/debug_extension_mv3/web/lifeline_connection.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'chrome_api.dart';
import 'web_api.dart';

void main() async {
_connectToLifelinePort();
}

void _connectToLifelinePort() {
console.log(
'[Dart Debug Extension] Connecting to lifeline port at ${_currentTime()}.');
chrome.runtime.connect(
/*extensionId=*/ null,
ConnectInfo(name: 'keepAlive'),
);
}

String _currentTime() {
final date = DateTime.now();
return '${date.hour}:${date.minute}::${date.second}';
}
119 changes: 119 additions & 0 deletions dwds/debug_extension_mv3/web/lifeline_ports.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// Keeps the background service worker alive for the duration of a Dart debug
// session by using the workaround described in:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1152255#c21
@JS()
library lifeline_ports;

import 'dart:async';
import 'package:js/js.dart';

import 'chrome_api.dart';
import 'web_api.dart';

// Switch to true to enable debug logs.
// TODO(elliette): Enable / disable with flag while building the extension.
final enableDebugLogging = true;

Port? lifelinePort;
int? lifelineTab;
final dartTabs = <int>{};

void maybeCreateLifelinePort(int tabId) {
// Keep track of current Dart tabs that are being debugged. This way if one of
// them is closed, we can reconnect the lifeline port to another one:
dartTabs.add(tabId);
_debugLog('Dart tabs are: $dartTabs');
// Don't create a lifeline port if we already have one (meaning another Dart
// app is currently being debugged):
if (lifelinePort != null) {
_debugWarn('Port already exists.');
return;
}
// Start the keep-alive logic when the port connects:
chrome.runtime.onConnect.addListener(allowInterop(_keepLifelinePortAlive));
// Inject the connection script into the current Dart tab, that way the tab
// will connect to the port:
_debugLog('Creating lifeline port.');
lifelineTab = tabId;
chrome.scripting.executeScript(
InjectDetails(
target: Target(tabId: tabId),
files: ['lifeline_connection.dart.js'],
),
/*callback*/ null,
);
}

void maybeRemoveLifelinePort(int removedTabId) {
final removedDartTab = dartTabs.remove(removedTabId);
// If the removed tab was not a Dart tab, return early.
if (!removedDartTab) return;
_debugLog('Removed tab $removedTabId, Dart tabs are now $dartTabs.');
// If the removed Dart tab hosted the lifeline port connection, see if there
// are any other Dart tabs to connect to. Otherwise disconnect the port.
if (lifelineTab == removedTabId) {
if (dartTabs.isEmpty) {
lifelineTab = null;
_debugLog('No more Dart tabs, disconnecting from lifeline port.');
_disconnectFromLifelinePort();
} else {
lifelineTab = dartTabs.last;
_debugLog('Reconnecting lifeline port to a new Dart tab: $lifelineTab.');
_reconnectToLifelinePort();
}
}
}

void _keepLifelinePortAlive(Port port) {
final portName = port.name ?? '';
if (portName != 'keepAlive') return;
lifelinePort = port;
// Reconnect to the lifeline port every 5 minutes, as per:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1146434#c6
Timer(Duration(minutes: 5), () {
_debugLog('5 minutes have elapsed, therefore reconnecting.');
_reconnectToLifelinePort();
});
}

void _reconnectToLifelinePort() {
_debugLog('Reconnecting...');
if (lifelinePort == null) {
_debugWarn('Could not find a lifeline port.');
return;
}
if (lifelineTab == null) {
_debugWarn('Could not find a lifeline tab.');
return;
}
// Disconnect from the port, and then recreate the connection with the current
// Dart tab:
_disconnectFromLifelinePort();
maybeCreateLifelinePort(lifelineTab!);
_debugLog('Reconnection complete.');
}

void _disconnectFromLifelinePort() {
_debugLog('Disconnecting...');
if (lifelinePort != null) {
lifelinePort!.disconnect();
lifelinePort = null;
_debugLog('Disconnection complete.');
}
}

void _debugLog(String msg) {
if (enableDebugLogging) {
console.log(msg);
}
}

void _debugWarn(String msg) {
if (enableDebugLogging) {
console.warn(msg);
}
}
11 changes: 0 additions & 11 deletions dwds/debug_extension_mv3/web/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,6 @@
"host_permissions": [
"<all_urls>"
],
"web_accessible_resources": [
{
"matches": [
"<all_urls>"
],
"resources": [
"iframe.html",
"iframe_injector.dart.js"
]
}
],
"background": {
"service_worker": "background.dart.js"
},
Expand Down
Loading