// 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; import 'dart:async'; import 'dart:convert'; // TODO: https://github.com/dart-lang/webdev/issues/2508 // ignore: deprecated_member_use import 'dart:html'; import 'package:dwds/data/debug_info.dart'; // TODO: https://github.com/dart-lang/webdev/issues/2508 // ignore: deprecated_member_use import 'package:js/js.dart'; import 'chrome_api.dart'; import 'data_serializers.dart'; import 'data_types.dart'; import 'debug_session.dart'; import 'logger.dart'; import 'messaging.dart'; import 'storage.dart'; import 'utils.dart'; bool _connecting = false; String _backgroundColor = _darkColor; bool _isDartApp = true; const _bugLinkId = 'bugLink'; const _darkColor = '202125'; const _darkThemeClass = 'dark-theme'; const _hiddenClass = 'hidden'; const _iframeContainerId = 'iframeContainer'; const _landingPageId = 'landingPage'; const _launchDebugConnectionButtonId = 'launchDebugConnectionButton'; const _lightColor = 'ffffff'; const _lightThemeClass = 'light-theme'; const _loadingSpinnerId = 'loadingSpinner'; const _panelAttribute = 'data-panel'; const _panelBodyId = 'panelBody'; const _showClass = 'show'; const _warningBannerId = 'warningBanner'; const _warningMsgId = 'warningMsg'; const _noAppDetectedMsg = 'No app detected.'; const _lostConnectionMsg = 'Lost connection.'; const _connectionTimeoutMsg = 'Connection timed out.'; const _failedToConnectMsg = 'Failed to connect, please try again.'; const _pleaseAuthenticateMsg = 'Please re-authenticate and try again.'; const _multipleAppsMsg = 'Cannot debug multiple apps in a page.'; int get _tabId => chrome.devtools.inspectedWindow.tabId; Future main() async { unawaited( _registerListeners().catchError((Object? error) { debugWarn('Error registering listeners in panel: $error'); }), ); _setColorThemeToMatchChromeDevTools(); await _maybeUpdateFileABugLink(); final multipleApps = await fetchStorageObject( type: StorageObject.multipleAppsDetected, tabId: _tabId, ); _maybeShowMultipleAppsWarning(multipleApps); } Future _registerListeners() async { chrome.storage.onChanged.addListener(allowInterop(_handleStorageChanges)); chrome.runtime.onMessage.addListener(allowInterop(_handleRuntimeMessages)); final launchDebugConnectionButton = document.getElementById(_launchDebugConnectionButtonId) as ButtonElement; launchDebugConnectionButton.addEventListener('click', _launchDebugConnection); await _maybeInjectDevToolsIframe(); } void _handleRuntimeMessages( dynamic jsRequest, // ignore: avoid-unused-parameters MessageSender sender, // ignore: avoid-unused-parameters Function sendResponse, ) { if (jsRequest is! String) return; interceptMessage( message: jsRequest, expectedType: MessageType.debugStateChange, expectedSender: Script.background, expectedRecipient: Script.debuggerPanel, sender: sender, 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, sender: sender, messageHandler: (ConnectFailure connectFailure) async { debugLog( 'Received connect failure for ${connectFailure.tabId} vs $_tabId', ); if (connectFailure.tabId != _tabId) { return; } _connecting = false; _handleConnectFailure( ConnectFailureReason.fromString(connectFailure.reason ?? 'unknown'), ); }, ); } void _handleStorageChanges(Object storageObj, String _) { interceptStorageChange( storageObj: storageObj, expectedType: StorageObject.debugInfo, tabId: _tabId, changeHandler: _handleDebugInfoChanges, ); interceptStorageChange( storageObj: storageObj, expectedType: StorageObject.devToolsUri, tabId: _tabId, changeHandler: _handleDevToolsUriChanges, ); interceptStorageChange( storageObj: storageObj, expectedType: StorageObject.multipleAppsDetected, tabId: _tabId, changeHandler: _maybeShowMultipleAppsWarning, ); } void _handleDebugInfoChanges(DebugInfo? debugInfo) { if (debugInfo == null && _isDartApp) { _isDartApp = false; if (!_warningBannerIsVisible()) { _showWarningBanner(_noAppDetectedMsg); } } if (debugInfo != null && !_isDartApp) { _isDartApp = true; if (_warningBannerIsVisible()) { _hideWarningBanner(); } } } void _handleDevToolsUriChanges(String? devToolsUri) { if (devToolsUri != null) { _injectDevToolsIframe(devToolsUri); } } void _maybeShowMultipleAppsWarning(String? multipleApps) { if (multipleApps != null) { _showWarningBanner(_multipleAppsMsg); } else { if (_warningBannerIsVisible(message: _multipleAppsMsg)) { _hideWarningBanner(); } } } Future _maybeUpdateFileABugLink() async { final debugInfo = await fetchStorageObject( type: StorageObject.debugInfo, tabId: _tabId, ); final isInternal = debugInfo?.isInternalBuild ?? false; if (isInternal) { final bugLink = document.getElementById(_bugLinkId); if (bugLink == null) return; bugLink.setAttribute('href', 'http://go/dde-bug'); } } void _setColorThemeToMatchChromeDevTools() { final chromeTheme = chrome.devtools.panels.themeName; final panelBody = document.getElementById(_panelBodyId); if (chromeTheme == 'dark') { _backgroundColor = _darkColor; _updateColorThemeForElement(panelBody, isDarkTheme: true); } else { _backgroundColor = _lightColor; _updateColorThemeForElement(panelBody, isDarkTheme: false); } } void _updateColorThemeForElement( Element? element, { required bool isDarkTheme, }) { if (element == null) return; final classToRemove = isDarkTheme ? _lightThemeClass : _darkThemeClass; if (element.classes.contains(classToRemove)) { element.classes.remove(classToRemove); final classToAdd = isDarkTheme ? _darkThemeClass : _lightThemeClass; element.classes.add(classToAdd); } } void _handleDebugConnectionLost(String? reason) { final detachReason = DetachReason.fromString(reason ?? 'unknown'); _removeDevToolsIframe(); _updateElementVisibility(_landingPageId, visible: true); switch (detachReason) { case DetachReason.canceledByUser: return; case DetachReason.staleDebugSession: case DetachReason.navigatedAwayFromApp: _showWarningBanner(_noAppDetectedMsg); default: _showWarningBanner(_lostConnectionMsg); } } void _handleConnectFailure(ConnectFailureReason reason) { switch (reason) { case ConnectFailureReason.authentication: _showWarningBanner(_pleaseAuthenticateMsg); case ConnectFailureReason.noDartApp: _showWarningBanner(_noAppDetectedMsg); case ConnectFailureReason.timeout: _showWarningBanner(_connectionTimeoutMsg); default: _showWarningBanner(_failedToConnectMsg); } _updateElementVisibility(_launchDebugConnectionButtonId, visible: true); _updateElementVisibility(_loadingSpinnerId, visible: false); } bool _warningBannerIsVisible({String? message}) { final warningBanner = document.getElementById(_warningBannerId); final isVisible = warningBanner != null && warningBanner.classes.contains(_showClass); if (message == null || !isVisible) return isVisible; final warningMsg = document.getElementById(_warningMsgId); return warningMsg?.innerHtml == message; } void _showWarningBanner(String message) { final warningMsg = document.getElementById(_warningMsgId); warningMsg?.setInnerHtml(message); final warningBanner = document.getElementById(_warningBannerId); warningBanner?.classes.add(_showClass); } void _hideWarningBanner() { final warningBanner = document.getElementById(_warningBannerId); warningBanner?.classes.remove(_showClass); } Future _launchDebugConnection(Event _) async { _updateElementVisibility(_launchDebugConnectionButtonId, visible: false); _updateElementVisibility(_loadingSpinnerId, visible: true); final json = jsonEncode( serializers.serialize( DebugStateChange( (b) => b ..tabId = _tabId ..newState = DebugStateChange.startDebugging, ), ), ); await sendRuntimeMessage( type: MessageType.debugStateChange, body: json, sender: Script.debuggerPanel, recipient: Script.background, ); unawaited(_maybeHandleConnectionTimeout().catchError((_) {})); } Future _maybeHandleConnectionTimeout() async { _connecting = true; await Future.delayed(const Duration(seconds: 10)); if (_connecting) { _handleConnectFailure(ConnectFailureReason.timeout); } } Future _maybeInjectDevToolsIframe() async { final devToolsUri = await fetchStorageObject( type: StorageObject.devToolsUri, tabId: _tabId, ); if (devToolsUri == null) return; if (isActiveDebugSession(_tabId)) { debugWarn('Unexpected state. Stale DevTools URI.'); await clearStaleDebugSession(_tabId); _updateElementVisibility(_landingPageId, visible: true); } else { _injectDevToolsIframe(devToolsUri); } } void _injectDevToolsIframe(String devToolsUri) { _connecting = false; final iframeContainer = document.getElementById(_iframeContainerId); if (iframeContainer == null) return; final panelBody = document.getElementById(_panelBodyId); final panelType = panelBody?.getAttribute(_panelAttribute) ?? 'debugger'; final iframe = document.createElement('iframe'); final iframeSrc = addQueryParameters( devToolsUri, queryParameters: { 'ide': 'ChromeDevTools', 'embed': 'true', 'page': panelType, 'backgroundColor': _backgroundColor, }, ); iframe.setAttribute('src', iframeSrc); _hideWarningBanner(); _updateElementVisibility(_landingPageId, visible: false); _updateElementVisibility(_loadingSpinnerId, visible: false); _updateElementVisibility(_launchDebugConnectionButtonId, visible: true); iframeContainer.append(iframe); } void _removeDevToolsIframe() { final iframeContainer = document.getElementById(_iframeContainerId); final iframe = iframeContainer?.firstChild; if (iframe == null) return; iframe.remove(); } void _updateElementVisibility(String elementId, {required bool visible}) { final element = document.getElementById(elementId); if (element == null) return; if (visible) { element.classes.remove(_hiddenClass); } else { element.classes.add(_hiddenClass); } }