diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart index 50ef17eff..7a65cbb19 100644 --- a/dwds/debug_extension_mv3/web/background.dart +++ b/dwds/debug_extension_mv3/web/background.dart @@ -20,7 +20,6 @@ import 'logger.dart'; import 'messaging.dart'; import 'storage.dart'; import 'utils.dart'; -import 'web_api.dart'; void main() { _registerListeners(); @@ -53,50 +52,13 @@ void _registerListeners() { // Detect clicks on the Dart Debug Extension icon. chrome.action.onClicked.addListener(allowInterop( - (Tab tab) => _startDebugSession( + (Tab tab) => attachDebugger( tab.id, trigger: Trigger.extensionIcon, ), )); } -Future _startDebugSession( - int tabId, { - required Trigger trigger, -}) async { - final isAuthenticated = await _authenticateUser(tabId); - if (!isAuthenticated) return; - - maybeCreateLifelinePort(tabId); - attachDebugger(tabId, trigger: trigger); -} - -Future _authenticateUser(int tabId) async { - final isAlreadyAuthenticated = await _fetchIsAuthenticated(tabId); - if (isAlreadyAuthenticated) return true; - final debugInfo = await _fetchDebugInfo(tabId); - final authUrl = debugInfo?.authUrl; - if (authUrl == null) { - _showWarningNotification('Cannot authenticate user.'); - return false; - } - final isAuthenticated = await _sendAuthRequest(authUrl); - if (isAuthenticated) { - await setStorageObject( - type: StorageObject.isAuthenticated, - value: '$isAuthenticated', - tabId: tabId, - ); - } else { - sendConnectFailureMessage( - ConnectFailureReason.authentication, - dartAppTabId: tabId, - ); - await createTab(authUrl, inNewWindow: false); - } - return isAuthenticated; -} - void _handleRuntimeMessages( dynamic jsRequest, MessageSender sender, Function sendResponse) async { if (jsRequest is! String) return; @@ -150,7 +112,7 @@ void _handleRuntimeMessages( final newState = debugStateChange.newState; final tabId = debugStateChange.tabId; if (newState == DebugStateChange.startDebugging) { - _startDebugSession(tabId, trigger: Trigger.extensionPanel); + attachDebugger(tabId, trigger: Trigger.extensionPanel); } }); } @@ -198,33 +160,6 @@ Future _fetchDebugInfo(int tabId) { ); } -Future _fetchIsAuthenticated(int tabId) async { - final authenticated = await fetchStorageObject( - type: StorageObject.isAuthenticated, - tabId: tabId, - ); - return authenticated == 'true'; -} - -Future _sendAuthRequest(String authUrl) async { - final response = await fetchRequest(authUrl); - final responseBody = response.body ?? ''; - return responseBody.contains('Dart Debug Authentication Success!'); -} - -void _showWarningNotification(String message) { - chrome.notifications.create( - /*notificationId*/ null, - NotificationOptions( - title: '[Error] Dart Debug Extension', - message: message, - iconUrl: 'static_assets/dart.png', - type: 'basic', - ), - /*callback*/ null, - ); -} - Future _getTab() async { final query = QueryInfo(active: true, currentWindow: true); final tabs = List.from(await promiseToFuture(chrome.tabs.query(query))); diff --git a/dwds/debug_extension_mv3/web/debug_session.dart b/dwds/debug_extension_mv3/web/debug_session.dart index 6e7fe94c6..f7ec91b01 100644 --- a/dwds/debug_extension_mv3/web/debug_session.dart +++ b/dwds/debug_extension_mv3/web/debug_session.dart @@ -80,7 +80,39 @@ enum Trigger { extensionIcon, } -void attachDebugger(int dartAppTabId, {required Trigger trigger}) { +enum DebuggerLocation { + angularDartDevTools, + chromeDevTools, + dartDevTools, + ide; + + String get displayName { + switch (this) { + case DebuggerLocation.angularDartDevTools: + return 'AngularDart DevTools'; + case DebuggerLocation.chromeDevTools: + return 'Chrome DevTools'; + case DebuggerLocation.dartDevTools: + return 'a Dart DevTools tab'; + case DebuggerLocation.ide: + return 'an IDE'; + } + } +} + +void attachDebugger(int dartAppTabId, {required Trigger trigger}) async { + // Check if a debugger is already attached: + final existingDebuggerLocation = _debuggerLocation(dartAppTabId); + if (existingDebuggerLocation != null) { + return _showWarningNotification( + 'Already debugging in ${existingDebuggerLocation.displayName}.', + ); + } + + // Verify that the user is authenticated: + final isAuthenticated = await _authenticateUser(dartAppTabId); + if (!isAuthenticated) return; + _tabIdToTrigger[dartAppTabId] = trigger; _registerDebugEventListeners(); chrome.debugger.attach( @@ -154,11 +186,15 @@ String _translateChromeError(String chromeErrorMessage) { Future _onDebuggerEvent( Debuggee source, String method, Object? params) async { + final tabId = source.tabId; maybeForwardMessageToAngularDartDevTools( method: method, params: params, tabId: source.tabId); if (method == 'Runtime.executionContextCreated') { - return _maybeConnectToDwds(source.tabId, params); + // Only try to connect to DWDS if we don't already have a debugger instance: + if (_debuggerLocation(tabId) == null) { + return _maybeConnectToDwds(source.tabId, params); + } } return _forwardChromeDebuggerEventToDwds(source, method, params); @@ -183,7 +219,7 @@ Future _maybeConnectToDwds(int tabId, Object? params) async { ); if (!connected) { debugWarn('Failed to connect to DWDS for $contextOrigin.'); - sendConnectFailureMessage(ConnectFailureReason.unknown, + _sendConnectFailureMessage(ConnectFailureReason.unknown, dartAppTabId: tabId); } } @@ -373,7 +409,7 @@ void _removeDebugSession(_DebugSession debugSession) { } } -void sendConnectFailureMessage(ConnectFailureReason reason, +void _sendConnectFailureMessage(ConnectFailureReason reason, {required int dartAppTabId}) async { final json = jsonEncode(serializers.serialize(ConnectFailure((b) => b ..tabId = dartAppTabId @@ -409,6 +445,81 @@ _DebugSession? _debugSessionForTab(tabId, {required TabType type}) { } } +Future _authenticateUser(int tabId) async { + final isAlreadyAuthenticated = await _fetchIsAuthenticated(tabId); + if (isAlreadyAuthenticated) return true; + final debugInfo = await fetchStorageObject( + type: StorageObject.debugInfo, + tabId: tabId, + ); + final authUrl = debugInfo?.authUrl; + if (authUrl == null) { + _showWarningNotification('Cannot authenticate user.'); + return false; + } + final isAuthenticated = await _sendAuthRequest(authUrl); + if (isAuthenticated) { + await setStorageObject( + type: StorageObject.isAuthenticated, + value: '$isAuthenticated', + tabId: tabId, + ); + } else { + _sendConnectFailureMessage( + ConnectFailureReason.authentication, + dartAppTabId: tabId, + ); + await createTab(authUrl, inNewWindow: false); + } + return isAuthenticated; +} + +Future _fetchIsAuthenticated(int tabId) async { + final authenticated = await fetchStorageObject( + type: StorageObject.isAuthenticated, + tabId: tabId, + ); + return authenticated == 'true'; +} + +Future _sendAuthRequest(String authUrl) async { + final response = await fetchRequest(authUrl); + final responseBody = response.body ?? ''; + return responseBody.contains('Dart Debug Authentication Success!'); +} + +void _showWarningNotification(String message) { + chrome.notifications.create( + /*notificationId*/ null, + NotificationOptions( + title: '[Error] Dart Debug Extension', + message: message, + iconUrl: 'static_assets/dart.png', + type: 'basic', + ), + /*callback*/ null, + ); +} + +DebuggerLocation? _debuggerLocation(int dartAppTabId) { + final debugSession = _debugSessionForTab(dartAppTabId, type: TabType.dartApp); + final trigger = _tabIdToTrigger[dartAppTabId]; + if (debugSession == null || trigger == null) return null; + + switch (trigger) { + case Trigger.extensionIcon: + if (debugSession.devToolsTabId != null) { + return DebuggerLocation.dartDevTools; + } else { + return DebuggerLocation.ide; + } + case Trigger.angularDartDevTools: + return DebuggerLocation.angularDartDevTools; + case Trigger.extensionPanel: + return DebuggerLocation.chromeDevTools; + } +} + /// Construct an [ExtensionEvent] from [method] and [params]. ExtensionEvent _extensionEventFor(String method, dynamic params) { return ExtensionEvent((b) => b diff --git a/dwds/test/puppeteer/extension_test.dart b/dwds/test/puppeteer/extension_test.dart index aa8ac6073..0be2c5128 100644 --- a/dwds/test/puppeteer/extension_test.dart +++ b/dwds/test/puppeteer/extension_test.dart @@ -238,6 +238,62 @@ void main() async { // Verify that the Dart DevTools tab closes: await devToolsTabTarget.onClose; }); + + test('Clicking extension icon while debugging shows warning', () async { + final appUrl = context.appUrl; + final devToolsUrlFragment = + useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; + // Navigate to the Dart app: + final appTab = + await navigateToPage(browser, url: appUrl, isNew: true); + // Click on the Dart Debug Extension icon: + await workerEvalDelay(); + await clickOnExtensionIcon(worker); + // Wait for Dart Devtools to open: + final devToolsTabTarget = await browser.waitForTarget( + (target) => target.url.contains(devToolsUrlFragment)); + // There should be no warning notifications: + var chromeNotifications = await worker.evaluate(_getNotifications()); + expect(chromeNotifications, isEmpty); + // Navigate back to Dart app: + await navigateToPage(browser, url: appUrl, isNew: false); + // Click on the Dart Debug Extension icon again: + await workerEvalDelay(); + await clickOnExtensionIcon(worker); + await workerEvalDelay(); + // There should now be a warning notificiation: + chromeNotifications = await worker.evaluate(_getNotifications()); + expect(chromeNotifications, isNotEmpty); + // Close the Dart app and the associated Dart DevTools: + await appTab.close(); + await devToolsTabTarget.onClose; + }); + + test('Refreshing the Dart app does not open a new Dart DevTools', + () async { + final appUrl = context.appUrl; + final devToolsUrlFragment = + useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; + // Navigate to the Dart app: + final appTab = + await navigateToPage(browser, url: appUrl, isNew: true); + // Click on the Dart Debug Extension icon: + await workerEvalDelay(); + await clickOnExtensionIcon(worker); + // Verify that the Dart DevTools tab is open: + final devToolsTabTarget = await browser.waitForTarget( + (target) => target.url.contains(devToolsUrlFragment)); + expect(devToolsTabTarget.type, equals('page')); + // Refresh the app tab: + await appTab.reload(); + // Verify that we don't open a new Dart DevTools on page refresh: + final devToolsTargets = browser.targets + .where((target) => target.url.contains(devToolsUrlFragment)); + expect(devToolsTargets.length, equals(1)); + // Close the Dart app and the associated Dart DevTools: + await appTab.close(); + await devToolsTabTarget.onClose; + }); }); } @@ -673,6 +729,18 @@ String _clearStorageJs() { '''; } +String _getNotifications() { + return ''' + async () => { + return new Promise((resolve, reject) => { + chrome.notifications.getAll((notifications) => { + resolve(notifications); + }); + }); + } +'''; +} + // TODO(https://github.com/dart-lang/webdev/issues/1787): Compare to golden // images. Currently golden comparison is not set up, since this is only run // locally, not as part of our CI test suite.