From f476821fa148f6167527bb2f7b4c455fe20dca6e Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Wed, 14 Dec 2022 13:29:15 -0800 Subject: [PATCH 01/12] Added debugger_panel.dart --- .../web/debugger_panel.dart | 32 +++++++++++++++++++ .../web/static_assets/debugger_panel.html | 3 ++ 2 files changed, 35 insertions(+) create mode 100644 dwds/debug_extension_mv3/web/debugger_panel.dart diff --git a/dwds/debug_extension_mv3/web/debugger_panel.dart b/dwds/debug_extension_mv3/web/debugger_panel.dart new file mode 100644 index 000000000..f45d32eef --- /dev/null +++ b/dwds/debug_extension_mv3/web/debugger_panel.dart @@ -0,0 +1,32 @@ +// 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 debugger_panel; + +import 'dart:html'; + +import 'package:js/js.dart'; + +import 'debug_session.dart'; +import 'logger.dart'; +import 'utils.dart'; + +void main() async { + _registerListeners(); +} + +void _registerListeners() { + final launchDebuggerButton = document.getElementById('launchDebuggerButton') as ButtonElement; + launchDebuggerButton.addEventListener('click', _launchDebugger); +} + +void _launchDebugger(Event _) async { + final dartAppTab = await getActiveTab(); + if (dartAppTab != null) { + attachDebugger(dartAppTab.id); + } else { + debugWarn('Could not get current tab'); + } +} \ No newline at end of file diff --git a/dwds/debug_extension_mv3/web/static_assets/debugger_panel.html b/dwds/debug_extension_mv3/web/static_assets/debugger_panel.html index 43bcbee1c..7ed9e14e9 100644 --- a/dwds/debug_extension_mv3/web/static_assets/debugger_panel.html +++ b/dwds/debug_extension_mv3/web/static_assets/debugger_panel.html @@ -30,6 +30,7 @@
@@ -41,6 +42,8 @@
+ + From 958c5efc085f175969793aa6f962b7b7a985bb52 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Thu, 15 Dec 2022 15:55:25 -0800 Subject: [PATCH 02/12] Debugger panel loads --- dwds/debug_extension_mv3/web/background.dart | 38 +- .../web/data_serializers.dart | 3 + .../web/data_serializers.g.dart | 3 + dwds/debug_extension_mv3/web/data_types.dart | 49 ++ .../debug_extension_mv3/web/data_types.g.dart | 452 ++++++++++++++++++ .../web/debug_session.dart | 135 +++++- .../web/debugger_panel.dart | 177 ++++++- dwds/debug_extension_mv3/web/detector.dart | 13 +- dwds/debug_extension_mv3/web/messaging.dart | 28 +- .../web/static_assets/debugger_panel.html | 89 ++-- .../web/static_assets/styles.css | 15 +- 11 files changed, 914 insertions(+), 88 deletions(-) diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart index 87cd29f1f..4a4044f37 100644 --- a/dwds/debug_extension_mv3/web/background.dart +++ b/dwds/debug_extension_mv3/web/background.dart @@ -12,6 +12,7 @@ import 'package:dwds/data/debug_info.dart'; import 'package:dwds/data/extension_request.dart'; import 'package:js/js.dart'; +import 'data_types.dart'; import 'debug_session.dart'; import 'chrome_api.dart'; import 'lifeline_ports.dart'; @@ -45,23 +46,30 @@ void _registerListeners() { .addListener(allowInterop(_detectNavigationAwayFromDartApp)); // Detect clicks on the Dart Debug Extension icon. - chrome.action.onClicked.addListener(allowInterop(_startDebugSession)); + chrome.action.onClicked.addListener(allowInterop( + (Tab tab) => _startDebugSession( + tab.id, + trigger: Trigger.extensionIcon, + ), + )); } -// TODO(elliette): Start a debug session instead. -Future _startDebugSession(Tab currentTab) async { - final tabId = currentTab.id; +Future _startDebugSession(int tabId, {required Trigger trigger}) async { final debugInfo = await _fetchDebugInfo(tabId); final extensionUrl = debugInfo?.extensionUrl; if (extensionUrl == null) { _showWarningNotification('Can\'t debug Dart app. Extension URL not found.'); + sendConnectFailureMessage(ConnectFailureReason.no_dart_app, dartAppTabId: tabId); return; } final isAuthenticated = await _authenticateUser(extensionUrl, tabId); - if (!isAuthenticated) return; + if (!isAuthenticated) { + sendConnectFailureMessage(ConnectFailureReason.authentication, dartAppTabId: tabId); + return; + } - maybeCreateLifelinePort(currentTab.id); - attachDebugger(tabId); + maybeCreateLifelinePort(tabId); + attachDebugger(tabId, trigger: trigger); } Future _authenticateUser(String extensionUrl, int tabId) async { @@ -113,6 +121,20 @@ void _handleRuntimeMessages( _setDebuggableIcon(); } }); + + interceptMessage( + message: jsRequest, + expectedType: MessageType.debugStateChange, + expectedSender: Script.debuggerPanel, + expectedRecipient: Script.background, + messageHandler: (DebugStateChange debugStateChange) { + final newState = debugStateChange.newState; + final tabId = debugStateChange.tabId; + if (newState == DebugStateChange.startDebugging) { + debugLog('sending start debugging for $tabId, triggered from extension panel'); + _startDebugSession(tabId, trigger: Trigger.extensionPanel); + } + }); } void _detectNavigationAwayFromDartApp(NavigationInfo navigationInfo) async { @@ -125,7 +147,7 @@ void _detectNavigationAwayFromDartApp(NavigationInfo navigationInfo) async { detachDebugger( tabId, type: TabType.dartApp, - reason: 'Navigated away from Dart app.', + reason: DetachReason.navigated_away_from_app, ); } } diff --git a/dwds/debug_extension_mv3/web/data_serializers.dart b/dwds/debug_extension_mv3/web/data_serializers.dart index 1cb6268ae..9c4e31460 100644 --- a/dwds/debug_extension_mv3/web/data_serializers.dart +++ b/dwds/debug_extension_mv3/web/data_serializers.dart @@ -15,8 +15,11 @@ part 'data_serializers.g.dart'; /// Serializers for all the data types used in the Dart Debug Extension. @SerializersFor([ BatchedEvents, + ConnectFailure, DebugInfo, + DebugStateChange, DevToolsOpener, + DevToolsUrl, DevToolsRequest, ExtensionEvent, ExtensionRequest, diff --git a/dwds/debug_extension_mv3/web/data_serializers.g.dart b/dwds/debug_extension_mv3/web/data_serializers.g.dart index 5dc6bb9b1..7c15ed146 100644 --- a/dwds/debug_extension_mv3/web/data_serializers.g.dart +++ b/dwds/debug_extension_mv3/web/data_serializers.g.dart @@ -8,9 +8,12 @@ part of 'data_serializers.dart'; Serializers _$serializers = (new Serializers().toBuilder() ..add(BatchedEvents.serializer) + ..add(ConnectFailure.serializer) ..add(DebugInfo.serializer) + ..add(DebugStateChange.serializer) ..add(DevToolsOpener.serializer) ..add(DevToolsRequest.serializer) + ..add(DevToolsUrl.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 69df9d70a..eb22eb4e7 100644 --- a/dwds/debug_extension_mv3/web/data_types.dart +++ b/dwds/debug_extension_mv3/web/data_types.dart @@ -7,6 +7,21 @@ import 'package:built_value/serializer.dart'; part 'data_types.g.dart'; +abstract class ConnectFailure + implements Built { + static Serializer get serializer => + _$connectFailureSerializer; + + factory ConnectFailure([Function(ConnectFailureBuilder) updates]) = + _$ConnectFailure; + + ConnectFailure._(); + + int get tabId; + + String? get reason; +} + abstract class DevToolsOpener implements Built { static Serializer get serializer => @@ -19,3 +34,37 @@ abstract class DevToolsOpener bool get newWindow; } + +abstract class DevToolsUrl implements Built { + static Serializer get serializer => _$devToolsUrlSerializer; + + factory DevToolsUrl([Function(DevToolsUrlBuilder) updates]) = _$DevToolsUrl; + + DevToolsUrl._(); + + int get tabId; + + String get url; +} + +abstract class DebugStateChange + implements Built { + static const startDebugging = 'start-debugging'; + static const stopDebugging = 'stop-debugging'; + static const failedToConnect = 'failed-to-connect'; + + static Serializer get serializer => + _$debugStateChangeSerializer; + + factory DebugStateChange([Function(DebugStateChangeBuilder) updates]) = + _$DebugStateChange; + + DebugStateChange._(); + + int get tabId; + + /// Can only be [startDebugging] or [stopDebugging]. + String get newState; + + String? get reason; +} diff --git a/dwds/debug_extension_mv3/web/data_types.g.dart b/dwds/debug_extension_mv3/web/data_types.g.dart index cfdbbe5f5..34c78b418 100644 --- a/dwds/debug_extension_mv3/web/data_types.g.dart +++ b/dwds/debug_extension_mv3/web/data_types.g.dart @@ -6,8 +6,65 @@ part of 'data_types.dart'; // BuiltValueGenerator // ************************************************************************** +Serializer _$connectFailureSerializer = + new _$ConnectFailureSerializer(); Serializer _$devToolsOpenerSerializer = new _$DevToolsOpenerSerializer(); +Serializer _$devToolsUrlSerializer = new _$DevToolsUrlSerializer(); +Serializer _$debugStateChangeSerializer = + new _$DebugStateChangeSerializer(); + +class _$ConnectFailureSerializer + implements StructuredSerializer { + @override + final Iterable types = const [ConnectFailure, _$ConnectFailure]; + @override + final String wireName = 'ConnectFailure'; + + @override + Iterable serialize(Serializers serializers, ConnectFailure object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'tabId', + serializers.serialize(object.tabId, specifiedType: const FullType(int)), + ]; + Object? value; + value = object.reason; + if (value != null) { + result + ..add('reason') + ..add(serializers.serialize(value, + specifiedType: const FullType(String))); + } + return result; + } + + @override + ConnectFailure deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new ConnectFailureBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current! as String; + iterator.moveNext(); + final Object? value = iterator.current; + switch (key) { + case 'tabId': + result.tabId = serializers.deserialize(value, + specifiedType: const FullType(int))! as int; + break; + case 'reason': + result.reason = serializers.deserialize(value, + specifiedType: const FullType(String)) as String?; + break; + } + } + + return result.build(); + } +} class _$DevToolsOpenerSerializer implements StructuredSerializer { @@ -51,6 +108,202 @@ class _$DevToolsOpenerSerializer } } +class _$DevToolsUrlSerializer implements StructuredSerializer { + @override + final Iterable types = const [DevToolsUrl, _$DevToolsUrl]; + @override + final String wireName = 'DevToolsUrl'; + + @override + Iterable serialize(Serializers serializers, DevToolsUrl object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'tabId', + serializers.serialize(object.tabId, specifiedType: const FullType(int)), + 'url', + serializers.serialize(object.url, specifiedType: const FullType(String)), + ]; + + return result; + } + + @override + DevToolsUrl deserialize(Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new DevToolsUrlBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current! as String; + iterator.moveNext(); + final Object? value = iterator.current; + switch (key) { + case 'tabId': + result.tabId = serializers.deserialize(value, + specifiedType: const FullType(int))! as int; + break; + case 'url': + result.url = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; + } + } + + return result.build(); + } +} + +class _$DebugStateChangeSerializer + implements StructuredSerializer { + @override + final Iterable types = const [DebugStateChange, _$DebugStateChange]; + @override + final String wireName = 'DebugStateChange'; + + @override + Iterable serialize(Serializers serializers, DebugStateChange object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'tabId', + serializers.serialize(object.tabId, specifiedType: const FullType(int)), + 'newState', + serializers.serialize(object.newState, + specifiedType: const FullType(String)), + ]; + Object? value; + value = object.reason; + if (value != null) { + result + ..add('reason') + ..add(serializers.serialize(value, + specifiedType: const FullType(String))); + } + return result; + } + + @override + DebugStateChange deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new DebugStateChangeBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current! as String; + iterator.moveNext(); + final Object? value = iterator.current; + switch (key) { + case 'tabId': + result.tabId = serializers.deserialize(value, + specifiedType: const FullType(int))! as int; + break; + case 'newState': + result.newState = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; + case 'reason': + result.reason = serializers.deserialize(value, + specifiedType: const FullType(String)) as String?; + break; + } + } + + return result.build(); + } +} + +class _$ConnectFailure extends ConnectFailure { + @override + final int tabId; + @override + final String? reason; + + factory _$ConnectFailure([void Function(ConnectFailureBuilder)? updates]) => + (new ConnectFailureBuilder()..update(updates))._build(); + + _$ConnectFailure._({required this.tabId, this.reason}) : super._() { + BuiltValueNullFieldError.checkNotNull(tabId, r'ConnectFailure', 'tabId'); + } + + @override + ConnectFailure rebuild(void Function(ConnectFailureBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + ConnectFailureBuilder toBuilder() => + new ConnectFailureBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is ConnectFailure && + tabId == other.tabId && + reason == other.reason; + } + + @override + int get hashCode { + return $jf($jc($jc(0, tabId.hashCode), reason.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'ConnectFailure') + ..add('tabId', tabId) + ..add('reason', reason)) + .toString(); + } +} + +class ConnectFailureBuilder + implements Builder { + _$ConnectFailure? _$v; + + int? _tabId; + int? get tabId => _$this._tabId; + set tabId(int? tabId) => _$this._tabId = tabId; + + String? _reason; + String? get reason => _$this._reason; + set reason(String? reason) => _$this._reason = reason; + + ConnectFailureBuilder(); + + ConnectFailureBuilder get _$this { + final $v = _$v; + if ($v != null) { + _tabId = $v.tabId; + _reason = $v.reason; + _$v = null; + } + return this; + } + + @override + void replace(ConnectFailure other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$ConnectFailure; + } + + @override + void update(void Function(ConnectFailureBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + ConnectFailure build() => _build(); + + _$ConnectFailure _build() { + final _$result = _$v ?? + new _$ConnectFailure._( + tabId: BuiltValueNullFieldError.checkNotNull( + tabId, r'ConnectFailure', 'tabId'), + reason: reason); + replace(_$result); + return _$result; + } +} + class _$DevToolsOpener extends DevToolsOpener { @override final bool newWindow; @@ -133,4 +386,203 @@ class DevToolsOpenerBuilder } } +class _$DevToolsUrl extends DevToolsUrl { + @override + final int tabId; + @override + final String url; + + factory _$DevToolsUrl([void Function(DevToolsUrlBuilder)? updates]) => + (new DevToolsUrlBuilder()..update(updates))._build(); + + _$DevToolsUrl._({required this.tabId, required this.url}) : super._() { + BuiltValueNullFieldError.checkNotNull(tabId, r'DevToolsUrl', 'tabId'); + BuiltValueNullFieldError.checkNotNull(url, r'DevToolsUrl', 'url'); + } + + @override + DevToolsUrl rebuild(void Function(DevToolsUrlBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + DevToolsUrlBuilder toBuilder() => new DevToolsUrlBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is DevToolsUrl && tabId == other.tabId && url == other.url; + } + + @override + int get hashCode { + return $jf($jc($jc(0, tabId.hashCode), url.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'DevToolsUrl') + ..add('tabId', tabId) + ..add('url', url)) + .toString(); + } +} + +class DevToolsUrlBuilder implements Builder { + _$DevToolsUrl? _$v; + + int? _tabId; + int? get tabId => _$this._tabId; + set tabId(int? tabId) => _$this._tabId = tabId; + + String? _url; + String? get url => _$this._url; + set url(String? url) => _$this._url = url; + + DevToolsUrlBuilder(); + + DevToolsUrlBuilder get _$this { + final $v = _$v; + if ($v != null) { + _tabId = $v.tabId; + _url = $v.url; + _$v = null; + } + return this; + } + + @override + void replace(DevToolsUrl other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$DevToolsUrl; + } + + @override + void update(void Function(DevToolsUrlBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + DevToolsUrl build() => _build(); + + _$DevToolsUrl _build() { + final _$result = _$v ?? + new _$DevToolsUrl._( + tabId: BuiltValueNullFieldError.checkNotNull( + tabId, r'DevToolsUrl', 'tabId'), + url: BuiltValueNullFieldError.checkNotNull( + url, r'DevToolsUrl', 'url')); + replace(_$result); + return _$result; + } +} + +class _$DebugStateChange extends DebugStateChange { + @override + final int tabId; + @override + final String newState; + @override + final String? reason; + + factory _$DebugStateChange( + [void Function(DebugStateChangeBuilder)? updates]) => + (new DebugStateChangeBuilder()..update(updates))._build(); + + _$DebugStateChange._( + {required this.tabId, required this.newState, this.reason}) + : super._() { + BuiltValueNullFieldError.checkNotNull(tabId, r'DebugStateChange', 'tabId'); + BuiltValueNullFieldError.checkNotNull( + newState, r'DebugStateChange', 'newState'); + } + + @override + DebugStateChange rebuild(void Function(DebugStateChangeBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + DebugStateChangeBuilder toBuilder() => + new DebugStateChangeBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is DebugStateChange && + tabId == other.tabId && + newState == other.newState && + reason == other.reason; + } + + @override + int get hashCode { + return $jf( + $jc($jc($jc(0, tabId.hashCode), newState.hashCode), reason.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'DebugStateChange') + ..add('tabId', tabId) + ..add('newState', newState) + ..add('reason', reason)) + .toString(); + } +} + +class DebugStateChangeBuilder + implements Builder { + _$DebugStateChange? _$v; + + int? _tabId; + int? get tabId => _$this._tabId; + set tabId(int? tabId) => _$this._tabId = tabId; + + String? _newState; + String? get newState => _$this._newState; + set newState(String? newState) => _$this._newState = newState; + + String? _reason; + String? get reason => _$this._reason; + set reason(String? reason) => _$this._reason = reason; + + DebugStateChangeBuilder(); + + DebugStateChangeBuilder get _$this { + final $v = _$v; + if ($v != null) { + _tabId = $v.tabId; + _newState = $v.newState; + _reason = $v.reason; + _$v = null; + } + return this; + } + + @override + void replace(DebugStateChange other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$DebugStateChange; + } + + @override + void update(void Function(DebugStateChangeBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + DebugStateChange build() => _build(); + + _$DebugStateChange _build() { + final _$result = _$v ?? + new _$DebugStateChange._( + tabId: BuiltValueNullFieldError.checkNotNull( + tabId, r'DebugStateChange', 'tabId'), + newState: BuiltValueNullFieldError.checkNotNull( + newState, r'DebugStateChange', 'newState'), + reason: reason); + replace(_$result); + return _$result; + } +} + // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas diff --git a/dwds/debug_extension_mv3/web/debug_session.dart b/dwds/debug_extension_mv3/web/debug_session.dart index 4e265c392..b8c299e81 100644 --- a/dwds/debug_extension_mv3/web/debug_session.dart +++ b/dwds/debug_extension_mv3/web/debug_session.dart @@ -30,6 +30,7 @@ import 'logger.dart'; import 'storage.dart'; import 'utils.dart'; import 'web_api.dart'; +import 'messaging.dart'; const _notADartAppAlert = 'No Dart application detected.' ' Are you trying to debug an application that includes a Chrome hosted app' @@ -40,14 +41,47 @@ const _notADartAppAlert = 'No Dart application detected.' const _devToolsAlreadyOpenedAlert = 'DevTools is already opened on a different window.'; +const _authSuccessResponse = 'Dart Debug Authentication Success!'; + final _debugSessions = <_DebugSession>[]; +final _tabIdToTrigger = {}; + +enum DetachReason { + canceled_by_user, + connection_error_event, + connection_done_event, + devtools_tab_closed, + navigated_away_from_app, + unknown; + + factory DetachReason.fromString(String value) { + return DetachReason.values.byName(value); + } +} + +enum ConnectFailureReason { + authentication, + no_dart_app, + timeout, + unknown; + + factory ConnectFailureReason.fromString(String value) { + return ConnectFailureReason.values.byName(value); + } +} enum TabType { dartApp, devTools, } -void attachDebugger(int dartAppTabId) { +enum Trigger { + extensionPanel, + extensionIcon, +} + +void attachDebugger(int dartAppTabId, {required Trigger trigger}) { + _tabIdToTrigger[dartAppTabId] = trigger; _registerDebugEventListeners(); chrome.debugger.attach( Debuggee(tabId: dartAppTabId), @@ -61,7 +95,7 @@ void attachDebugger(int dartAppTabId) { void detachDebugger( int tabId, { required TabType type, - required String reason, + required DetachReason reason, }) async { final debugSession = _debugSessionForTab(tabId, type: type); if (debugSession == null) return; @@ -79,12 +113,17 @@ void detachDebugger( void _registerDebugEventListeners() { chrome.debugger.onEvent.addListener(allowInterop(_onDebuggerEvent)); - chrome.debugger.onDetach.addListener(allowInterop(_handleDebuggerDetach)); + chrome.debugger.onDetach.addListener(allowInterop( + (source, _) => _handleDebuggerDetach( + source, + DetachReason.canceled_by_user, + ), + )); chrome.tabs.onRemoved.addListener(allowInterop( (tabId, _) => detachDebugger( tabId, type: TabType.devTools, - reason: 'DevTools tab closed.', + reason: DetachReason.devtools_tab_closed, ), )); } @@ -141,6 +180,7 @@ Future _maybeConnectToDwds(int tabId, Object? params) async { ); if (!connected) { debugWarn('Failed to connect to DWDS for $contextOrigin.'); + sendConnectFailureMessage(ConnectFailureReason.unknown, dartAppTabId: tabId); } } @@ -158,22 +198,25 @@ Future _connectToDwds({ final client = uri.isScheme('ws') || uri.isScheme('wss') ? WebSocketClient(WebSocketChannel.connect(uri)) : SseSocketClient(SseClient(uri.toString())); + final trigger = _tabIdToTrigger[dartAppTabId]; final debugSession = _DebugSession( client: client, appTabId: dartAppTabId, + trigger: trigger, onIncoming: (data) => _routeDwdsEvent(data, client, dartAppTabId), onDone: () { detachDebugger( dartAppTabId, type: TabType.dartApp, - reason: 'Done event in DWDS stream.', + reason: DetachReason.connection_done_event, ); }, onError: (err) { + debugWarn('Connection error: $err', verbose: true); detachDebugger( dartAppTabId, type: TabType.dartApp, - reason: 'Error in DWDS stream: $err', + reason: DetachReason.connection_error_event, ); }, cancelOnError: true, @@ -200,7 +243,7 @@ void _routeDwdsEvent(String eventData, SocketClient client, int tabId) { // TODO(elliette): Forward to external extensions. break; case 'dwds.devtoolsUri': - _openDevTools(message.params, dartTabId: tabId); + _openDevTools(message.params, dartAppTabId: tabId); break; } } @@ -243,26 +286,31 @@ void _forwardChromeDebuggerEventToDwds( } } -void _openDevTools(String devToolsUrl, {required int dartTabId}) async { +void _openDevTools(String devToolsUrl, {required int dartAppTabId}) async { if (devToolsUrl.isEmpty) { debugError('DevTools URL is empty.'); return; } - final debugSession = _debugSessionForTab(dartTabId, type: TabType.dartApp); + final debugSession = _debugSessionForTab(dartAppTabId, type: TabType.dartApp); if (debugSession == null) { debugError('Debug session not found.'); return; } - final devToolsOpener = await fetchStorageObject( - type: StorageObject.devToolsOpener); - final devToolsTab = await createTab( - devToolsUrl, - inNewWindow: devToolsOpener?.newWindow ?? false, - ); - debugSession.devToolsTabId = devToolsTab.id; + // 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) { + final devToolsOpener = await fetchStorageObject( + type: StorageObject.devToolsOpener); + final devToolsTab = await createTab( + devToolsUrl, + inNewWindow: devToolsOpener?.newWindow ?? false, + ); + debugSession.devToolsTabId = devToolsTab.id; + } } -void _handleDebuggerDetach(Debuggee source, String reason) async { +void _handleDebuggerDetach(Debuggee source, DetachReason reason) async { debugLog( 'Debugger detached due to: $reason', verbose: true, @@ -272,8 +320,11 @@ void _handleDebuggerDetach(Debuggee source, String reason) async { if (debugSession == null) return; debugLog('Removing debug session...'); _removeDebugSession(debugSession); + // Notify the extension panels that the debug session has ended: + _sendStopDebuggingMessage(reason, dartAppTabId: source.tabId); // Maybe close the associated DevTools tab as well: final devToolsTabId = debugSession.devToolsTabId; + if (devToolsTabId == null) return; final devToolsTab = await getTab(devToolsTabId); if (devToolsTab != null) { debugLog('Closing DevTools tab...'); @@ -297,6 +348,43 @@ void _removeDebugSession(_DebugSession debugSession) { } } +void sendConnectFailureMessage(ConnectFailureReason reason, + {required int dartAppTabId}) async { + final json = jsonEncode(serializers.serialize(ConnectFailure((b) => b + ..tabId = dartAppTabId + ..reason = reason.name))); + sendRuntimeMessage( + type: MessageType.connectFailure, + body: json, + sender: Script.background, + recipient: Script.debuggerPanel); +} + +void _sendDevToolsUrlMessage(String devToolsUrl, + {required int dartAppTabId}) async { + final json = jsonEncode(serializers.serialize(DevToolsUrl((b) => b + ..tabId = dartAppTabId + ..url = devToolsUrl))); + sendRuntimeMessage( + type: MessageType.devToolsUrl, + body: json, + sender: Script.background, + recipient: Script.debuggerPanel); +} + +void _sendStopDebuggingMessage(DetachReason reason, + {required int dartAppTabId}) async { + final json = jsonEncode(serializers.serialize(DebugStateChange((b) => b + ..tabId = dartAppTabId + ..reason = reason.name + ..newState = DebugStateChange.stopDebugging))); + sendRuntimeMessage( + type: MessageType.debugStateChange, + body: json, + sender: Script.background, + recipient: Script.debuggerPanel); +} + _DebugSession? _debugSessionForTab(tabId, {required TabType type}) { switch (type) { case TabType.dartApp: @@ -330,8 +418,11 @@ class _DebugSession { // The tab ID that contains the running Dart application. final int appTabId; + // What triggered the debug session (debugger panel, extension icon, etc.) + final Trigger? trigger; + // The tab ID that contains the corresponding Dart DevTools. - late final int devToolsTabId; + late final int? devToolsTabId; // Socket client for communication with dwds extension backend. late final SocketClient _socketClient; @@ -347,6 +438,7 @@ class _DebugSession { _DebugSession({ required client, required this.appTabId, + required this.trigger, required void Function(String data) onIncoming, required void Function() onDone, required void Function(dynamic error) onError, @@ -390,3 +482,10 @@ class _DebugSession { _batchController.close(); } } + +Future _fetchDebugInfo(int tabId) { + return fetchStorageObject( + type: StorageObject.debugInfo, + tabId: tabId, + ); +} diff --git a/dwds/debug_extension_mv3/web/debugger_panel.dart b/dwds/debug_extension_mv3/web/debugger_panel.dart index f45d32eef..2cb627880 100644 --- a/dwds/debug_extension_mv3/web/debugger_panel.dart +++ b/dwds/debug_extension_mv3/web/debugger_panel.dart @@ -5,28 +5,183 @@ @JS() library debugger_panel; +import 'dart:convert'; import 'dart:html'; import 'package:js/js.dart'; +import 'package:dwds/data/debug_info.dart'; -import 'debug_session.dart'; +import 'chrome_api.dart'; +import 'data_types.dart'; +import 'messaging.dart'; +import 'data_serializers.dart'; import 'logger.dart'; -import 'utils.dart'; +import 'debug_session.dart'; +import 'storage.dart'; -void main() async { +bool connecting = false; + +void main() { _registerListeners(); + _maybeUpdateFileABugLink(); } void _registerListeners() { - final launchDebuggerButton = document.getElementById('launchDebuggerButton') as ButtonElement; - launchDebuggerButton.addEventListener('click', _launchDebugger); + chrome.runtime.onMessage.addListener(allowInterop(_handleRuntimeMessages)); + final launchDebuggerButton = + document.getElementById('launchDebuggerButton') as ButtonElement; + launchDebuggerButton.addEventListener('click', _launchDebugConnection); +} + +void _handleRuntimeMessages( + dynamic jsRequest, MessageSender sender, Function sendResponse) async { + if (jsRequest is! String) return; + final tabId = chrome.devtools.inspectedWindow.tabId; + interceptMessage( + message: jsRequest, + expectedType: MessageType.devToolsUrl, + expectedSender: Script.background, + expectedRecipient: Script.debuggerPanel, + messageHandler: (DevToolsUrl devToolsUrl) async { + if (devToolsUrl.tabId != tabId) { + debugWarn( + 'Received DevTools URL, but Dart app tab does not match current tab.'); + return; + } + connecting = false; + _injectDevToolsIframe(devToolsUrl.url); + }); + + interceptMessage( + message: jsRequest, + expectedType: MessageType.debugStateChange, + expectedSender: Script.background, + expectedRecipient: Script.debuggerPanel, + messageHandler: (DebugStateChange debugStateChange) async { + if (debugStateChange.tabId != tabId) { + debugWarn( + 'Received debug state change request, but Dart app tab does not match current tab.'); + return; + } + if (debugStateChange.newState == DebugStateChange.stopDebugging) { + _handleDebugConnectionLost(debugStateChange.reason); + } + }); + + interceptMessage( + message: jsRequest, + expectedType: MessageType.connectFailure, + expectedSender: Script.background, + expectedRecipient: Script.debuggerPanel, + messageHandler: (ConnectFailure connectFailure) async { + if (connectFailure.tabId != tabId) { + return; + } + connecting = false; + _handleConnectFailure( + ConnectFailureReason.fromString(connectFailure.reason ?? 'unknown'), + ); + }); +} + +void _maybeUpdateFileABugLink() async { + final debugInfo = await fetchStorageObject( + type: StorageObject.debugInfo, + tabId: chrome.devtools.inspectedWindow.tabId, + ); + final isInternal = debugInfo?.isInternalBuild ?? false; + if (isInternal) { + final bugLink = document.getElementById('bugLink'); + if (bugLink == null) return; + bugLink.setAttribute('src', + 'https://b.corp.google.com/issues/new?component=775375&template=1369639'); + } +} + +void _handleDebugConnectionLost(String? reason) { + final detachReason = DetachReason.fromString(reason ?? 'unknown'); + // Remove Dart DevTools IFRAME: + _removeDevToolsIframe(); + _updateElementVisibility('landingPage', visible: true); + if (detachReason != DetachReason.canceled_by_user) { + _showWarningBanner('Lost connection.'); + } +} + +void _handleConnectFailure(ConnectFailureReason reason) { + switch (reason) { + case ConnectFailureReason.authentication: + _showWarningBanner('Please re-authenticate and try again.'); + break; + case ConnectFailureReason.no_dart_app: + _showWarningBanner('No Dart app detected.'); + break; + case ConnectFailureReason.timeout: + _showWarningBanner('Connection timed out.'); + break; + default: + _showWarningBanner('Failed to connect, please try again.'); + } + _updateElementVisibility('launchDebuggerButton', visible: true); + _updateElementVisibility('loadingSpinner', visible: false); +} + +void _showWarningBanner(String message) { + final warningMsg = document.getElementById('warningMsg'); + warningMsg?.setInnerHtml(message); + _updateElementVisibility('warningBanner', visible: true); +} + +void _launchDebugConnection(Event _) async { + _updateElementVisibility('launchDebuggerButton', visible: false); + _updateElementVisibility('loadingSpinner', visible: true); + final dartAppTabId = chrome.devtools.inspectedWindow.tabId; + final json = jsonEncode(serializers.serialize(DebugStateChange((b) => b + ..tabId = dartAppTabId + ..newState = DebugStateChange.startDebugging))); + sendRuntimeMessage( + type: MessageType.debugStateChange, + body: json, + sender: Script.debuggerPanel, + recipient: Script.background); + _maybeHandleConnectionTimeout(); +} + +void _maybeHandleConnectionTimeout() async { + connecting = true; + await Future.delayed(Duration(seconds: 10)); + if (connecting == true) { + _handleConnectFailure(ConnectFailureReason.timeout); + } +} + +void _injectDevToolsIframe(String devToolsUrl) { + final iframeContainer = document.getElementById('iframeContainer'); + if (iframeContainer == null) return; + final iframe = document.createElement('iframe'); + iframe.setAttribute('src', '$devToolsUrl&embed=true&page=debugger'); + iframe.setAttribute('id', 'dartDebugExtensionIframe'); + _updateElementVisibility('landingPage', visible: false); + _updateElementVisibility('lostConnectionSnackbar', visible: false); + _updateElementVisibility('loadingSpinner', visible: false); + _updateElementVisibility('launchDebuggerButton', visible: true); + iframeContainer.append(iframe); } -void _launchDebugger(Event _) async { - final dartAppTab = await getActiveTab(); - if (dartAppTab != null) { - attachDebugger(dartAppTab.id); +void _removeDevToolsIframe() { + final iframeContainer = document.getElementById('iframeContainer'); + final iframe = iframeContainer?.firstChild; + if (iframe == null) return; + iframe.remove(); + _updateElementVisibility('lostConnectionSnackbar', visible: true); +} + +void _updateElementVisibility(String elementId, {required bool visible}) { + final element = document.getElementById(elementId); + if (element == null) return; + if (visible) { + element.classes.remove('hidden'); } else { - debugWarn('Could not get current tab'); + element.classes.add('hidden'); } -} \ No newline at end of file +} diff --git a/dwds/debug_extension_mv3/web/detector.dart b/dwds/debug_extension_mv3/web/detector.dart index fdd3fcb35..0b9357844 100644 --- a/dwds/debug_extension_mv3/web/detector.dart +++ b/dwds/debug_extension_mv3/web/detector.dart @@ -9,7 +9,6 @@ import 'dart:html'; import 'dart:js_util'; import 'package:js/js.dart'; -import 'chrome_api.dart'; import 'logger.dart'; import 'messaging.dart'; @@ -37,16 +36,10 @@ void _sendMessageToBackgroundScript({ required MessageType type, required String body, }) { - final message = Message( - to: Script.background, - from: Script.detector, + sendRuntimeMessage( type: type, body: body, - ); - chrome.runtime.sendMessage( - /*id*/ null, - message.toJSON(), - /*options*/ null, - /*callback*/ null, + sender: Script.detector, + recipient: Script.background, ); } diff --git a/dwds/debug_extension_mv3/web/messaging.dart b/dwds/debug_extension_mv3/web/messaging.dart index 8041e4ade..5aa551ab8 100644 --- a/dwds/debug_extension_mv3/web/messaging.dart +++ b/dwds/debug_extension_mv3/web/messaging.dart @@ -7,13 +7,15 @@ library messaging; import 'dart:convert'; -import 'package:dwds/data/serializers.dart'; import 'package:js/js.dart'; +import 'data_serializers.dart'; +import 'chrome_api.dart'; import 'logger.dart'; enum Script { background, + debuggerPanel, detector; factory Script.fromString(String value) { @@ -22,7 +24,10 @@ enum Script { } enum MessageType { - debugInfo; + connectFailure, + debugInfo, + debugStateChange, + devToolsUrl; factory MessageType.fromString(String value) { return MessageType.values.byName(value); @@ -89,3 +94,22 @@ void interceptMessage({ 'Error intercepting $expectedType from $expectedSender to $expectedRecipient: $error'); } } + +void sendRuntimeMessage( + {required MessageType type, + required String body, + required Script sender, + required Script recipient}) { + final message = Message( + to: recipient, + from: sender, + type: type, + body: body, + ); + chrome.runtime.sendMessage( + /*id*/ null, + message.toJSON(), + /*options*/ null, + /*callback*/ null, + ); +} diff --git a/dwds/debug_extension_mv3/web/static_assets/debugger_panel.html b/dwds/debug_extension_mv3/web/static_assets/debugger_panel.html index 7ed9e14e9..2aec181b6 100644 --- a/dwds/debug_extension_mv3/web/static_assets/debugger_panel.html +++ b/dwds/debug_extension_mv3/web/static_assets/debugger_panel.html @@ -1,49 +1,62 @@ - - - - - - - - - -
- -
-
- Dart Debugger -
+ + + + + + + +
+
+
+ Dart Debugger +
+
+
+ +
+
+
+
+
+

+ Before debugging, please disable focus on the + Sources panel for breakpoints: +

+ Settings > Preferences > Sources > uncheck "Focus Sources panel + when triggering a breakpoint"
-
- -
-
- -
-
-
-

Before debugging, please disable focus on the Sources panel for breakpoints: -

- Settings > Preferences > Sources > uncheck "Focus Sources panel when triggering a breakpoint" -
-
- -
-
- +
+ +
+
+
+ +
+ - - + diff --git a/dwds/debug_extension_mv3/web/static_assets/styles.css b/dwds/debug_extension_mv3/web/static_assets/styles.css index acc0d7c99..3df1fae99 100644 --- a/dwds/debug_extension_mv3/web/static_assets/styles.css +++ b/dwds/debug_extension_mv3/web/static_assets/styles.css @@ -1,3 +1,12 @@ +iframe { + border: 0pt none; + height: 100%; + left: 0px; + position: absolute; + top: 0px; + width: 100%; +} + .dark-theme { background-color: #262626; color: #eeeeee; @@ -75,7 +84,7 @@ h6 { position: fixed; text-align: center; visibility: hidden; - width: 250px; + width: 350px; z-index: 1; } @@ -93,6 +102,10 @@ h6 { visibility: visible; } +.hidden { + visibility: none; +} + @-webkit-keyframes fadein { from { bottom: 0; From 769756be93fa5178f379d83ace880d6a618416fe Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Thu, 15 Dec 2022 16:21:48 -0800 Subject: [PATCH 03/12] Warning message shows up --- dwds/debug_extension_mv3/web/background.dart | 13 +++++++++---- .../web/debugger_panel.dart | 19 ++++++++++++++----- .../web/static_assets/debugger_panel.html | 16 +++++++++++++--- .../web/static_assets/styles.css | 9 +++++++-- 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart index 4a4044f37..748631e87 100644 --- a/dwds/debug_extension_mv3/web/background.dart +++ b/dwds/debug_extension_mv3/web/background.dart @@ -59,13 +59,19 @@ Future _startDebugSession(int tabId, {required Trigger trigger}) async { final extensionUrl = debugInfo?.extensionUrl; if (extensionUrl == null) { _showWarningNotification('Can\'t debug Dart app. Extension URL not found.'); - sendConnectFailureMessage(ConnectFailureReason.no_dart_app, dartAppTabId: tabId); + sendConnectFailureMessage( + ConnectFailureReason.no_dart_app, + dartAppTabId: tabId, + ); return; } final isAuthenticated = await _authenticateUser(extensionUrl, tabId); if (!isAuthenticated) { - sendConnectFailureMessage(ConnectFailureReason.authentication, dartAppTabId: tabId); - return; + sendConnectFailureMessage( + ConnectFailureReason.authentication, + dartAppTabId: tabId, + ); + return; } maybeCreateLifelinePort(tabId); @@ -131,7 +137,6 @@ void _handleRuntimeMessages( final newState = debugStateChange.newState; final tabId = debugStateChange.tabId; if (newState == DebugStateChange.startDebugging) { - debugLog('sending start debugging for $tabId, triggered from extension panel'); _startDebugSession(tabId, trigger: Trigger.extensionPanel); } }); diff --git a/dwds/debug_extension_mv3/web/debugger_panel.dart b/dwds/debug_extension_mv3/web/debugger_panel.dart index 2cb627880..ec63c8a49 100644 --- a/dwds/debug_extension_mv3/web/debugger_panel.dart +++ b/dwds/debug_extension_mv3/web/debugger_panel.dart @@ -74,10 +74,13 @@ void _handleRuntimeMessages( expectedSender: Script.background, expectedRecipient: Script.debuggerPanel, messageHandler: (ConnectFailure connectFailure) async { + debugLog( + 'Received connect failure for ${connectFailure.tabId} vs $tabId'); if (connectFailure.tabId != tabId) { return; } connecting = false; + debugLog('handling connect failure'); _handleConnectFailure( ConnectFailureReason.fromString(connectFailure.reason ?? 'unknown'), ); @@ -93,14 +96,13 @@ void _maybeUpdateFileABugLink() async { if (isInternal) { final bugLink = document.getElementById('bugLink'); if (bugLink == null) return; - bugLink.setAttribute('src', + bugLink.setAttribute('href', 'https://b.corp.google.com/issues/new?component=775375&template=1369639'); } } void _handleDebugConnectionLost(String? reason) { final detachReason = DetachReason.fromString(reason ?? 'unknown'); - // Remove Dart DevTools IFRAME: _removeDevToolsIframe(); _updateElementVisibility('landingPage', visible: true); if (detachReason != DetachReason.canceled_by_user) { @@ -129,7 +131,14 @@ void _handleConnectFailure(ConnectFailureReason reason) { void _showWarningBanner(String message) { final warningMsg = document.getElementById('warningMsg'); warningMsg?.setInnerHtml(message); - _updateElementVisibility('warningBanner', visible: true); + print(warningMsg); + final warningBanner = document.getElementById('warningBanner'); + warningBanner?.classes.add('show'); +} + +void _hideWarningBanner() { + final warningBanner = document.getElementById('warningBanner'); + warningBanner?.classes.remove('show'); } void _launchDebugConnection(Event _) async { @@ -161,8 +170,8 @@ void _injectDevToolsIframe(String devToolsUrl) { final iframe = document.createElement('iframe'); iframe.setAttribute('src', '$devToolsUrl&embed=true&page=debugger'); iframe.setAttribute('id', 'dartDebugExtensionIframe'); + _hideWarningBanner(); _updateElementVisibility('landingPage', visible: false); - _updateElementVisibility('lostConnectionSnackbar', visible: false); _updateElementVisibility('loadingSpinner', visible: false); _updateElementVisibility('launchDebuggerButton', visible: true); iframeContainer.append(iframe); @@ -173,7 +182,7 @@ void _removeDevToolsIframe() { final iframe = iframeContainer?.firstChild; if (iframe == null) return; iframe.remove(); - _updateElementVisibility('lostConnectionSnackbar', visible: true); + _showWarningBanner('Lost connection.'); } void _updateElementVisibility(String elementId, {required bool visible}) { diff --git a/dwds/debug_extension_mv3/web/static_assets/debugger_panel.html b/dwds/debug_extension_mv3/web/static_assets/debugger_panel.html index 2aec181b6..872d43ec8 100644 --- a/dwds/debug_extension_mv3/web/static_assets/debugger_panel.html +++ b/dwds/debug_extension_mv3/web/static_assets/debugger_panel.html @@ -44,7 +44,10 @@ > Launch Debugger - + @@ -52,8 +55,15 @@
-