blob: e112b16bea8e6e403035ba2351df541e1a0fa597 [file] [log] [blame]
// 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 '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 'cider_connection.dart';
import 'cross_extension_communication.dart';
import 'data_types.dart';
import 'debug_session.dart';
import 'logger.dart';
import 'messaging.dart';
import 'storage.dart';
import 'utils.dart';
void main() {
_registerListeners();
}
void _registerListeners() {
chrome.runtime.onMessage.addListener(allowInterop(_handleRuntimeMessages));
// The only extension allowed to send messages to this extension is the
// AngularDart DevTools extension. Its permission is set in the manifest.json
// externally_connectable field.
chrome.runtime.onMessageExternal.addListener(
allowInterop(handleMessagesFromAngularDartDevTools),
);
// The only external service that sends messages to the Dart Debug Extension
// is Cider.
chrome.runtime.onConnectExternal.addListener(
allowInterop(handleCiderConnectRequest),
);
// Update the extension icon on tab navigation:
chrome.tabs.onActivated.addListener(
allowInterop((ActiveInfo info) async {
await _updateIcon(info.tabId);
}),
);
chrome.windows.onFocusChanged.addListener(
allowInterop((_) async {
final currentTab = await activeTab;
if (currentTab?.id case final tabId?) {
await _updateIcon(tabId);
}
}),
);
chrome.webNavigation.onCommitted.addListener(
allowInterop(_detectNavigationAwayFromDartApp),
);
chrome.commands.onCommand.addListener(
allowInterop(_maybeSendCopyAppIdRequest),
);
}
Future<void> _handleRuntimeMessages(
dynamic jsRequest,
MessageSender sender,
Function sendResponse,
) async {
if (jsRequest is! String) return;
interceptMessage<String>(
message: jsRequest,
expectedType: MessageType.isAuthenticated,
expectedSender: Script.detector,
expectedRecipient: Script.background,
sender: sender,
messageHandler: (String isAuthenticated) async {
final dartTab = sender.tab;
if (dartTab == null) {
debugWarn('Received auth info but tab is missing.');
return;
}
// Save the authentication info in storage:
await setStorageObject<String>(
type: StorageObject.isAuthenticated,
value: isAuthenticated,
tabId: dartTab.id,
);
},
);
interceptMessage<DebugInfo>(
message: jsRequest,
expectedType: MessageType.debugInfo,
expectedSender: Script.detector,
expectedRecipient: Script.background,
sender: sender,
messageHandler: (DebugInfo debugInfo) async {
final dartTab = sender.tab;
if (dartTab == null) {
debugWarn('Received debug info but tab is missing.');
return;
}
// If this is a new Dart app, we need to clear old debug session data:
if (!await _matchesAppInStorage(debugInfo.appId, tabId: dartTab.id)) {
await clearStaleDebugSession(dartTab.id);
}
// Save the debug info for the Dart app in storage:
await setStorageObject<DebugInfo>(
type: StorageObject.debugInfo,
value: _addTabInfo(debugInfo, tab: dartTab),
tabId: dartTab.id,
);
// Update the icon to show that a Dart app has been detected:
final currentTab = await activeTab;
if (currentTab?.id == dartTab.id) {
await _updateIcon(dartTab.id);
}
},
);
interceptMessage<DebugStateChange>(
message: jsRequest,
expectedType: MessageType.debugStateChange,
expectedSender: Script.debuggerPanel,
expectedRecipient: Script.background,
sender: sender,
messageHandler: (DebugStateChange debugStateChange) {
final newState = debugStateChange.newState;
final tabId = debugStateChange.tabId;
if (newState == DebugStateChange.startDebugging) {
attachDebugger(tabId, trigger: Trigger.extensionPanel);
}
},
);
interceptMessage<DebugStateChange>(
message: jsRequest,
expectedType: MessageType.debugStateChange,
expectedSender: Script.popup,
expectedRecipient: Script.background,
sender: sender,
messageHandler: (DebugStateChange debugStateChange) {
final newState = debugStateChange.newState;
final tabId = debugStateChange.tabId;
if (newState == DebugStateChange.startDebugging) {
attachDebugger(tabId, trigger: Trigger.extensionIcon);
}
},
);
interceptMessage<String>(
message: jsRequest,
expectedType: MessageType.multipleAppsDetected,
expectedSender: Script.detector,
expectedRecipient: Script.background,
sender: sender,
messageHandler: (String multipleAppsDetected) async {
final dartTab = sender.tab;
if (dartTab == null) {
debugWarn('Received multiple apps detected but tab is missing.');
return;
}
// Save the multiple apps info in storage:
await setStorageObject<String>(
type: StorageObject.multipleAppsDetected,
value: multipleAppsDetected,
tabId: dartTab.id,
);
_setWarningIcon(dartTab.id);
},
);
interceptMessage<String>(
message: jsRequest,
expectedType: MessageType.appId,
expectedSender: Script.copier,
expectedRecipient: Script.background,
sender: sender,
messageHandler: (String appId) {
displayNotification('Copied app ID: $appId');
},
);
sendResponse(defaultResponse);
}
Future<void> _detectNavigationAwayFromDartApp(
NavigationInfo navigationInfo,
) async {
// Ignore any navigation events within the page itself (e.g., opening a link,
// reloading the page, reloading an IFRAME, etc):
if (_isInternalNavigation(navigationInfo)) return;
final tabId = navigationInfo.tabId;
final debugInfo = await _fetchDebugInfo(navigationInfo.tabId);
if (debugInfo == null) return;
if (debugInfo.tabUrl != navigationInfo.url) {
_setDefaultIcon(navigationInfo.tabId);
await clearStaleDebugSession(tabId);
await removeStorageObject(type: StorageObject.debugInfo, tabId: tabId);
await detachDebugger(
tabId,
type: TabType.dartApp,
reason: DetachReason.navigatedAwayFromApp,
);
}
}
bool _isInternalNavigation(NavigationInfo navigationInfo) {
return [
'auto_subframe',
'form_submit',
'link',
'manual_subframe',
'reload',
].contains(navigationInfo.transitionType);
}
DebugInfo _addTabInfo(DebugInfo debugInfo, {required Tab tab}) {
return DebugInfo(
(b) =>
b
..appEntrypointPath = debugInfo.appEntrypointPath
..appId = debugInfo.appId
..appInstanceId = debugInfo.appInstanceId
..appOrigin = debugInfo.appOrigin
..appUrl = debugInfo.appUrl
..authUrl = debugInfo.authUrl
..extensionUrl = debugInfo.extensionUrl
..isInternalBuild = debugInfo.isInternalBuild
..isFlutterApp = debugInfo.isFlutterApp
..workspaceName = debugInfo.workspaceName
..tabUrl = tab.url
..tabId = tab.id,
);
}
Future<bool> _maybeSendCopyAppIdRequest(String command, [Tab? tab]) async {
if (command != 'copyAppId') return false;
final tabId = (tab ?? await activeTab)?.id;
if (tabId == null) return false;
final debugInfo = await _fetchDebugInfo(tabId);
final workspaceName = debugInfo?.workspaceName;
if (workspaceName == null) return false;
final appId = '$workspaceName-$tabId';
return sendTabsMessage(
tabId: tabId,
type: MessageType.appId,
body: appId,
sender: Script.background,
recipient: Script.copier,
);
}
Future<void> _updateIcon(int activeTabId) async {
final debugInfo = await _fetchDebugInfo(activeTabId);
if (debugInfo == null) {
_setDefaultIcon(activeTabId);
return;
}
final multipleApps = await fetchStorageObject<String>(
type: StorageObject.multipleAppsDetected,
tabId: activeTabId,
);
multipleApps == null
? _setDebuggableIcon(activeTabId)
: _setWarningIcon(activeTabId);
}
void _setDebuggableIcon(int tabId) {
setExtensionIcon(IconInfo(path: 'static_assets/dart.png'));
setExtensionPopup(
PopupDetails(popup: 'static_assets/popup.html', tabId: tabId),
);
}
void _setWarningIcon(int tabId) {
setExtensionPopup(
PopupDetails(popup: 'static_assets/popup.html', tabId: tabId),
);
}
void _setDefaultIcon(int tabId) {
final iconPath =
isDevMode ? 'static_assets/dart_dev.png' : 'static_assets/dart_grey.png';
setExtensionIcon(IconInfo(path: iconPath));
setExtensionPopup(PopupDetails(popup: '', tabId: tabId));
}
Future<DebugInfo?> _fetchDebugInfo(int tabId) {
return fetchStorageObject<DebugInfo>(
type: StorageObject.debugInfo,
tabId: tabId,
);
}
Future<bool> _matchesAppInStorage(String? appId, {required int tabId}) async {
final debugInfo = await _fetchDebugInfo(tabId);
return appId != null && appId == debugInfo?.appId;
}