diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart index a119d1a6b..10436c63e 100644 --- a/dwds/debug_extension_mv3/web/background.dart +++ b/dwds/debug_extension_mv3/web/background.dart @@ -15,6 +15,7 @@ import 'package:js/js.dart'; import 'data_types.dart'; import 'debug_session.dart'; import 'chrome_api.dart'; +import 'cross_extension_communication.dart'; import 'lifeline_ports.dart'; import 'logger.dart'; import 'messaging.dart'; @@ -29,7 +30,15 @@ void main() { } void _registerListeners() { - chrome.runtime.onMessage.addListener(allowInterop(_handleRuntimeMessages)); + chrome.runtime.onMessage.addListener( + allowInterop(_handleRuntimeMessages), + ); + // The only extension allowed to send messages to this extension is the + // AngularDart DevTools extension. Its permission is set in the manifest.json + // externally_connectable field. + chrome.runtime.onMessageExternal.addListener( + allowInterop(handleMessagesFromAngularDartDevTools), + ); chrome.tabs.onRemoved .addListener(allowInterop((tabId, _) => maybeRemoveLifelinePort(tabId))); // Update the extension icon on tab navigation: diff --git a/dwds/debug_extension_mv3/web/chrome_api.dart b/dwds/debug_extension_mv3/web/chrome_api.dart index 76114fd4a..803e13082 100644 --- a/dwds/debug_extension_mv3/web/chrome_api.dart +++ b/dwds/debug_extension_mv3/web/chrome_api.dart @@ -176,6 +176,8 @@ class Runtime { external ConnectionHandler get onConnect; external OnMessageHandler get onMessage; + + external OnMessageHandler get onMessageExternal; } @JS() diff --git a/dwds/debug_extension_mv3/web/cross_extension_communication.dart b/dwds/debug_extension_mv3/web/cross_extension_communication.dart new file mode 100644 index 000000000..f62143bb4 --- /dev/null +++ b/dwds/debug_extension_mv3/web/cross_extension_communication.dart @@ -0,0 +1,161 @@ +// Copyright (c) 2023, 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 cross_extension_communication; + +import 'package:js/js.dart'; + +import 'chrome_api.dart'; +import 'data_types.dart'; +import 'debug_session.dart'; +import 'logger.dart'; +import 'storage.dart'; +import 'web_api.dart'; + +// The only extension allowed to communicate with this extension is the +// AngularDart DevTools extension. +// +// This ID is used to send messages to AngularDart DevTools, while the +// externally_connectable field in the manifest.json allows AngularDart DevTools +// to send messages to this extension. +const _angularDartDevToolsId = 'nbkbficgbembimioedhceniahniffgpl'; + +// A set of events to forward to the AngularDart DevTools extension. +final _eventsForAngularDartDevTools = { + 'Overlay.inspectNodeRequested', + 'dwds.encodedUri', +}; + +void handleMessagesFromAngularDartDevTools( + dynamic jsRequest, MessageSender sender, Function sendResponse) async { + if (jsRequest == null) return; + final message = jsRequest as ExternalExtensionMessage; + if (message.name == 'chrome.debugger.sendCommand') { + _forwardCommandToChromeDebugger(message, sendResponse); + } else if (message.name == 'dwds.encodedUri') { + _respondWithEncodedUri(message.tabId, sendResponse); + } else if (message.name == 'dwds.startDebugging') { + attachDebugger(message.tabId, trigger: Trigger.angularDartDevTools); + sendResponse(true); + } else { + sendResponse( + ErrorResponse()..error = 'Unknown message name: ${message.name}'); + } +} + +void maybeForwardMessageToAngularDartDevTools( + {required String method, required dynamic params, required int tabId}) { + if (!_eventsForAngularDartDevTools.contains(method)) return; + + final message = method.startsWith('dwds') + ? _dwdsEventMessage(method: method, params: params, tabId: tabId) + : _debugEventMessage(method: method, params: params, tabId: tabId); + + _forwardMessageToAngularDartDevTools(message); +} + +void _forwardCommandToChromeDebugger( + ExternalExtensionMessage message, Function sendResponse) { + try { + final options = message.options as SendCommandOptions; + chrome.debugger.sendCommand( + Debuggee(tabId: message.tabId), + options.method, + options.commandParams, + allowInterop( + ([result]) => _respondWithChromeResult(result, sendResponse)), + ); + } catch (e) { + sendResponse(ErrorResponse()..error = '$e'); + } +} + +void _respondWithChromeResult(Object? chromeResult, Function sendResponse) { + // No result indicates that an error occurred. + if (chromeResult == null) { + sendResponse(ErrorResponse() + ..error = JSON.stringify( + chrome.runtime.lastError ?? 'Unknown error.', + )); + } else { + sendResponse(chromeResult); + } +} + +void _respondWithEncodedUri(int tabId, Function sendResponse) async { + final encodedUri = await fetchStorageObject( + type: StorageObject.encodedUri, tabId: tabId); + sendResponse(encodedUri ?? ''); +} + +void _forwardMessageToAngularDartDevTools(ExternalExtensionMessage message) { + chrome.runtime.sendMessage( + _angularDartDevToolsId, + message, + /* options */ null, + allowInterop(([result]) => _checkForErrors(result, message.name)), + ); +} + +void _checkForErrors(Object? chromeResult, String messageName) { + // No result indicates that an error occurred. + if (chromeResult == null) { + final errorMessage = chrome.runtime.lastError?.message ?? 'Unknown error.'; + debugWarn('Error forwarding $messageName: $errorMessage'); + } +} + +ExternalExtensionMessage _debugEventMessage({ + required String method, + required dynamic params, + required int tabId, +}) => + ExternalExtensionMessage( + name: 'chrome.debugger.event', + tabId: tabId, + options: DebugEvent(method: method, params: params), + ); + +ExternalExtensionMessage _dwdsEventMessage({ + required String method, + required dynamic params, + required int tabId, +}) => + ExternalExtensionMessage( + name: method, + tabId: tabId, + options: params, + ); + +// This message is used for cross-extension communication between this extension +// and the AngularDart DevTools extension. +@JS() +@anonymous +class ExternalExtensionMessage { + external int get tabId; + external String get name; + external dynamic get options; + external factory ExternalExtensionMessage( + {required int tabId, required String name, required dynamic options}); +} + +@JS() +@anonymous +class DebugEvent { + external factory DebugEvent({String method, Object? params}); +} + +@JS() +@anonymous +class SendCommandOptions { + external String get method; + external Object get commandParams; +} + +@JS() +@anonymous +class ErrorResponse { + external set error(String error); +} diff --git a/dwds/debug_extension_mv3/web/data_serializers.dart b/dwds/debug_extension_mv3/web/data_serializers.dart index 9c4e31460..164b02602 100644 --- a/dwds/debug_extension_mv3/web/data_serializers.dart +++ b/dwds/debug_extension_mv3/web/data_serializers.dart @@ -21,6 +21,7 @@ part 'data_serializers.g.dart'; DevToolsOpener, DevToolsUrl, DevToolsRequest, + EncodedUri, ExtensionEvent, ExtensionRequest, ExtensionResponse, diff --git a/dwds/debug_extension_mv3/web/data_serializers.g.dart b/dwds/debug_extension_mv3/web/data_serializers.g.dart index 7c15ed146..724ebcc4b 100644 --- a/dwds/debug_extension_mv3/web/data_serializers.g.dart +++ b/dwds/debug_extension_mv3/web/data_serializers.g.dart @@ -14,6 +14,7 @@ Serializers _$serializers = (new Serializers().toBuilder() ..add(DevToolsOpener.serializer) ..add(DevToolsRequest.serializer) ..add(DevToolsUrl.serializer) + ..add(EncodedUri.serializer) ..add(ExtensionEvent.serializer) ..add(ExtensionRequest.serializer) ..add(ExtensionResponse.serializer) diff --git a/dwds/debug_extension_mv3/web/data_types.dart b/dwds/debug_extension_mv3/web/data_types.dart index eb22eb4e7..5065e3e56 100644 --- a/dwds/debug_extension_mv3/web/data_types.dart +++ b/dwds/debug_extension_mv3/web/data_types.dart @@ -35,6 +35,18 @@ abstract class DevToolsOpener bool get newWindow; } +// TODO(elliette): Standardize on uri or url here and across DWDS, instead of a +// combination of both. +abstract class EncodedUri implements Built { + static Serializer get serializer => _$encodedUriSerializer; + + factory EncodedUri([Function(EncodedUriBuilder) updates]) = _$EncodedUri; + + EncodedUri._(); + + String get uri; +} + abstract class DevToolsUrl implements Built { static Serializer get serializer => _$devToolsUrlSerializer; diff --git a/dwds/debug_extension_mv3/web/data_types.g.dart b/dwds/debug_extension_mv3/web/data_types.g.dart index 34c78b418..a625af3e2 100644 --- a/dwds/debug_extension_mv3/web/data_types.g.dart +++ b/dwds/debug_extension_mv3/web/data_types.g.dart @@ -10,6 +10,7 @@ Serializer _$connectFailureSerializer = new _$ConnectFailureSerializer(); Serializer _$devToolsOpenerSerializer = new _$DevToolsOpenerSerializer(); +Serializer _$encodedUriSerializer = new _$EncodedUriSerializer(); Serializer _$devToolsUrlSerializer = new _$DevToolsUrlSerializer(); Serializer _$debugStateChangeSerializer = new _$DebugStateChangeSerializer(); @@ -108,6 +109,45 @@ class _$DevToolsOpenerSerializer } } +class _$EncodedUriSerializer implements StructuredSerializer { + @override + final Iterable types = const [EncodedUri, _$EncodedUri]; + @override + final String wireName = 'EncodedUri'; + + @override + Iterable serialize(Serializers serializers, EncodedUri object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'uri', + serializers.serialize(object.uri, specifiedType: const FullType(String)), + ]; + + return result; + } + + @override + EncodedUri deserialize(Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new EncodedUriBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current! as String; + iterator.moveNext(); + final Object? value = iterator.current; + switch (key) { + case 'uri': + result.uri = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; + } + } + + return result.build(); + } +} + class _$DevToolsUrlSerializer implements StructuredSerializer { @override final Iterable types = const [DevToolsUrl, _$DevToolsUrl]; @@ -386,6 +426,84 @@ class DevToolsOpenerBuilder } } +class _$EncodedUri extends EncodedUri { + @override + final String uri; + + factory _$EncodedUri([void Function(EncodedUriBuilder)? updates]) => + (new EncodedUriBuilder()..update(updates))._build(); + + _$EncodedUri._({required this.uri}) : super._() { + BuiltValueNullFieldError.checkNotNull(uri, r'EncodedUri', 'uri'); + } + + @override + EncodedUri rebuild(void Function(EncodedUriBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + EncodedUriBuilder toBuilder() => new EncodedUriBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is EncodedUri && uri == other.uri; + } + + @override + int get hashCode { + return $jf($jc(0, uri.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'EncodedUri')..add('uri', uri)) + .toString(); + } +} + +class EncodedUriBuilder implements Builder { + _$EncodedUri? _$v; + + String? _uri; + String? get uri => _$this._uri; + set uri(String? uri) => _$this._uri = uri; + + EncodedUriBuilder(); + + EncodedUriBuilder get _$this { + final $v = _$v; + if ($v != null) { + _uri = $v.uri; + _$v = null; + } + return this; + } + + @override + void replace(EncodedUri other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$EncodedUri; + } + + @override + void update(void Function(EncodedUriBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + EncodedUri build() => _build(); + + _$EncodedUri _build() { + final _$result = _$v ?? + new _$EncodedUri._( + uri: BuiltValueNullFieldError.checkNotNull( + uri, r'EncodedUri', 'uri')); + replace(_$result); + return _$result; + } +} + class _$DevToolsUrl extends DevToolsUrl { @override final int tabId; diff --git a/dwds/debug_extension_mv3/web/debug_session.dart b/dwds/debug_extension_mv3/web/debug_session.dart index 02977643f..1b7a00023 100644 --- a/dwds/debug_extension_mv3/web/debug_session.dart +++ b/dwds/debug_extension_mv3/web/debug_session.dart @@ -24,6 +24,7 @@ import 'package:sse/client/sse_client.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'chrome_api.dart'; +import 'cross_extension_communication.dart'; import 'data_serializers.dart'; import 'data_types.dart'; import 'logger.dart'; @@ -74,6 +75,7 @@ enum TabType { } enum Trigger { + angularDartDevTools, extensionPanel, extensionIcon, } @@ -152,6 +154,9 @@ String _translateChromeError(String chromeErrorMessage) { Future _onDebuggerEvent( Debuggee source, String method, Object? params) async { + maybeForwardMessageToAngularDartDevTools( + method: method, params: params, tabId: source.tabId); + if (method == 'Runtime.executionContextCreated') { return _maybeConnectToDwds(source.tabId, params); } @@ -237,13 +242,10 @@ void _routeDwdsEvent(String eventData, SocketClient client, int tabId) { if (message is ExtensionRequest) { _forwardDwdsEventToChromeDebugger(message, client, tabId); } else if (message is ExtensionEvent) { - switch (message.method) { - case 'dwds.encodedUri': - // TODO(elliette): Forward to external extensions. - break; - case 'dwds.devtoolsUri': - _openDevTools(message.params, dartAppTabId: tabId); - break; + maybeForwardMessageToAngularDartDevTools( + method: message.method, params: message.params, tabId: tabId); + if (message.method == 'dwds.devtoolsUri') { + _openDevTools(message.params, dartAppTabId: tabId); } } } @@ -297,8 +299,10 @@ void _openDevTools(String devToolsUrl, {required int dartAppTabId}) async { } // Send the DevTools URL to the extension panels: _sendDevToolsUrlMessage(devToolsUrl, dartAppTabId: dartAppTabId); - // Open a separate tab / window if triggered through the extension icon: - if (debugSession.trigger == Trigger.extensionIcon) { + // Open a separate tab / window if triggered through the extension icon or + // through AngularDart DevTools: + if (debugSession.trigger == Trigger.extensionIcon || + debugSession.trigger == Trigger.angularDartDevTools) { final devToolsOpener = await fetchStorageObject( type: StorageObject.devToolsOpener); final devToolsTab = await createTab( diff --git a/dwds/debug_extension_mv3/web/manifest.json b/dwds/debug_extension_mv3/web/manifest.json index 5481fea40..e44ed5828 100644 --- a/dwds/debug_extension_mv3/web/manifest.json +++ b/dwds/debug_extension_mv3/web/manifest.json @@ -6,6 +6,11 @@ "action": { "default_icon": "static_assets/dart_dev.png" }, + "externally_connectable": { + "ids": [ + "nbkbficgbembimioedhceniahniffgpl" + ] + }, "permissions": [ "debugger", "notifications", diff --git a/dwds/debug_extension_mv3/web/storage.dart b/dwds/debug_extension_mv3/web/storage.dart index 5ba186005..31a3fd228 100644 --- a/dwds/debug_extension_mv3/web/storage.dart +++ b/dwds/debug_extension_mv3/web/storage.dart @@ -17,16 +17,8 @@ import 'logger.dart'; enum StorageObject { debugInfo, - devToolsOpener; - - String get keyName { - switch (this) { - case StorageObject.debugInfo: - return 'debugInfo'; - case StorageObject.devToolsOpener: - return 'devToolsOpener'; - } - } + devToolsOpener, + encodedUri; Persistance get persistance { switch (this) { @@ -34,6 +26,8 @@ enum StorageObject { return Persistance.sessionOnly; case StorageObject.devToolsOpener: return Persistance.acrossSessions; + case StorageObject.encodedUri: + return Persistance.sessionOnly; } } } @@ -103,6 +97,6 @@ StorageArea _getStorageArea(Persistance persistance) { } String _createStorageKey(StorageObject type, int? tabId) { - if (tabId == null) return type.keyName; - return '$tabId-${type.keyName}'; + if (tabId == null) return type.name; + return '$tabId-${type.name}'; }