Attaches a debug connection to the tab when clicked (#1740)
diff --git a/dwds/debug_extension_mv3/build.yaml b/dwds/debug_extension_mv3/build.yaml
index cdffb3a..b5aaf6d 100644
--- a/dwds/debug_extension_mv3/build.yaml
+++ b/dwds/debug_extension_mv3/build.yaml
@@ -20,6 +20,7 @@
{
"web/{{}}.dart.js": ["compiled/{{}}.dart.js"],
"web/{{}}.png": ["compiled/{{}}.png"],
+ "web/{{}}.html": ["compiled/{{}}.html"],
"web/manifest.json": ["compiled/manifest.json"],
}
auto_apply: none
diff --git a/dwds/debug_extension_mv3/pubspec.yaml b/dwds/debug_extension_mv3/pubspec.yaml
index b94ad15..a1167d1 100644
--- a/dwds/debug_extension_mv3/pubspec.yaml
+++ b/dwds/debug_extension_mv3/pubspec.yaml
@@ -6,7 +6,7 @@
A Chrome extension for Dart debugging.
environment:
- sdk: '>=2.12.0 <3.0.0'
+ sdk: '>=2.18.0 <3.0.0'
dependencies:
js: ^0.6.1+1
diff --git a/dwds/debug_extension_mv3/tool/copy_builder.dart b/dwds/debug_extension_mv3/tool/copy_builder.dart
index 07a0c79..5f0760f 100644
--- a/dwds/debug_extension_mv3/tool/copy_builder.dart
+++ b/dwds/debug_extension_mv3/tool/copy_builder.dart
@@ -1,4 +1,4 @@
-// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// 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.
@@ -12,6 +12,7 @@
Map<String, List<String>> get buildExtensions => {
"web/{{}}.dart.js": ["compiled/{{}}.dart.js"],
"web/{{}}.png": ["compiled/{{}}.png"],
+ "web/{{}}.html": ["compiled/{{}}.html"],
"web/manifest.json": ["compiled/manifest.json"],
};
diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart
index b1478cb..43e490f 100644
--- a/dwds/debug_extension_mv3/web/background.dart
+++ b/dwds/debug_extension_mv3/web/background.dart
@@ -5,15 +5,44 @@
@JS()
library background;
+import 'dart:html';
+
import 'package:js/js.dart';
import 'chrome_api.dart';
-import 'web_api.dart';
void main() {
- console.log('Running Dart Debug Extension.');
// Detect clicks on the Dart Debug Extension icon.
- chrome.action.onClicked.addListener(allowInterop((_) {
- console.log('Detected click on the Dart Debug Extension icon.');
+ chrome.action.onClicked.addListener(allowInterop((_) async {
+ await _createDebugTab();
+ await _executeInjectorScript();
}));
}
+
+Future<Tab> _createDebugTab() async {
+ final url = chrome.runtime.getURL('debug_tab.html');
+ final tabPromise = chrome.tabs.create(TabInfo(
+ active: false,
+ pinned: true,
+ url: url,
+ ));
+ return promiseToFuture<Tab>(tabPromise);
+}
+
+Future<void> _executeInjectorScript() async {
+ final tabId = await _getTabId();
+ if (tabId != null) {
+ chrome.scripting.executeScript(
+ InjectDetails(
+ target: Target(tabId: tabId), files: ['iframe_injector.dart.js']),
+ /*callback*/ null,
+ );
+ }
+}
+
+Future<int?> _getTabId() async {
+ final query = QueryInfo(active: true, currentWindow: true);
+ final tabs = List<Tab>.from(await promiseToFuture(chrome.tabs.query(query)));
+ final tab = tabs.isNotEmpty ? tabs.first : null;
+ return tab?.id;
+}
diff --git a/dwds/debug_extension_mv3/web/chrome_api.dart b/dwds/debug_extension_mv3/web/chrome_api.dart
index 9bc20b0..505b2b3 100644
--- a/dwds/debug_extension_mv3/web/chrome_api.dart
+++ b/dwds/debug_extension_mv3/web/chrome_api.dart
@@ -11,6 +11,10 @@
@anonymous
class Chrome {
external Action get action;
+ external Debugger get debugger;
+ external Runtime get runtime;
+ external Scripting get scripting;
+ external Tabs get tabs;
}
@JS()
@@ -28,7 +32,107 @@
@JS()
@anonymous
+class Debugger {
+ // https://developer.chrome.com/docs/extensions/reference/debugger/#method-attach
+ external void attach(
+ Debuggee target, String requiredVersion, Function? callback);
+
+ // https://developer.chrome.com/docs/extensions/reference/debugger/#method-sendCommand
+ external void sendCommand(Debuggee target, String method,
+ Object? commandParams, Function? callback);
+}
+
+@JS()
+@anonymous
+class Runtime {
+ // https://developer.chrome.com/docs/extensions/reference/runtime/#method-sendMessage
+ external void sendMessage(
+ String? id, Object? message, Object? options, Function? callback);
+
+ // https://developer.chrome.com/docs/extensions/reference/runtime/#method-getURL
+ external String getURL(String path);
+
+ // https://developer.chrome.com/docs/extensions/reference/runtime/#event-onMessage
+ external OnMessageHandler get onMessage;
+}
+
+@JS()
+@anonymous
+class OnMessageHandler {
+ external void addListener(
+ void Function(dynamic, MessageSender, Function) callback);
+}
+
+@JS()
+@anonymous
+class Scripting {
+ // https://developer.chrome.com/docs/extensions/reference/scripting/#method-executeScript
+ external executeScript(InjectDetails details, Function? callback);
+}
+
+@JS()
+@anonymous
+class Tabs {
+ // https://developer.chrome.com/docs/extensions/reference/tabs/#method-query
+ external Object query(QueryInfo queryInfo);
+
+ // https://developer.chrome.com/docs/extensions/reference/tabs/#method-create
+ external Object create(TabInfo tabInfo);
+}
+
+@JS()
+@anonymous
+class Debuggee {
+ external int get tabId;
+ external String get extensionId;
+ external String get targetId;
+ external factory Debuggee({int tabId, String? extensionId, String? targetId});
+}
+
+@JS()
+@anonymous
+class MessageSender {
+ external String? get id;
+ external Tab? get tab;
+ external String? get url;
+ external factory MessageSender({String? id, String? url, Tab? tab});
+}
+
+@JS()
+@anonymous
+class TabInfo {
+ external bool? get active;
+ external bool? get pinned;
+ external String? get url;
+ external factory TabInfo({bool? active, bool? pinned, String? url});
+}
+
+@JS()
+@anonymous
+class QueryInfo {
+ external bool get active;
+ external bool get currentWindow;
+ external factory QueryInfo({bool? active, bool? currentWindow});
+}
+
+@JS()
+@anonymous
class Tab {
external int get id;
external String get url;
}
+
+@JS()
+@anonymous
+class InjectDetails {
+ external Target get target;
+ external List<String>? get files;
+ external factory InjectDetails({Target target, List<String> files});
+}
+
+@JS()
+@anonymous
+class Target {
+ external int get tabId;
+ external factory Target({int tabId});
+}
diff --git a/dwds/debug_extension_mv3/web/debug_tab.dart b/dwds/debug_extension_mv3/web/debug_tab.dart
new file mode 100644
index 0000000..338f0e6
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/debug_tab.dart
@@ -0,0 +1,59 @@
+// 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.
+
+@JS()
+library debug_tab;
+
+import 'dart:html';
+
+import 'package:js/js.dart';
+
+import 'chrome_api.dart';
+import 'messaging.dart';
+
+final _channel = BroadcastChannel(debugTabChannelName);
+
+void main() {
+ _registerListeners();
+}
+
+void _registerListeners() {
+ _channel.addEventListener(
+ 'message',
+ allowInterop(_handleChannelMessageEvents),
+ );
+}
+
+void _handleChannelMessageEvents(Event event) {
+ final messageData =
+ jsEventToMessageData(event, expectedOrigin: chrome.runtime.getURL(''));
+ if (messageData == null) return;
+
+ interceptMessage<DebugInfo>(
+ message: messageData,
+ expectedType: MessageType.debugInfo,
+ expectedSender: Script.iframe,
+ expectedRecipient: Script.debugTab,
+ messageHandler: _debugInfoMessageHandler,
+ );
+}
+
+void _debugInfoMessageHandler(DebugInfo message) {
+ final tabId = message.tabId;
+ _startDebugging(tabId);
+}
+
+void _startDebugging(int tabId) {
+ final debuggee = Debuggee(tabId: tabId);
+ chrome.debugger.attach(debuggee, '1.3', allowInterop(() async {
+ chrome.debugger.sendCommand(
+ debuggee, 'Runtime.enable', EmptyParam(), allowInterop((e) {}));
+ }));
+}
+
+@JS()
+@anonymous
+class EmptyParam {
+ external factory EmptyParam();
+}
diff --git a/dwds/debug_extension_mv3/web/debug_tab.html b/dwds/debug_extension_mv3/web/debug_tab.html
new file mode 100644
index 0000000..063f933
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/debug_tab.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+
+<body>
+ <h1>Dart Debug Tab</h1>
+ <h2>Closing will disconnect the debug connection.</h2>
+ <script type="module" src="debug_tab.dart.js"></script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/dwds/debug_extension_mv3/web/iframe.dart b/dwds/debug_extension_mv3/web/iframe.dart
new file mode 100644
index 0000000..9da4144
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/iframe.dart
@@ -0,0 +1,75 @@
+// 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.
+
+@JS()
+library iframe;
+
+import 'dart:html';
+
+import 'package:js/js.dart';
+
+import 'chrome_api.dart';
+import 'messaging.dart';
+
+final _channel = BroadcastChannel(debugTabChannelName);
+
+void main() {
+ _registerListeners();
+
+ // Send a message to the injector script that the IFRAME has loaded.
+ _sendMessageToIframeInjector(
+ type: MessageType.iframeReady,
+ encodedBody: IframeReady(isReady: true).toJSON(),
+ );
+}
+
+void _registerListeners() {
+ chrome.runtime.onMessage.addListener(allowInterop(_handleRuntimeMessages));
+}
+
+void _sendMessageToIframeInjector({
+ required MessageType type,
+ required String encodedBody,
+}) {
+ final message = Message(
+ to: Script.iframeInjector,
+ from: Script.iframe,
+ type: type,
+ encodedBody: encodedBody,
+ );
+ window.parent?.postMessage(message.toJSON(), '*');
+}
+
+void _handleRuntimeMessages(
+ dynamic jsRequest, MessageSender sender, Function sendResponse) {
+ if (jsRequest is! String) return;
+
+ interceptMessage<DebugState>(
+ message: jsRequest,
+ expectedType: MessageType.debugState,
+ expectedSender: Script.iframeInjector,
+ expectedRecipient: Script.iframe,
+ messageHandler: (DebugState message) {
+ final tabId = sender.tab?.id;
+ if (tabId == null) return;
+
+ _sendMessageToDebugTab(
+ type: MessageType.debugInfo,
+ encodedBody: DebugInfo(tabId: tabId).toJSON(),
+ );
+ });
+}
+
+void _sendMessageToDebugTab({
+ required MessageType type,
+ required String encodedBody,
+}) {
+ final message = Message(
+ to: Script.debugTab,
+ from: Script.iframe,
+ type: type,
+ encodedBody: encodedBody,
+ );
+ _channel.postMessage(message.toJSON());
+}
diff --git a/dwds/debug_extension_mv3/web/iframe.html b/dwds/debug_extension_mv3/web/iframe.html
new file mode 100644
index 0000000..1b4ba17
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/iframe.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+
+<body>
+ <h1>Dart Debug IFRAME</h1>
+ <script type="module" src="iframe.dart.js"></script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/dwds/debug_extension_mv3/web/iframe_injector.dart b/dwds/debug_extension_mv3/web/iframe_injector.dart
new file mode 100644
index 0000000..a83f145
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/iframe_injector.dart
@@ -0,0 +1,72 @@
+// 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 'dart:html';
+import 'dart:js';
+
+import 'chrome_api.dart';
+import 'messaging.dart';
+
+void main() {
+ _registerListeners();
+
+ // Inject the IFRAME into the current tab.
+ _injectIframe();
+}
+
+void _registerListeners() {
+ window.addEventListener(
+ 'message',
+ allowInterop(_handleWindowMessageEvents),
+ );
+}
+
+void _injectIframe() {
+ final iframe = document.createElement('iframe');
+ final iframeSrc = chrome.runtime.getURL('iframe.html');
+ iframe.setAttribute('src', iframeSrc);
+ document.body?.append(iframe);
+}
+
+void _handleWindowMessageEvents(Event event) {
+ final messageData =
+ jsEventToMessageData(event, expectedOrigin: chrome.runtime.getURL(''));
+ if (messageData == null) return;
+
+ interceptMessage<IframeReady>(
+ message: messageData,
+ expectedType: MessageType.iframeReady,
+ expectedSender: Script.iframe,
+ expectedRecipient: Script.iframeInjector,
+ messageHandler: _iframeReadyMessageHandler,
+ );
+}
+
+void _iframeReadyMessageHandler(IframeReady message) {
+ if (message.isReady != true) return;
+ // TODO(elliette): Inject a script to fetch debug info global variables.
+
+ // Send a message back to IFRAME so that it has access to the tab ID.
+ _sendMessageToIframe(
+ type: MessageType.debugState,
+ encodedBody: DebugState(shouldDebug: true).toJSON());
+}
+
+void _sendMessageToIframe({
+ required MessageType type,
+ required String encodedBody,
+}) {
+ final message = Message(
+ to: Script.iframe,
+ from: Script.iframeInjector,
+ type: type,
+ encodedBody: encodedBody,
+ );
+ chrome.runtime.sendMessage(
+ /*id*/ null,
+ message.toJSON(),
+ /*options*/ null,
+ /*callback*/ null,
+ );
+}
diff --git a/dwds/debug_extension_mv3/web/manifest.json b/dwds/debug_extension_mv3/web/manifest.json
index 4434a26..06fc0ca 100644
--- a/dwds/debug_extension_mv3/web/manifest.json
+++ b/dwds/debug_extension_mv3/web/manifest.json
@@ -5,7 +5,26 @@
"action": {
"default_icon": "dart_dev.png"
},
+ "permissions": [
+ "scripting",
+ "tabs",
+ "debugger"
+ ],
+ "host_permissions": [
+ "<all_urls>"
+ ],
+ "web_accessible_resources": [
+ {
+ "matches": [
+ "<all_urls>"
+ ],
+ "resources": [
+ "iframe.html",
+ "iframe_injector.dart.js"
+ ]
+ }
+ ],
"background": {
"service_worker": "background.dart.js"
}
-}
+}
\ No newline at end of file
diff --git a/dwds/debug_extension_mv3/web/messaging.dart b/dwds/debug_extension_mv3/web/messaging.dart
new file mode 100644
index 0000000..1593a3b
--- /dev/null
+++ b/dwds/debug_extension_mv3/web/messaging.dart
@@ -0,0 +1,189 @@
+// 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.
+
+@JS()
+library messaging;
+
+import 'dart:convert';
+import 'dart:html';
+
+import 'package:js/js.dart';
+
+import 'web_api.dart';
+
+const debugTabChannelName = 'DEBUG_TAB_CHANNEL';
+
+enum Script {
+ background,
+ debugTab,
+ iframe,
+ iframeInjector;
+
+ factory Script.fromString(String value) {
+ return Script.values.byName(value);
+ }
+}
+
+enum MessageType {
+ debugInfo,
+ debugState,
+ iframeReady;
+
+ factory MessageType.fromString(String value) {
+ return MessageType.values.byName(value);
+ }
+}
+
+class Message {
+ final Script to;
+ final Script from;
+ final MessageType type;
+ final String encodedBody;
+ final String? error;
+
+ Message({
+ required this.to,
+ required this.from,
+ required this.type,
+ required this.encodedBody,
+ this.error,
+ });
+
+ factory Message.fromJSON(String json) {
+ final decoded = jsonDecode(json) as Map<String, dynamic>;
+
+ return Message(
+ to: Script.fromString(decoded['to'] as String),
+ from: Script.fromString(decoded['from'] as String),
+ type: MessageType.fromString(decoded['type'] as String),
+ encodedBody: decoded['encodedBody'] as String,
+ error: decoded['error'] as String?,
+ );
+ }
+
+ String toJSON() {
+ return jsonEncode({
+ 'type': type.name,
+ 'to': to.name,
+ 'from': from.name,
+ 'encodedBody': encodedBody,
+ if (error != null) 'error': error,
+ });
+ }
+}
+
+void interceptMessage<T>({
+ required String? message,
+ required MessageType expectedType,
+ required Script expectedSender,
+ required Script expectedRecipient,
+ required void Function(T message) messageHandler,
+}) {
+ try {
+ if (message == null) return;
+ final decodedMessage = Message.fromJSON(message);
+ if (decodedMessage.type != expectedType ||
+ decodedMessage.to != expectedRecipient ||
+ decodedMessage.from != expectedSender) {
+ return;
+ }
+ final messageType = decodedMessage.type;
+ final messageBody = decodedMessage.encodedBody;
+ switch (messageType) {
+ case MessageType.debugInfo:
+ messageHandler(DebugInfo.fromJSON(messageBody) as T);
+ break;
+ case MessageType.debugState:
+ messageHandler(DebugState.fromJSON(messageBody) as T);
+ break;
+ case MessageType.iframeReady:
+ messageHandler(IframeReady.fromJSON(messageBody) as T);
+ break;
+ }
+ } catch (error) {
+ console.warn('Error intercepting expected message: $error');
+ return;
+ }
+}
+
+String? jsEventToMessageData(
+ Event event, {
+ required String expectedOrigin,
+}) {
+ try {
+ final messageEvent = event as MessageEvent;
+ final origin = messageEvent.origin;
+ if (origin.removeTrailingSlash() != expectedOrigin.removeTrailingSlash()) {
+ return null;
+ }
+ return messageEvent.data as String;
+ } catch (error) {
+ console.warn('Error converting event to message data: $error');
+ return null;
+ }
+}
+
+class IframeReady {
+ final bool isReady;
+
+ IframeReady({required this.isReady});
+
+ factory IframeReady.fromJSON(String json) {
+ final decoded = jsonDecode(json) as Map<String, dynamic>;
+ final isReady = decoded['isReady'] as bool;
+ return IframeReady(isReady: isReady);
+ }
+
+ String toJSON() {
+ return jsonEncode({
+ 'isReady': isReady,
+ });
+ }
+}
+
+class DebugState {
+ final bool shouldDebug;
+
+ DebugState({required this.shouldDebug});
+
+ factory DebugState.fromJSON(String json) {
+ final decoded = jsonDecode(json) as Map<String, dynamic>;
+ final shouldDebug = decoded['shouldDebug'] as bool;
+ return DebugState(shouldDebug: shouldDebug);
+ }
+
+ String toJSON() {
+ return jsonEncode({
+ 'shouldDebug': shouldDebug,
+ });
+ }
+}
+
+class DebugInfo {
+ final int tabId;
+
+ DebugInfo({required this.tabId});
+
+ factory DebugInfo.fromJSON(String json) {
+ final decoded = jsonDecode(json) as Map<String, dynamic>;
+ final tabId = decoded['tabId'] as int;
+ return DebugInfo(tabId: tabId);
+ }
+
+ String toJSON() {
+ return jsonEncode({
+ 'tabId': tabId,
+ });
+ }
+}
+
+extension RemoveTrailingSlash on String {
+ String removeTrailingSlash() {
+ final trailingSlash = '/';
+ if (endsWith(trailingSlash)) {
+ return substring(0, length - 1);
+ }
+ return this;
+ }
+}
diff --git a/dwds/debug_extension_mv3/web/web_api.dart b/dwds/debug_extension_mv3/web/web_api.dart
index 8b927b4..938a0a1 100644
--- a/dwds/debug_extension_mv3/web/web_api.dart
+++ b/dwds/debug_extension_mv3/web/web_api.dart
@@ -12,4 +12,7 @@
class Console {
external void log(String header,
[String style1, String style2, String style3]);
+
+ external void warn(String header,
+ [String style1, String style2, String style3]);
}