From c6c50cfe04d52b5fb3f2cb3695dcc2fc7c7015e7 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Mon, 28 Nov 2022 14:58:38 -0800 Subject: [PATCH 1/9] Pull out debug logging into one file --- dwds/debug_extension_mv3/web/background.dart | 4 +- dwds/debug_extension_mv3/web/detector.dart | 4 +- .../web/lifeline_connection.dart | 8 ++-- .../web/lifeline_ports.dart | 40 ++++++---------- dwds/debug_extension_mv3/web/logger.dart | 47 +++++++++++++++++++ dwds/debug_extension_mv3/web/messaging.dart | 4 +- dwds/debug_extension_mv3/web/storage.dart | 23 ++------- dwds/debug_extension_mv3/web/web_api.dart | 3 ++ 8 files changed, 80 insertions(+), 53 deletions(-) create mode 100644 dwds/debug_extension_mv3/web/logger.dart diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart index 9de0c0f60..f1b853444 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 'chrome_api.dart'; import 'data_types.dart'; import 'lifeline_ports.dart'; +import 'logger.dart'; import 'messaging.dart'; import 'storage.dart'; import 'web_api.dart'; @@ -68,6 +69,7 @@ Future _authenticateUser(String extensionUrl, int tabId) async { final response = await fetchRequest(authUrl); final responseBody = response.body ?? ''; if (!responseBody.contains(_authSuccessResponse)) { + debugWarn('Not authenticated: ${response.status} / $responseBody'); _showWarningNotification('Please re-authenticate and try again.'); await _createTab(authUrl, inNewWindow: false); return false; @@ -98,7 +100,7 @@ void _handleRuntimeMessages( messageHandler: (DebugInfo debugInfo) async { final dartTab = sender.tab; if (dartTab == null) { - console.warn('Received debug info but tab is missing.'); + debugWarn('Received debug info but tab is missing.'); return; } // Save the debug info for the Dart app in storage: diff --git a/dwds/debug_extension_mv3/web/detector.dart b/dwds/debug_extension_mv3/web/detector.dart index ca4be0c56..35bf64bc6 100644 --- a/dwds/debug_extension_mv3/web/detector.dart +++ b/dwds/debug_extension_mv3/web/detector.dart @@ -10,8 +10,8 @@ import 'dart:js_util'; import 'package:js/js.dart'; import 'chrome_api.dart'; +import 'logger.dart'; import 'messaging.dart'; -import 'web_api.dart'; void main() { _registerListeners(); @@ -24,7 +24,7 @@ void _registerListeners() { void _onDartAppReadyEvent(Event event) { final debugInfo = getProperty(event, 'detail') as String?; if (debugInfo == null) { - console.warn('Can\'t debug Dart app without debug info.'); + debugWarn('Can\'t debug Dart app without debug info.'); return; } _sendMessageToBackgroundScript( diff --git a/dwds/debug_extension_mv3/web/lifeline_connection.dart b/dwds/debug_extension_mv3/web/lifeline_connection.dart index ec32786b9..3096a9566 100644 --- a/dwds/debug_extension_mv3/web/lifeline_connection.dart +++ b/dwds/debug_extension_mv3/web/lifeline_connection.dart @@ -3,15 +3,17 @@ // BSD-style license that can be found in the LICENSE file. import 'chrome_api.dart'; -import 'web_api.dart'; +import 'logger.dart'; void main() async { _connectToLifelinePort(); } void _connectToLifelinePort() { - console.log( - '[Dart Debug Extension] Connecting to lifeline port at ${_currentTime()}.'); + debugLog( + 'Connecting to lifeline port at ${_currentTime()}.', + prefix: 'Dart Debug Extension', + ); chrome.runtime.connect( /*extensionId=*/ null, ConnectInfo(name: 'keepAlive'), diff --git a/dwds/debug_extension_mv3/web/lifeline_ports.dart b/dwds/debug_extension_mv3/web/lifeline_ports.dart index a465cebbd..23f57ed62 100644 --- a/dwds/debug_extension_mv3/web/lifeline_ports.dart +++ b/dwds/debug_extension_mv3/web/lifeline_ports.dart @@ -12,7 +12,7 @@ import 'dart:async'; import 'package:js/js.dart'; import 'chrome_api.dart'; -import 'web_api.dart'; +import 'logger.dart'; // Switch to true to enable debug logs. // TODO(elliette): Enable / disable with flag while building the extension. @@ -26,18 +26,18 @@ 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'); + 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.'); + 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.'); + debugLog('Creating lifeline port.'); lifelineTab = tabId; chrome.scripting.executeScript( InjectDetails( @@ -52,17 +52,17 @@ 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.'); + 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.'); + debugLog('No more Dart tabs, disconnecting from lifeline port.'); _disconnectFromLifelinePort(); } else { lifelineTab = dartTabs.last; - _debugLog('Reconnecting lifeline port to a new Dart tab: $lifelineTab.'); + debugLog('Reconnecting lifeline port to a new Dart tab: $lifelineTab.'); _reconnectToLifelinePort(); } } @@ -75,45 +75,33 @@ void _keepLifelinePortAlive(Port 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.'); + debugLog('5 minutes have elapsed, therefore reconnecting.'); _reconnectToLifelinePort(); }); } void _reconnectToLifelinePort() { - _debugLog('Reconnecting...'); + debugLog('Reconnecting...'); if (lifelinePort == null) { - _debugWarn('Could not find a lifeline port.'); + debugWarn('Could not find a lifeline port.'); return; } if (lifelineTab == null) { - _debugWarn('Could not find a lifeline tab.'); + 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.'); + debugLog('Reconnection complete.'); } void _disconnectFromLifelinePort() { - _debugLog('Disconnecting...'); + 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); + debugLog('Disconnection complete.'); } } diff --git a/dwds/debug_extension_mv3/web/logger.dart b/dwds/debug_extension_mv3/web/logger.dart new file mode 100644 index 000000000..a2dddb65d --- /dev/null +++ b/dwds/debug_extension_mv3/web/logger.dart @@ -0,0 +1,47 @@ +// 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 logger; + +import 'package:js/js.dart'; + +import 'web_api.dart'; + +// Switch to true for debug logging. +bool _enableDebugLogging = true; + +enum _LogLevel { + info, + warn, + error, +} + +debugLog(String msg, {String? prefix}) { + _log(msg, prefix: prefix); +} + +debugWarn(String msg, {String? prefix}) { + _log(msg, prefix: prefix, level: _LogLevel.warn); +} + +debugError(String msg, {String? prefix}) { + _log(msg, prefix: prefix, level: _LogLevel.error); +} + +void _log(String msg, {_LogLevel? level, String? prefix}) { + if (!_enableDebugLogging) return; + final logMsg = prefix != null ? '[$prefix] $msg' : msg; + final logLevel = level ?? _LogLevel.info; + switch (logLevel) { + case _LogLevel.error: + console.error(logMsg); + break; + case _LogLevel.warn: + console.warn(logMsg); + break; + case _LogLevel.info: + console.log(logMsg); + } +} diff --git a/dwds/debug_extension_mv3/web/messaging.dart b/dwds/debug_extension_mv3/web/messaging.dart index 4f4c1745b..8041e4ade 100644 --- a/dwds/debug_extension_mv3/web/messaging.dart +++ b/dwds/debug_extension_mv3/web/messaging.dart @@ -10,7 +10,7 @@ import 'dart:convert'; import 'package:dwds/data/serializers.dart'; import 'package:js/js.dart'; -import 'web_api.dart'; +import 'logger.dart'; enum Script { background, @@ -85,7 +85,7 @@ void interceptMessage({ messageHandler( serializers.deserialize(jsonDecode(decodedMessage.body)) as T); } catch (error) { - console.warn( + debugError( 'Error intercepting $expectedType from $expectedSender to $expectedRecipient: $error'); } } diff --git a/dwds/debug_extension_mv3/web/storage.dart b/dwds/debug_extension_mv3/web/storage.dart index a989e6310..dc4328ced 100644 --- a/dwds/debug_extension_mv3/web/storage.dart +++ b/dwds/debug_extension_mv3/web/storage.dart @@ -13,10 +13,7 @@ import 'package:js/js.dart'; import 'chrome_api.dart'; import 'data_serializers.dart'; -import 'web_api.dart'; - -/// Switch to true for debug logging. -bool enableDebugLogging = true; +import 'logger.dart'; enum StorageObject { debugInfo, @@ -46,7 +43,7 @@ Future setStorageObject({ if (callback != null) { callback(); } - _debugLog(storageKey, 'Set: $json'); + debugLog('Set: $json', prefix: storageKey); completer.complete(true); })); return completer.future; @@ -58,11 +55,11 @@ Future fetchStorageObject({required StorageObject type, int? tabId}) { chrome.storage.local.get([storageKey], allowInterop((Object storageObj) { final json = getProperty(storageObj, storageKey) as String?; if (json == null) { - _debugWarn(storageKey, 'Does not exist.'); + debugWarn('Does not exist.', prefix: storageKey); completer.complete(null); } else { final value = serializers.deserialize(jsonDecode(json)) as T; - _debugLog(storageKey, 'Fetched: $json'); + debugLog('Fetched: $json', prefix: storageKey); completer.complete(value); } })); @@ -73,15 +70,3 @@ String _createStorageKey(StorageObject type, int? tabId) { if (tabId == null) return type.keyName; return '$tabId-${type.keyName}'; } - -void _debugLog(String storageKey, String msg) { - if (enableDebugLogging) { - console.log('[$storageKey] $msg'); - } -} - -void _debugWarn(String storageKey, String msg) { - if (enableDebugLogging) { - console.warn('[$storageKey] $msg'); - } -} diff --git a/dwds/debug_extension_mv3/web/web_api.dart b/dwds/debug_extension_mv3/web/web_api.dart index 6679af033..9bc8c3381 100644 --- a/dwds/debug_extension_mv3/web/web_api.dart +++ b/dwds/debug_extension_mv3/web/web_api.dart @@ -17,6 +17,9 @@ class Console { external void warn(String header, [String style1, String style2, String style3]); + + external void error(String header, + [String style1, String style2, String style3]); } // Custom implementation of Fetch API until the Dart implementation supports From 80427381b453b6cf6705707573e2cd6b132c2abc Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Mon, 28 Nov 2022 15:10:49 -0800 Subject: [PATCH 2/9] Getting back a DevTools URL --- dwds/debug_extension_mv3/pubspec.yaml | 4 + dwds/debug_extension_mv3/web/background.dart | 9 +- dwds/debug_extension_mv3/web/chrome_api.dart | 44 +++ .../web/data_serializers.dart | 8 + .../web/data_serializers.g.dart | 10 +- .../web/debug_session.dart | 309 ++++++++++++++++++ dwds/debug_extension_mv3/web/web_api.dart | 11 + 7 files changed, 389 insertions(+), 6 deletions(-) create mode 100644 dwds/debug_extension_mv3/web/debug_session.dart diff --git a/dwds/debug_extension_mv3/pubspec.yaml b/dwds/debug_extension_mv3/pubspec.yaml index 2e13fcba1..bb54e5596 100644 --- a/dwds/debug_extension_mv3/pubspec.yaml +++ b/dwds/debug_extension_mv3/pubspec.yaml @@ -10,11 +10,13 @@ environment: dependencies: built_value: ^8.3.0 + collection: ^1.15.0 js: ^0.6.1+1 dev_dependencies: build: ^2.0.0 build_runner: ^2.0.6 + built_collection: ^5.0.0 built_value_generator: ^8.3.0 build_web_compilers: ^3.0.0 dwds: ^16.0.0 @@ -22,3 +24,5 @@ dev_dependencies: dependency_overrides: dwds: path: .. + sse: + path: /Users/elliottbrooks/dev/sse diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart index f1b853444..92d19c69a 100644 --- a/dwds/debug_extension_mv3/web/background.dart +++ b/dwds/debug_extension_mv3/web/background.dart @@ -12,8 +12,9 @@ import 'package:dwds/data/debug_info.dart'; import 'package:dwds/data/extension_request.dart'; import 'package:js/js.dart'; -import 'chrome_api.dart'; import 'data_types.dart'; +import 'debug_session.dart'; +import 'chrome_api.dart'; import 'lifeline_ports.dart'; import 'logger.dart'; import 'messaging.dart'; @@ -58,10 +59,8 @@ Future _startDebugSession(Tab currentTab) async { if (!isAuthenticated) return; maybeCreateLifelinePort(currentTab.id); - final devToolsOpener = await fetchStorageObject( - type: StorageObject.devToolsOpener); - await _createTab('https://dart.dev/', - inNewWindow: devToolsOpener?.newWindow ?? false); + registerDebugEventListeners(); + attachDebugger(tabId); } Future _authenticateUser(String extensionUrl, int tabId) async { diff --git a/dwds/debug_extension_mv3/web/chrome_api.dart b/dwds/debug_extension_mv3/web/chrome_api.dart index 14d4c368f..4f61c5ee5 100644 --- a/dwds/debug_extension_mv3/web/chrome_api.dart +++ b/dwds/debug_extension_mv3/web/chrome_api.dart @@ -11,6 +11,7 @@ external Chrome get chrome; @anonymous class Chrome { external Action get action; + external Debugger get debugger; external Notifications get notifications; external Runtime get runtime; external Scripting get scripting; @@ -43,6 +44,39 @@ class IconInfo { external factory IconInfo({String path}); } +/// chrome.debugger APIs: +/// https://developer.chrome.com/docs/extensions/reference/debugger + +@JS() +@anonymous +class Debugger { + external void attach( + Debuggee target, String requiredVersion, Function? callback); + + external void detach(Debuggee target, Function? callback); + + external void sendCommand(Debuggee target, String method, + Object? commandParams, Function? callback); + + external OnEventHandler get onEvent; +} + +@JS() +@anonymous +class OnEventHandler { + external void addListener( + void Function(Debuggee source, String method, Object? params) callback); +} + +@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}); +} + /// chrome.notification APIs: /// https://developer.chrome.com/docs/extensions/reference/notifications @@ -75,11 +109,19 @@ class Runtime { external void sendMessage( String? id, Object? message, Object? options, Function? callback); + // Note: Not checking the lastError when one occurs throws a runtime exception. + external ChromeError? get lastError; + external ConnectionHandler get onConnect; external OnMessageHandler get onMessage; } +@JS() +class ChromeError { + external String get message; +} + @JS() @anonymous class ConnectInfo { @@ -177,6 +219,8 @@ class Tabs { external Object create(TabInfo tabInfo); + external Object get(int tabId); + external OnActivatedHandler get onActivated; external OnRemovedHandler get onRemoved; diff --git a/dwds/debug_extension_mv3/web/data_serializers.dart b/dwds/debug_extension_mv3/web/data_serializers.dart index 20552e636..1cb6268ae 100644 --- a/dwds/debug_extension_mv3/web/data_serializers.dart +++ b/dwds/debug_extension_mv3/web/data_serializers.dart @@ -2,8 +2,11 @@ // 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 'package:built_collection/built_collection.dart'; import 'package:built_value/serializer.dart'; import 'package:dwds/data/debug_info.dart'; +import 'package:dwds/data/devtools_request.dart'; +import 'package:dwds/data/extension_request.dart'; import 'data_types.dart'; @@ -11,7 +14,12 @@ part 'data_serializers.g.dart'; /// Serializers for all the data types used in the Dart Debug Extension. @SerializersFor([ + BatchedEvents, DebugInfo, DevToolsOpener, + DevToolsRequest, + ExtensionEvent, + ExtensionRequest, + ExtensionResponse, ]) final Serializers serializers = _$serializers; diff --git a/dwds/debug_extension_mv3/web/data_serializers.g.dart b/dwds/debug_extension_mv3/web/data_serializers.g.dart index 2d11b95a2..5dc6bb9b1 100644 --- a/dwds/debug_extension_mv3/web/data_serializers.g.dart +++ b/dwds/debug_extension_mv3/web/data_serializers.g.dart @@ -7,8 +7,16 @@ part of 'data_serializers.dart'; // ************************************************************************** Serializers _$serializers = (new Serializers().toBuilder() + ..add(BatchedEvents.serializer) ..add(DebugInfo.serializer) - ..add(DevToolsOpener.serializer)) + ..add(DevToolsOpener.serializer) + ..add(DevToolsRequest.serializer) + ..add(ExtensionEvent.serializer) + ..add(ExtensionRequest.serializer) + ..add(ExtensionResponse.serializer) + ..addBuilderFactory( + const FullType(BuiltList, const [const FullType(ExtensionEvent)]), + () => new ListBuilder())) .build(); // 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 new file mode 100644 index 000000000..a85990917 --- /dev/null +++ b/dwds/debug_extension_mv3/web/debug_session.dart @@ -0,0 +1,309 @@ +// 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 debugging; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:html'; +import 'dart:js_util'; + +import 'package:built_collection/built_collection.dart'; +import 'package:collection/collection.dart' show IterableExtension; +import 'package:dwds/data/debug_info.dart'; +import 'package:dwds/data/devtools_request.dart'; +import 'package:dwds/data/extension_request.dart'; +import 'package:dwds/src/sockets.dart'; +// TODO(https://github.com/dart-lang/sdk/issues/49973): Use conditional imports +// in .../utilities/batched_stream so that we don't need to import a copy. +import 'package:dwds/src/web_utilities/batched_stream.dart'; +import 'package:js/js.dart'; +import 'package:js/js_util.dart' as js_util; +import 'package:sse/client/sse_client.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; +import 'chrome_api.dart'; +import 'data_serializers.dart'; +import 'logger.dart'; +import 'storage.dart'; +import 'web_api.dart'; + +bool _enableDebugLogging = const bool.fromEnvironment("is_dev"); + +const _notADartAppAlert = 'No Dart application detected.' + ' Are you trying to debug an application that includes a Chrome hosted app' + ' (an application listed in chrome://apps)? If so, debugging is disabled.' + ' You can fix this by removing the application from chrome://apps. Please' + ' see https://bugs.chromium.org/p/chromium/issues/detail?id=885025#c11.'; + +const _devToolsAlreadyOpenedAlert = + 'DevTools is already opened on a different window.'; + +final _debugSessions = <_DebugSession>[]; + +void registerDebugEventListeners() { + chrome.debugger.onEvent.addListener(allowInterop(_onDebuggerEvent)); +} + +void attachDebugger(int tabId) { + chrome.debugger.attach( + Debuggee(tabId: tabId), + '1.3', + allowInterop( + () => _enableExecutionContextReporting(tabId), + ), + ); +} + +_enableExecutionContextReporting(int tabId) { + // Runtime.enable enables reporting of execution contexts creation by means of + // executionContextCreated event. When the reporting gets enabled the event + // will be sent immediately for each existing execution context: + chrome.debugger.sendCommand( + Debuggee(tabId: tabId), 'Runtime.enable', EmptyParam(), allowInterop((_) { + final chromeError = chrome.runtime.lastError; + if (chromeError != null) { + final errorMessage = _translateChromeError(chromeError.message); + chrome.notifications.create(/*notificationId*/ null, + NotificationOptions(message: errorMessage), /*callback*/ null); + return; + } + })); +} + +String _translateChromeError(String chromeErrorMessage) { + if (chromeErrorMessage.contains('Cannot access') || + chromeErrorMessage.contains('Cannot attach')) { + return _notADartAppAlert; + } + return _devToolsAlreadyOpenedAlert; +} + +Future _onDebuggerEvent( + Debuggee source, String method, Object? params) async { + if (method == 'Runtime.executionContextCreated') { + return _maybeConnectToDwds(source.tabId, params); + } + + return _forwardChromeDebuggerEventToDwds(source, method, params); +} + +Future _maybeConnectToDwds(int tabId, Object? params) async { + final context = json.decode(JSON.stringify(params))['context']; + final contextOrigin = context['origin'] as String?; + if (contextOrigin == null) return; + if (contextOrigin.contains(('chrome-extension:'))) return; + final debugInfo = await fetchStorageObject( + type: StorageObject.debugInfo, + tabId: tabId, + ); + if (debugInfo == null) return; + if (contextOrigin != debugInfo.appOrigin) return; + final contextId = context['id'] as int; + final connected = await _connectToDwds( + dartAppContextId: contextId, + dartAppTabId: tabId, + debugInfo: debugInfo, + ); + if (!connected) { + debugWarn('Failed to connect to DWDS for $contextOrigin.'); + } +} + +Future _connectToDwds({ + required int dartAppContextId, + required int dartAppTabId, + required DebugInfo debugInfo, +}) async { + if (debugInfo.extensionUrl == null) { + debugWarn('Can\'t connect to DWDS without an extension URL.'); + return false; + } + final uri = Uri.parse(debugInfo.extensionUrl!); + // Start the client connection with DWDS: + final client = uri.isScheme('ws') || uri.isScheme('wss') + ? WebSocketClient(WebSocketChannel.connect(uri)) + : SseSocketClient(SseClient(uri.toString())); + final debugSession = _DebugSession(client: client, appTabId: dartAppTabId); + _debugSessions.add(debugSession); + client.stream.listen( + (data) => _routeDwdsEvent(data, client, dartAppTabId), + onDone: () { + debugLog('Shutting down DWDS communication channel.'); + _removeAndDetachDebugSessionForTab(dartAppTabId); + }, + onError: (err) { + debugWarn('Error in DWDS communication channel: $err'); + _removeAndDetachDebugSessionForTab(dartAppTabId); + }, + cancelOnError: true, + ); + final tabUrl = await _getTabUrl(dartAppTabId); + // Send a DevtoolsRequest to the event stream: + final event = jsonEncode(serializers.serialize(DevToolsRequest((b) => b + ..appId = debugInfo.appId + ..instanceId = debugInfo.appInstanceId + ..contextId = dartAppContextId + ..tabUrl = tabUrl + ..uriOnly = true))); + client.sink.add(event); + return true; +} + +void _routeDwdsEvent(String eventData, SocketClient client, int tabId) { + final message = serializers.deserialize(jsonDecode(eventData)); + 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': + debugLog('Received DevTools URL: ${message.params}'); + break; + } + } +} + +void _forwardDwdsEventToChromeDebugger( + ExtensionRequest message, SocketClient client, int tabId) { + final messageParams = message.commandParams ?? '{}'; + final params = BuiltMap(json.decode(messageParams)).toMap(); + chrome.debugger.sendCommand( + Debuggee(tabId: tabId), message.command, js_util.jsify(params), + allowInterop(([e]) { + // No arguments indicate that an error occurred. + if (e == null) { + client.sink + .add(jsonEncode(serializers.serialize(ExtensionResponse((b) => b + ..id = message.id + ..success = false + ..result = JSON.stringify(chrome.runtime.lastError))))); + } else { + client.sink + .add(jsonEncode(serializers.serialize(ExtensionResponse((b) => b + ..id = message.id + ..success = true + ..result = JSON.stringify(e))))); + } + })); +} + +void _forwardChromeDebuggerEventToDwds( + Debuggee source, String method, dynamic params) { + final debugSession = _debugSessions + .firstWhereOrNull((session) => session.appTabId == source.tabId); + + if (debugSession == null) return; + + final event = _extensionEventFor(method, params); + + if (method == 'Debugger.scriptParsed') { + debugSession.sendBatchedEvent(event); + } else { + debugSession.sendEvent(event); + } +} + +// Tries to remove the debug session for the specified tab, and detach the +// debugger associated with that debug session. +void _removeAndDetachDebugSessionForTab(int tabId) { + final removedTabId = _removeDebugSessionForTab(tabId); + + if (removedTabId != -1) { + chrome.debugger.detach(Debuggee(tabId: removedTabId), allowInterop(() {})); + } +} + +// Tries to remove the debug session for the specified tab. If no session is +// found, returns -1. Otherwise returns the tab ID. +int _removeDebugSessionForTab(int tabId) { + final session = + _debugSessions.firstWhereOrNull((session) => session.appTabId == tabId); + if (session != null) { + // Note: package:sse will try to keep the connection alive, even after the + // client has been closed. Therefore the extension sends an event to notify + // DWDS that we should close the connection, instead of relying on the done + // event sent when the client is closed. See details: + // https://github.com/dart-lang/webdev/pull/1595#issuecomment-1116773378 + final event = + _extensionEventFor('DebugExtension.detached', js_util.jsify({})); + session.sendEvent(event); + session.close(); + _debugSessions.remove(session); + return session.appTabId; + } else { + return -1; + } +} + +/// Construct an [ExtensionEvent] from [method] and [params]. +ExtensionEvent _extensionEventFor(String method, dynamic params) { + return ExtensionEvent((b) => b + ..params = jsonEncode(json.decode(JSON.stringify(params))) + ..method = jsonEncode(method)); +} + +Future _getTabUrl(int tabId) async { + final tab = await promiseToFuture(chrome.tabs.get(tabId)); + return tab?.url ?? ''; +} + +@JS() +@anonymous +class EmptyParam { + external factory EmptyParam(); +} + +class _DebugSession { + // The tab ID that contains the running Dart application. + final int appTabId; + + // Socket client for communication with dwds extension backend. + late final SocketClient _socketClient; + + // How often to send batched events. + static const int _batchDelayMilliseconds = 1000; + + // Collect events into batches to be send periodically to the server. + final _batchController = + BatchedStreamController(delay: _batchDelayMilliseconds); + late final StreamSubscription> _batchSubscription; + + _DebugSession({ + required client, + required this.appTabId, + }) : _socketClient = client { + // Collect extension events and send them periodically to the server. + _batchSubscription = _batchController.stream.listen((events) { + _socketClient.sink.add(jsonEncode(serializers.serialize(BatchedEvents( + (b) => b.events = ListBuilder(events))))); + }); + } + + void set socketClient(SocketClient client) { + _socketClient = client; + + // Collect extension events and send them periodically to the server. + _batchSubscription = _batchController.stream.listen((events) { + _socketClient.sink.add(jsonEncode(serializers.serialize(BatchedEvents( + (b) => b.events = ListBuilder(events))))); + }); + } + + void sendEvent(ExtensionEvent event) { + _socketClient.sink.add(jsonEncode(serializers.serialize(event))); + } + + void sendBatchedEvent(ExtensionEvent event) { + _batchController.sink.add(event); + } + + void close() { + _socketClient.close(); + _batchSubscription.cancel(); + _batchController.close(); + } +} diff --git a/dwds/debug_extension_mv3/web/web_api.dart b/dwds/debug_extension_mv3/web/web_api.dart index 9bc8c3381..b91e80c4f 100644 --- a/dwds/debug_extension_mv3/web/web_api.dart +++ b/dwds/debug_extension_mv3/web/web_api.dart @@ -9,6 +9,10 @@ import 'dart:js_util' as js_util; @JS() external Console get console; +@JS() +// ignore: non_constant_identifier_names +external Json get JSON; + @JS() @anonymous class Console { @@ -22,6 +26,13 @@ class Console { [String style1, String style2, String style3]); } +@JS() +@anonymous +class Json { +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify + external String stringify(o); +} + // Custom implementation of Fetch API until the Dart implementation supports // credentials. See https://github.com/dart-lang/http/issues/595. @JS('fetch') From 9b57a3b4b0217102f8f5ab432cd7bf3463ee8f7c Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Mon, 28 Nov 2022 15:27:25 -0800 Subject: [PATCH 3/9] Can open DevTools --- dwds/debug_extension_mv3/web/background.dart | 4 +-- .../web/debug_session.dart | 16 ++++++++-- dwds/debug_extension_mv3/web/tabs.dart | 29 +++++++++++++++++++ 3 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 dwds/debug_extension_mv3/web/tabs.dart diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart index 92d19c69a..53caca037 100644 --- a/dwds/debug_extension_mv3/web/background.dart +++ b/dwds/debug_extension_mv3/web/background.dart @@ -12,13 +12,13 @@ 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'; import 'logger.dart'; import 'messaging.dart'; import 'storage.dart'; +import 'tabs.dart'; import 'web_api.dart'; const _authSuccessResponse = 'Dart Debug Authentication Success!'; @@ -70,7 +70,7 @@ Future _authenticateUser(String extensionUrl, int tabId) async { if (!responseBody.contains(_authSuccessResponse)) { debugWarn('Not authenticated: ${response.status} / $responseBody'); _showWarningNotification('Please re-authenticate and try again.'); - await _createTab(authUrl, inNewWindow: false); + await createTab(authUrl, inNewWindow: false); return false; } return true; diff --git a/dwds/debug_extension_mv3/web/debug_session.dart b/dwds/debug_extension_mv3/web/debug_session.dart index a85990917..b90a71870 100644 --- a/dwds/debug_extension_mv3/web/debug_session.dart +++ b/dwds/debug_extension_mv3/web/debug_session.dart @@ -23,14 +23,14 @@ import 'package:js/js.dart'; import 'package:js/js_util.dart' as js_util; import 'package:sse/client/sse_client.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; +import 'data_types.dart'; import 'chrome_api.dart'; import 'data_serializers.dart'; import 'logger.dart'; import 'storage.dart'; +import 'tabs.dart'; import 'web_api.dart'; -bool _enableDebugLogging = const bool.fromEnvironment("is_dev"); - const _notADartAppAlert = 'No Dart application detected.' ' Are you trying to debug an application that includes a Chrome hosted app' ' (an application listed in chrome://apps)? If so, debugging is disabled.' @@ -161,7 +161,7 @@ void _routeDwdsEvent(String eventData, SocketClient client, int tabId) { // TODO(elliette): Forward to external extensions. break; case 'dwds.devtoolsUri': - debugLog('Received DevTools URL: ${message.params}'); + _openDevTools(message.params); break; } } @@ -239,6 +239,16 @@ int _removeDebugSessionForTab(int tabId) { } } +void _openDevTools(String devToolsUrl) async { + if (devToolsUrl.isEmpty) { + debugError('DevTools URL is empty.'); + return; + } + final devToolsOpener = await fetchStorageObject( + type: StorageObject.devToolsOpener); + await createTab(devToolsUrl, inNewWindow: devToolsOpener?.newWindow ?? false); +} + /// Construct an [ExtensionEvent] from [method] and [params]. ExtensionEvent _extensionEventFor(String method, dynamic params) { return ExtensionEvent((b) => b diff --git a/dwds/debug_extension_mv3/web/tabs.dart b/dwds/debug_extension_mv3/web/tabs.dart new file mode 100644 index 000000000..4883f5f50 --- /dev/null +++ b/dwds/debug_extension_mv3/web/tabs.dart @@ -0,0 +1,29 @@ +// 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 tabs; + +import 'dart:async'; +import 'dart:html'; + +import 'package:js/js.dart'; + +import 'chrome_api.dart'; + + +Future createTab(String url, {bool inNewWindow = false}) async { + if (inNewWindow) { + final windowPromise = chrome.windows.create( + WindowInfo(focused: true, url: url), + ); + final windowObj = await promiseToFuture(windowPromise); + return windowObj.tabs.first; + } + final tabPromise = chrome.tabs.create(TabInfo( + active: true, + url: url, + )); + return promiseToFuture(tabPromise); +} \ No newline at end of file From ddf9c34d115ab0b46510f920eeb235ceee11c968 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Mon, 28 Nov 2022 17:09:49 -0800 Subject: [PATCH 4/9] Can detach debugger / close DevTools --- dwds/debug_extension_mv3/web/background.dart | 15 -- dwds/debug_extension_mv3/web/chrome_api.dart | 11 ++ .../web/debug_session.dart | 132 ++++++++++++------ dwds/debug_extension_mv3/web/tabs.dart | 13 +- 4 files changed, 111 insertions(+), 60 deletions(-) diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart index 53caca037..53ace219f 100644 --- a/dwds/debug_extension_mv3/web/background.dart +++ b/dwds/debug_extension_mv3/web/background.dart @@ -155,18 +155,3 @@ Future _getTab() async { final tabs = List.from(await promiseToFuture(chrome.tabs.query(query))); return tabs.isNotEmpty ? tabs.first : null; } - -Future _createTab(String url, {bool inNewWindow = false}) async { - if (inNewWindow) { - final windowPromise = chrome.windows.create( - WindowInfo(focused: true, url: url), - ); - final windowObj = await promiseToFuture(windowPromise); - return windowObj.tabs.first; - } - final tabPromise = chrome.tabs.create(TabInfo( - active: true, - url: url, - )); - return promiseToFuture(tabPromise); -} diff --git a/dwds/debug_extension_mv3/web/chrome_api.dart b/dwds/debug_extension_mv3/web/chrome_api.dart index 4f61c5ee5..ee115777e 100644 --- a/dwds/debug_extension_mv3/web/chrome_api.dart +++ b/dwds/debug_extension_mv3/web/chrome_api.dart @@ -58,9 +58,18 @@ class Debugger { external void sendCommand(Debuggee target, String method, Object? commandParams, Function? callback); + external OnDetachHandler get onDetach; + external OnEventHandler get onEvent; } +@JS() +@anonymous +class OnDetachHandler { + external void addListener( + void Function(Debuggee source, String reason) callback); +} + @JS() @anonymous class OnEventHandler { @@ -221,6 +230,8 @@ class Tabs { external Object get(int tabId); + external Object remove(int tabId); + external OnActivatedHandler get onActivated; external OnRemovedHandler get onRemoved; diff --git a/dwds/debug_extension_mv3/web/debug_session.dart b/dwds/debug_extension_mv3/web/debug_session.dart index b90a71870..f2642f238 100644 --- a/dwds/debug_extension_mv3/web/debug_session.dart +++ b/dwds/debug_extension_mv3/web/debug_session.dart @@ -3,12 +3,10 @@ // BSD-style license that can be found in the LICENSE file. @JS() -library debugging; +library debug_session; import 'dart:async'; import 'dart:convert'; -import 'dart:html'; -import 'dart:js_util'; import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableExtension; @@ -23,9 +21,10 @@ import 'package:js/js.dart'; import 'package:js/js_util.dart' as js_util; import 'package:sse/client/sse_client.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; -import 'data_types.dart'; + import 'chrome_api.dart'; import 'data_serializers.dart'; +import 'data_types.dart'; import 'logger.dart'; import 'storage.dart'; import 'tabs.dart'; @@ -42,8 +41,17 @@ const _devToolsAlreadyOpenedAlert = final _debugSessions = <_DebugSession>[]; +enum TabType { + dartApp, + devTools, +} + void registerDebugEventListeners() { chrome.debugger.onEvent.addListener(allowInterop(_onDebuggerEvent)); + chrome.debugger.onDetach.addListener(allowInterop(_handleDebuggerDetach)); + chrome.tabs.onRemoved.addListener(allowInterop( + (tabId, _) => _detachDebuggerForTab(tabId, type: TabType.devTools), + )); } void attachDebugger(int tabId) { @@ -131,11 +139,11 @@ Future _connectToDwds({ (data) => _routeDwdsEvent(data, client, dartAppTabId), onDone: () { debugLog('Shutting down DWDS communication channel.'); - _removeAndDetachDebugSessionForTab(dartAppTabId); + _detachDebuggerForTab(dartAppTabId, type: TabType.dartApp); }, onError: (err) { debugWarn('Error in DWDS communication channel: $err'); - _removeAndDetachDebugSessionForTab(dartAppTabId); + _detachDebuggerForTab(dartAppTabId, type: TabType.dartApp); }, cancelOnError: true, ); @@ -161,7 +169,7 @@ void _routeDwdsEvent(String eventData, SocketClient client, int tabId) { // TODO(elliette): Forward to external extensions. break; case 'dwds.devtoolsUri': - _openDevTools(message.params); + _openDevTools(message.params, dartTabId: tabId); break; } } @@ -195,11 +203,8 @@ void _forwardChromeDebuggerEventToDwds( Debuggee source, String method, dynamic params) { final debugSession = _debugSessions .firstWhereOrNull((session) => session.appTabId == source.tabId); - if (debugSession == null) return; - final event = _extensionEventFor(method, params); - if (method == 'Debugger.scriptParsed') { debugSession.sendBatchedEvent(event); } else { @@ -207,46 +212,76 @@ void _forwardChromeDebuggerEventToDwds( } } -// Tries to remove the debug session for the specified tab, and detach the -// debugger associated with that debug session. -void _removeAndDetachDebugSessionForTab(int tabId) { - final removedTabId = _removeDebugSessionForTab(tabId); +void _openDevTools(String devToolsUrl, {required int dartTabId}) async { + if (devToolsUrl.isEmpty) { + debugError('DevTools URL is empty.'); + return; + } + final debugSession = _debugSessionForTab(dartTabId, 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; +} + +void _detachDebuggerForTab(int tabId, {required TabType type}) { + final debugSession = _debugSessionForTab(tabId, type: type); + if (debugSession == null) return; + chrome.debugger.detach(Debuggee(tabId: debugSession.appTabId), + allowInterop(() { + final error = chrome.runtime.lastError; + if (error != null) { + debugWarn('Error detaching tab: ${error.message}'); + } else { + debugLog('Detached debugger for ${debugSession.appTabId}'); + } + })); +} - if (removedTabId != -1) { - chrome.debugger.detach(Debuggee(tabId: removedTabId), allowInterop(() {})); +void _handleDebuggerDetach(Debuggee source, String _) async { + final debugSession = _debugSessionForTab(source.tabId, type: TabType.dartApp); + if (debugSession == null) return; + _removeDebugSession(debugSession); + // Maybe close the associated DevTools tab as well: + final devToolsTabId = debugSession.devToolsTabId; + final devToolsTab = await getTab(devToolsTabId); + if (devToolsTab != null) { + chrome.tabs.remove(devToolsTabId); } } -// Tries to remove the debug session for the specified tab. If no session is -// found, returns -1. Otherwise returns the tab ID. -int _removeDebugSessionForTab(int tabId) { - final session = - _debugSessions.firstWhereOrNull((session) => session.appTabId == tabId); - if (session != null) { - // Note: package:sse will try to keep the connection alive, even after the - // client has been closed. Therefore the extension sends an event to notify - // DWDS that we should close the connection, instead of relying on the done - // event sent when the client is closed. See details: - // https://github.com/dart-lang/webdev/pull/1595#issuecomment-1116773378 - final event = - _extensionEventFor('DebugExtension.detached', js_util.jsify({})); - session.sendEvent(event); - session.close(); - _debugSessions.remove(session); - return session.appTabId; - } else { - return -1; +void _removeDebugSession(_DebugSession debugSession) { + // Note: package:sse will try to keep the connection alive, even after the + // client has been closed. Therefore the extension sends an event to notify + // DWDS that we should close the connection, instead of relying on the done + // event sent when the client is closed. See details: + // https://github.com/dart-lang/webdev/pull/1595#issuecomment-1116773378 + final event = + _extensionEventFor('DebugExtension.detached', js_util.jsify({})); + debugSession.sendEvent(event); + debugSession.close(); + final removed = _debugSessions.remove(debugSession); + if (!removed) { + debugWarn('Could not remove debug session.'); } } -void _openDevTools(String devToolsUrl) async { - if (devToolsUrl.isEmpty) { - debugError('DevTools URL is empty.'); - return; +_DebugSession? _debugSessionForTab(tabId, {required TabType type}) { + switch (type) { + case TabType.dartApp: + return _debugSessions + .firstWhereOrNull((session) => session.appTabId == tabId); + case TabType.devTools: + return _debugSessions + .firstWhereOrNull((session) => session.devToolsTabId == tabId); } - final devToolsOpener = await fetchStorageObject( - type: StorageObject.devToolsOpener); - await createTab(devToolsUrl, inNewWindow: devToolsOpener?.newWindow ?? false); } /// Construct an [ExtensionEvent] from [method] and [params]. @@ -257,7 +292,7 @@ ExtensionEvent _extensionEventFor(String method, dynamic params) { } Future _getTabUrl(int tabId) async { - final tab = await promiseToFuture(chrome.tabs.get(tabId)); + final tab = await getTab(tabId); return tab?.url ?? ''; } @@ -271,6 +306,9 @@ class _DebugSession { // The tab ID that contains the running Dart application. final int appTabId; + // The tab ID that contains the corresponding Dart DevTools. + late final int _devToolsTabId; + // Socket client for communication with dwds extension backend. late final SocketClient _socketClient; @@ -303,6 +341,14 @@ class _DebugSession { }); } + int get devToolsTabId { + return _devToolsTabId; + } + + void set devToolsTabId(int tabId) { + _devToolsTabId = tabId; + } + void sendEvent(ExtensionEvent event) { _socketClient.sink.add(jsonEncode(serializers.serialize(event))); } diff --git a/dwds/debug_extension_mv3/web/tabs.dart b/dwds/debug_extension_mv3/web/tabs.dart index 4883f5f50..a880a7b70 100644 --- a/dwds/debug_extension_mv3/web/tabs.dart +++ b/dwds/debug_extension_mv3/web/tabs.dart @@ -12,7 +12,6 @@ import 'package:js/js.dart'; import 'chrome_api.dart'; - Future createTab(String url, {bool inNewWindow = false}) async { if (inNewWindow) { final windowPromise = chrome.windows.create( @@ -26,4 +25,14 @@ Future createTab(String url, {bool inNewWindow = false}) async { url: url, )); return promiseToFuture(tabPromise); -} \ No newline at end of file +} + +Future getTab(int tabId) { + return promiseToFuture(chrome.tabs.get(tabId)); +} + +Future getActiveTab() async { + final query = QueryInfo(active: true, currentWindow: true); + final tabs = List.from(await promiseToFuture(chrome.tabs.query(query))); + return tabs.isNotEmpty ? tabs.first : null; +} From 6f4682cae0cce6a2434b62e8d24534e4a0909dd7 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Tue, 29 Nov 2022 13:53:23 -0800 Subject: [PATCH 5/9] Rename tabs as utils --- dwds/debug_extension_mv3/web/background.dart | 2 +- dwds/debug_extension_mv3/web/debug_session.dart | 2 +- dwds/debug_extension_mv3/web/{tabs.dart => utils.dart} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename dwds/debug_extension_mv3/web/{tabs.dart => utils.dart} (98%) diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart index 53ace219f..05ecfb738 100644 --- a/dwds/debug_extension_mv3/web/background.dart +++ b/dwds/debug_extension_mv3/web/background.dart @@ -18,7 +18,7 @@ import 'lifeline_ports.dart'; import 'logger.dart'; import 'messaging.dart'; import 'storage.dart'; -import 'tabs.dart'; +import 'utils.dart'; import 'web_api.dart'; const _authSuccessResponse = 'Dart Debug Authentication Success!'; diff --git a/dwds/debug_extension_mv3/web/debug_session.dart b/dwds/debug_extension_mv3/web/debug_session.dart index f2642f238..81441ed6e 100644 --- a/dwds/debug_extension_mv3/web/debug_session.dart +++ b/dwds/debug_extension_mv3/web/debug_session.dart @@ -27,7 +27,7 @@ import 'data_serializers.dart'; import 'data_types.dart'; import 'logger.dart'; import 'storage.dart'; -import 'tabs.dart'; +import 'utils.dart'; import 'web_api.dart'; const _notADartAppAlert = 'No Dart application detected.' diff --git a/dwds/debug_extension_mv3/web/tabs.dart b/dwds/debug_extension_mv3/web/utils.dart similarity index 98% rename from dwds/debug_extension_mv3/web/tabs.dart rename to dwds/debug_extension_mv3/web/utils.dart index a880a7b70..590d2c38e 100644 --- a/dwds/debug_extension_mv3/web/tabs.dart +++ b/dwds/debug_extension_mv3/web/utils.dart @@ -3,7 +3,7 @@ // BSD-style license that can be found in the LICENSE file. @JS() -library tabs; +library utils; import 'dart:async'; import 'dart:html'; From 51caa6ecdad6bdf67d4e3bc082aee8763a98006b Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Tue, 29 Nov 2022 14:23:46 -0800 Subject: [PATCH 6/9] Wip --- dwds/test/fixtures/context.dart | 2 +- dwds/test/puppeteer/extension_test.dart | 12 +++-- dwds/test/puppeteer/test_server.dart | 67 +++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 dwds/test/puppeteer/test_server.dart diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart index 7c0562665..f7e429d58 100644 --- a/dwds/test/fixtures/context.dart +++ b/dwds/test/fixtures/context.dart @@ -167,7 +167,7 @@ class TestContext { try { DartUri.currentDirectory = workingDirectory; - configureLogWriter(); + // configureLogWriter(); _client = IOClient(HttpClient() ..maxConnectionsPerHost = 200 diff --git a/dwds/test/puppeteer/extension_test.dart b/dwds/test/puppeteer/extension_test.dart index ad1920a94..396711872 100644 --- a/dwds/test/puppeteer/extension_test.dart +++ b/dwds/test/puppeteer/extension_test.dart @@ -86,6 +86,7 @@ void main() async { expect(debugInfo.appInstanceId, isNotNull); expect(debugInfo.appOrigin, isNotNull); expect(debugInfo.appUrl, isNotNull); + print('EXTENSION URL ${debugInfo.extensionUrl}'); await appTab.close(); }); @@ -94,9 +95,9 @@ void main() async { () async { final appUrl = context.appUrl; // TODO(elliette): Replace with the DevTools url. - final devToolsUrl = 'https://dart.dev/'; + final devToolsUrlFragment = useSse ? 'uri=sse' : 'uri=ws'; final windowIdForAppJs = _windowIdForTabJs(appUrl); - final windowIdForDevToolsJs = _windowIdForTabJs(devToolsUrl); + // final windowIdForDevToolsJs = _windowIdForTabJs(devToolsUrlFragment); // Navigate to the Dart app: final appTab = await navigateToPage(browser, url: appUrl, isNew: true); @@ -106,7 +107,10 @@ void main() async { await worker.evaluate(clickIconJs); // Verify the extension opened the Dart docs in the same window: var devToolsTabTarget = await browser - .waitForTarget((target) => target.url.contains(devToolsUrl)); + .waitForTarget((target) => target.url.contains(devToolsUrlFragment)); + final devToolsPage = await devToolsTabTarget.page; + print('URL IS: ${devToolsPage.url}'); + final windowIdForDevToolsJs = _windowIdForTabJs(devToolsPage.url!); var devToolsWindowId = (await worker.evaluate(windowIdForDevToolsJs)) as int?; var appWindowId = (await worker.evaluate(windowIdForAppJs)) as int?; @@ -134,7 +138,7 @@ void main() async { await worker.evaluate(clickIconJs); // Verify the extension opened DevTools in a different window: devToolsTabTarget = await browser - .waitForTarget((target) => target.url.contains(devToolsUrl)); + .waitForTarget((target) => target.url.contains(devToolsUrlFragment)); devToolsWindowId = (await worker.evaluate(windowIdForDevToolsJs)) as int?; appWindowId = (await worker.evaluate(windowIdForAppJs)) as int?; diff --git a/dwds/test/puppeteer/test_server.dart b/dwds/test/puppeteer/test_server.dart new file mode 100644 index 000000000..1e0fd2770 --- /dev/null +++ b/dwds/test/puppeteer/test_server.dart @@ -0,0 +1,67 @@ +// 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:io'; + +import 'package:args/args.dart'; +import 'package:path/path.dart' as p; + +import '../fixtures/context.dart'; + +const useSseFlagName = 'use-sse'; + +void main(List arguments) async { + exitCode = 0; // presume success + + // Parse command-line arguments: + final parser = ArgParser() + ..addFlag( + useSseFlagName, + negatable: true, + defaultsTo: false, + abbr: 's', + ); + final argResults = parser.parse(arguments); + final useSse = argResults[useSseFlagName] as bool; + + // Build the Dart Debug Extension: + print('Building Dart Debug Extension...'); + final currentDir = Directory.current; + final extensionDir = absoluteWebdevPath( + currentPath: currentDir.path, + relativeWebdevPath: 'dwds/debug_extension_mv3'); + await Process.run( + 'tool/build_extension.sh', + [], + workingDirectory: extensionDir, + ); + + // Create a test context to launch the test server: + print('Starting a test server...'); + final context = TestContext( + directory: absoluteWebdevPath( + currentPath: currentDir.path, + relativeWebdevPath: 'fixtures/_test', + ), + entry: absoluteWebdevPath( + currentPath: currentDir.path, + relativeWebdevPath: 'fixtures/_test/example/append_body/main.dart', + ), + ); + await context.setUp(serveDevTools: true, useSse: useSse, launchChrome: false, enableDebugExtension: true); + final appUrl = context.appUrl; + + print('============================================================'); + print('APP IS RUNNING AT: $appUrl'); +} + +String absoluteWebdevPath( + {required String currentPath, required String relativeWebdevPath}) { + final currentPathParts = p.split(currentPath); + final pathPartsToWebdev = + currentPathParts.sublist(0, currentPathParts.indexOf('webdev') + 1); + final relativePathParts = p.split(relativeWebdevPath); + final pathToDir = p.joinAll([...pathPartsToWebdev, ...relativePathParts]); + return pathToDir; +} \ No newline at end of file From bf385a2e65d12c6ea96ff1c00a959c41f4b0c3f5 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Tue, 29 Nov 2022 15:24:44 -0800 Subject: [PATCH 7/9] Added test --- dwds/test/fixtures/context.dart | 2 +- dwds/test/puppeteer/extension_test.dart | 14 +++--- dwds/test/puppeteer/test_server.dart | 67 ------------------------- 3 files changed, 8 insertions(+), 75 deletions(-) delete mode 100644 dwds/test/puppeteer/test_server.dart diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart index f7e429d58..7c0562665 100644 --- a/dwds/test/fixtures/context.dart +++ b/dwds/test/fixtures/context.dart @@ -167,7 +167,7 @@ class TestContext { try { DartUri.currentDirectory = workingDirectory; - // configureLogWriter(); + configureLogWriter(); _client = IOClient(HttpClient() ..maxConnectionsPerHost = 200 diff --git a/dwds/test/puppeteer/extension_test.dart b/dwds/test/puppeteer/extension_test.dart index 396711872..4c6654b86 100644 --- a/dwds/test/puppeteer/extension_test.dart +++ b/dwds/test/puppeteer/extension_test.dart @@ -42,6 +42,7 @@ void main() async { // TODO(elliette): Only start a TestServer, that way we can get rid of // the launchChrome parameter: https://github.com/dart-lang/webdev/issues/1779 await context.setUp( + serveDevTools: true, launchChrome: false, useSse: useSse, enableDebugExtension: true, @@ -86,7 +87,6 @@ void main() async { expect(debugInfo.appInstanceId, isNotNull); expect(debugInfo.appOrigin, isNotNull); expect(debugInfo.appUrl, isNotNull); - print('EXTENSION URL ${debugInfo.extensionUrl}'); await appTab.close(); }); @@ -95,7 +95,8 @@ void main() async { () async { final appUrl = context.appUrl; // TODO(elliette): Replace with the DevTools url. - final devToolsUrlFragment = useSse ? 'uri=sse' : 'uri=ws'; + final devToolsUrlFragment = + useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; final windowIdForAppJs = _windowIdForTabJs(appUrl); // final windowIdForDevToolsJs = _windowIdForTabJs(devToolsUrlFragment); // Navigate to the Dart app: @@ -106,10 +107,9 @@ void main() async { await Future.delayed(Duration(seconds: executionContextDelay)); await worker.evaluate(clickIconJs); // Verify the extension opened the Dart docs in the same window: - var devToolsTabTarget = await browser - .waitForTarget((target) => target.url.contains(devToolsUrlFragment)); + var devToolsTabTarget = await browser.waitForTarget( + (target) => target.url.contains(devToolsUrlFragment)); final devToolsPage = await devToolsTabTarget.page; - print('URL IS: ${devToolsPage.url}'); final windowIdForDevToolsJs = _windowIdForTabJs(devToolsPage.url!); var devToolsWindowId = (await worker.evaluate(windowIdForDevToolsJs)) as int?; @@ -137,8 +137,8 @@ void main() async { // Click on the Dart Debug Extension icon: await worker.evaluate(clickIconJs); // Verify the extension opened DevTools in a different window: - devToolsTabTarget = await browser - .waitForTarget((target) => target.url.contains(devToolsUrlFragment)); + devToolsTabTarget = await browser.waitForTarget( + (target) => target.url.contains(devToolsUrlFragment)); devToolsWindowId = (await worker.evaluate(windowIdForDevToolsJs)) as int?; appWindowId = (await worker.evaluate(windowIdForAppJs)) as int?; diff --git a/dwds/test/puppeteer/test_server.dart b/dwds/test/puppeteer/test_server.dart deleted file mode 100644 index 1e0fd2770..000000000 --- a/dwds/test/puppeteer/test_server.dart +++ /dev/null @@ -1,67 +0,0 @@ -// 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:io'; - -import 'package:args/args.dart'; -import 'package:path/path.dart' as p; - -import '../fixtures/context.dart'; - -const useSseFlagName = 'use-sse'; - -void main(List arguments) async { - exitCode = 0; // presume success - - // Parse command-line arguments: - final parser = ArgParser() - ..addFlag( - useSseFlagName, - negatable: true, - defaultsTo: false, - abbr: 's', - ); - final argResults = parser.parse(arguments); - final useSse = argResults[useSseFlagName] as bool; - - // Build the Dart Debug Extension: - print('Building Dart Debug Extension...'); - final currentDir = Directory.current; - final extensionDir = absoluteWebdevPath( - currentPath: currentDir.path, - relativeWebdevPath: 'dwds/debug_extension_mv3'); - await Process.run( - 'tool/build_extension.sh', - [], - workingDirectory: extensionDir, - ); - - // Create a test context to launch the test server: - print('Starting a test server...'); - final context = TestContext( - directory: absoluteWebdevPath( - currentPath: currentDir.path, - relativeWebdevPath: 'fixtures/_test', - ), - entry: absoluteWebdevPath( - currentPath: currentDir.path, - relativeWebdevPath: 'fixtures/_test/example/append_body/main.dart', - ), - ); - await context.setUp(serveDevTools: true, useSse: useSse, launchChrome: false, enableDebugExtension: true); - final appUrl = context.appUrl; - - print('============================================================'); - print('APP IS RUNNING AT: $appUrl'); -} - -String absoluteWebdevPath( - {required String currentPath, required String relativeWebdevPath}) { - final currentPathParts = p.split(currentPath); - final pathPartsToWebdev = - currentPathParts.sublist(0, currentPathParts.indexOf('webdev') + 1); - final relativePathParts = p.split(relativeWebdevPath); - final pathToDir = p.joinAll([...pathPartsToWebdev, ...relativePathParts]); - return pathToDir; -} \ No newline at end of file From e23274efa1a060b3cbb23c1f4b4026adc8fe44e4 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Tue, 29 Nov 2022 15:35:36 -0800 Subject: [PATCH 8/9] Clean up --- dwds/debug_extension_mv3/pubspec.yaml | 1 + dwds/test/puppeteer/extension_test.dart | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dwds/debug_extension_mv3/pubspec.yaml b/dwds/debug_extension_mv3/pubspec.yaml index bb54e5596..b9bb88a3a 100644 --- a/dwds/debug_extension_mv3/pubspec.yaml +++ b/dwds/debug_extension_mv3/pubspec.yaml @@ -24,5 +24,6 @@ dev_dependencies: dependency_overrides: dwds: path: .. + # TODO(elliette): Remove override once package:sse is published. sse: path: /Users/elliottbrooks/dev/sse diff --git a/dwds/test/puppeteer/extension_test.dart b/dwds/test/puppeteer/extension_test.dart index 4c6654b86..ae243c741 100644 --- a/dwds/test/puppeteer/extension_test.dart +++ b/dwds/test/puppeteer/extension_test.dart @@ -94,11 +94,9 @@ void main() async { 'can configure opening DevTools in a tab/window with extension settings', () async { final appUrl = context.appUrl; - // TODO(elliette): Replace with the DevTools url. final devToolsUrlFragment = useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; final windowIdForAppJs = _windowIdForTabJs(appUrl); - // final windowIdForDevToolsJs = _windowIdForTabJs(devToolsUrlFragment); // Navigate to the Dart app: final appTab = await navigateToPage(browser, url: appUrl, isNew: true); From 3978d44787a1b85a7018c0d4a281bf74db9fe609 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Tue, 29 Nov 2022 16:20:55 -0800 Subject: [PATCH 9/9] fix analyzer errors --- dwds/debug_extension_mv3/web/debug_session.dart | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/dwds/debug_extension_mv3/web/debug_session.dart b/dwds/debug_extension_mv3/web/debug_session.dart index 81441ed6e..b7f4c61d6 100644 --- a/dwds/debug_extension_mv3/web/debug_session.dart +++ b/dwds/debug_extension_mv3/web/debug_session.dart @@ -307,7 +307,7 @@ class _DebugSession { final int appTabId; // 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; @@ -331,7 +331,7 @@ class _DebugSession { }); } - void set socketClient(SocketClient client) { + set socketClient(SocketClient client) { _socketClient = client; // Collect extension events and send them periodically to the server. @@ -341,14 +341,6 @@ class _DebugSession { }); } - int get devToolsTabId { - return _devToolsTabId; - } - - void set devToolsTabId(int tabId) { - _devToolsTabId = tabId; - } - void sendEvent(ExtensionEvent event) { _socketClient.sink.add(jsonEncode(serializers.serialize(event))); }