Refactor `background.dart` in Dart Debug Extension (#1478)
diff --git a/dwds/debug_extension/web/background.dart b/dwds/debug_extension/web/background.dart
index bdd060c..51494ea 100644
--- a/dwds/debug_extension/web/background.dart
+++ b/dwds/debug_extension/web/background.dart
@@ -60,87 +60,53 @@
}
void main() {
- var startDebugging = allowInterop((_) {
- var query = QueryInfo(active: true, currentWindow: true);
- Tab currentTab;
+ // Start debugging when a user clicks the Dart Debug Extension:
+ browserActionOnClickedAddListener(allowInterop(_startDebugging));
- // Sends commands to debugger attached to the current tab.
- //
- // Extracts the extension backend port from the injected JS.
- var callback = allowInterop((List<Tab> tabs) async {
- currentTab = tabs[0];
- if (!_debuggableTabs.contains(currentTab.id)) return;
+ // Marks the current tab as debuggable and changes the extension icon to blue
+ // when it receives a message.
+ // TODO(elliette): Currently the only messages this ever receives is from
+ // the context script. Consider making it explicit what messages this is
+ // listening for.
+ onMessageAddListener(allowInterop(_maybeMarkTabAsDebuggable));
- if (_tabIdToWarning.containsKey(currentTab.id)) {
- alert(_tabIdToWarning[currentTab.id]);
- return;
- }
+ // Attaches a debug session to the app when the extension receives a
+ // Runtime.executionContextCreated event from DWDS:
+ addDebuggerListener(allowInterop(_maybeAttachDebugSession));
- attach(Debuggee(tabId: currentTab.id), '1.3', allowInterop(() async {
- if (lastError != null) {
- String alertMessage;
- if (lastError.message.contains('Cannot access') ||
- lastError.message.contains('Cannot attach')) {
- alertMessage = _notADartAppAlert;
- } else {
- alertMessage = 'DevTools is already opened on a different window.';
- }
- alert(alertMessage);
- return;
- }
- _tabsToAttach.add(currentTab);
- sendCommand(Debuggee(tabId: currentTab.id), 'Runtime.enable',
- EmptyParam(), allowInterop((e) {}));
- }));
- });
+ // When a Dart application tab is closed, detach the corresponding debug
+ // session:
+ tabsOnRemovedAddListener(allowInterop(_maybeDetachDebugSessionForTab));
- queryTabs(query, allowInterop((List tabs) {
- callback(List.from(tabs));
- }));
- });
- browserActionOnClickedAddListener(startDebugging);
-
- // For testing only.
- onFakeClick = allowInterop(() {
- startDebugging(null);
- });
-
- isDartDebugExtension = true;
-
- onMessageAddListener(allowInterop(
- (Request request, Sender sender, Function sendResponse) async {
- // Register any warnings for the tab:
- if (request.warning != '') {
- _tabIdToWarning[sender.tab.id] = request.warning;
- }
- _debuggableTabs.add(sender.tab.id);
- _updateIcon();
- // TODO(grouma) - We can conditionally auto start debugging here.
- // For example: startDebugging(null);
- sendResponse(true);
+ // When a debug session is detached, remove the reference to it:
+ onDetachAddListener(allowInterop((Debuggee source, DetachReason reason) {
+ _maybeRemoveDebugSessionForTab(source.tabId);
}));
+ // Save the tab ID for the opened DevTools.
+ tabsOnCreatedAddListener(allowInterop(_maybeSaveDevToolsTabId));
+
+ // Forward debugger events to the backend if applicable.
+ addDebuggerListener(allowInterop(_filterAndForwardToBackend));
+
+ // Maybe update the extension icon when a user clicks the tab:
tabsOnActivatedAddListener(allowInterop((ActiveInfo info) {
_updateIcon();
}));
- addDebuggerListener(allowInterop((
- Debuggee source,
- String method,
- Object params,
- ) async {
- if (method == 'Runtime.executionContextCreated') {
- var context = json.decode(stringify(params))['context'];
- var tab = _tabsToAttach.firstWhere((tab) => tab.id == source.tabId,
- orElse: () => null);
- if (tab != null) {
- if (await _tryAttach(context['id'] as int, tab)) {
- _tabsToAttach.remove(tab);
- }
- }
- }
+ // Message handler enabling communication with external Chrome extensions:
+ onMessageExternalAddListener(
+ allowInterop(_handleMessageFromExternalExtensions));
+
+ // Message forwarder enabling communication with external Chrome extensions:
+ addDebuggerListener(allowInterop(_forwardMessageToExternalExtensions));
+
+ // Maybe update the extension icon when the window focus changes:
+ windowOnFocusChangeAddListener(allowInterop((_) {
+ _updateIcon();
}));
+ // Maybe update the extension icon during tab navigation:
webNavigationOnCommittedAddListener(
allowInterop((NavigationInfo navigationInfo) {
if (navigationInfo.transitionType != 'auto_subframe' &&
@@ -149,84 +115,166 @@
}
}));
- windowOnFocusChangeAddListener(allowInterop((_) {
- _updateIcon();
+ /// Everything after this is for testing only.
+ /// TODO(elliette): Figure out if there is a workaround that would allow us to
+ /// remove this.
+ ///
+ /// An automated click on the extension icon is not supported by WebDriver.
+ /// We initiate a fake click from the `debug_extension_test`
+ /// after the extension is loaded.
+ onFakeClick = allowInterop(() {
+ _startDebugging(null);
+ });
+
+ /// This is how we determine the extension tab to connect to during E2E tests.
+ isDartDebugExtension = true;
+}
+
+// Gets the current tab, then attaches the debugger to it:
+void _startDebugging(_) {
+ final getCurrentTabQuery = QueryInfo(active: true, currentWindow: true);
+
+ // Sends commands to debugger attached to the current tab.
+ // Extracts the extension backend port from the injected JS.
+ var attachDebuggerToTab = allowInterop(_attachDebuggerToTab);
+
+ queryTabs(getCurrentTabQuery, allowInterop((List<Tab> tabs) {
+ attachDebuggerToTab(tabs[0]);
}));
+}
- tabsOnRemovedAddListener(allowInterop((int tabId, _) {
- _debuggableTabs.remove(tabId);
- var session = _debugSessions.firstWhere(
- (session) =>
- session.appTabId == tabId || session.devtoolsTabId == tabId,
- orElse: () => null);
- if (session != null) {
- session.socketClient.close();
- _debugSessions.remove(session);
- detach(Debuggee(tabId: session.appTabId), allowInterop(() {}));
- }
- }));
+void _attachDebuggerToTab(Tab currentTab) async {
+ if (!_debuggableTabs.contains(currentTab.id)) return;
- onDetachAddListener(allowInterop((Debuggee source, DetachReason reason) {
- var session = _debugSessions.firstWhere(
- (session) => session.appTabId == source.tabId,
- orElse: () => null);
- if (session != null) {
- session.socketClient.close();
- _debugSessions.remove(session);
- }
- }));
+ if (_tabIdToWarning.containsKey(currentTab.id)) {
+ alert(_tabIdToWarning[currentTab.id]);
+ return;
+ }
- tabsOnCreatedAddListener(allowInterop((Tab tab) async {
- // Remembers the ID of the DevTools tab.
- //
- // This assumes that the next launched tab after a session is created is the
- // DevTools tab.
- if (_debugSessions.isNotEmpty) _debugSessions.last.devtoolsTabId ??= tab.id;
- }));
-
- addDebuggerListener(allowInterop(_filterAndForward));
-
- onMessageExternalAddListener(allowInterop(
- (Request request, Sender sender, Function sendResponse) async {
- if (_allowedExtensions.contains(sender.id)) {
- if (request.name == 'chrome.debugger.sendCommand') {
- try {
- var options = request.options as SendCommandOptions;
- sendCommand(Debuggee(tabId: request.tabId), options.method,
- options.commandParams, allowInterop(([e]) {
- // No arguments indicate that an error occurred.
- if (e == null) {
- sendResponse(ErrorResponse()..error = stringify(lastError));
- } else {
- sendResponse(e);
- }
- }));
- } catch (e) {
- sendResponse(ErrorResponse()..error = '$e');
- }
- } else if (request.name == 'dwds.encodedUri') {
- sendResponse(_tabIdToEncodedUri[request.tabId] ?? '');
- } else if (request.name == 'dwds.startDebugging') {
- startDebugging(null);
- // TODO(grouma) - Actually determine if debugging initiated
- // successfully.
- sendResponse(true);
+ attach(Debuggee(tabId: currentTab.id), '1.3', allowInterop(() async {
+ if (lastError != null) {
+ String alertMessage;
+ if (lastError.message.contains('Cannot access') ||
+ lastError.message.contains('Cannot attach')) {
+ alertMessage = _notADartAppAlert;
} else {
- sendResponse(
- ErrorResponse()..error = 'Unknown request name: ${request.name}');
+ alertMessage = 'DevTools is already opened on a different window.';
}
+ alert(alertMessage);
+ return;
}
+ _tabsToAttach.add(currentTab);
+ sendCommand(Debuggee(tabId: currentTab.id), 'Runtime.enable', EmptyParam(),
+ allowInterop((e) {}));
}));
+}
- addDebuggerListener(
- allowInterop((Debuggee source, String method, Object params) async {
- if (_allowedEvents.contains(method)) {
- sendMessageToExtensions(Request(
- name: 'chrome.debugger.event',
- tabId: source.tabId,
- options: DebugEvent(method: method, params: params)));
+void _maybeMarkTabAsDebuggable(
+ Request request, Sender sender, Function sendResponse) async {
+ // Register any warnings for the tab:
+ if (request.warning != '') {
+ _tabIdToWarning[sender.tab.id] = request.warning;
+ }
+ _debuggableTabs.add(sender.tab.id);
+ _updateIcon();
+ // TODO(grouma) - We can conditionally auto start debugging here.
+ // For example: _startDebugging(null);
+ sendResponse(true);
+}
+
+void _maybeAttachDebugSession(
+ Debuggee source,
+ String method,
+ Object params,
+) async {
+ // Return early if it's not a Runtime.executionContextCreated event (sent from
+ // DWDS):
+ if (method != 'Runtime.executionContextCreated') return;
+
+ var context = json.decode(stringify(params))['context'];
+ var tab = _tabsToAttach.firstWhere((tab) => tab.id == source.tabId,
+ orElse: () => null);
+ if (tab != null) {
+ if (await _tryAttach(context['id'] as int, tab)) {
+ _tabsToAttach.remove(tab);
}
- }));
+ }
+}
+
+void _maybeDetachDebugSessionForTab(int tabId, _) {
+ final removedTabId = _maybeRemoveDebugSessionForTab(tabId);
+
+ if (removedTabId != -1) {
+ detach(Debuggee(tabId: removedTabId), allowInterop(() {}));
+ }
+}
+
+void _maybeSaveDevToolsTabId(Tab tab) async {
+ // Remembers the ID of the DevTools tab.
+ //
+ // This assumes that the next launched tab after a session is created is the
+ // DevTools tab.
+ if (_debugSessions.isNotEmpty) _debugSessions.last.devtoolsTabId ??= tab.id;
+}
+
+void _handleMessageFromExternalExtensions(
+ Request request, Sender sender, Function sendResponse) async {
+ if (_allowedExtensions.contains(sender.id)) {
+ if (request.name == 'chrome.debugger.sendCommand') {
+ try {
+ var options = request.options as SendCommandOptions;
+
+ void sendResponseOrError([e]) {
+ // No arguments indicate that an error occurred.
+ if (e == null) {
+ sendResponse(ErrorResponse()..error = stringify(lastError));
+ } else {
+ sendResponse(e);
+ }
+ }
+
+ sendCommand(Debuggee(tabId: request.tabId), options.method,
+ options.commandParams, allowInterop(sendResponseOrError));
+ } catch (e) {
+ sendResponse(ErrorResponse()..error = '$e');
+ }
+ } else if (request.name == 'dwds.encodedUri') {
+ sendResponse(_tabIdToEncodedUri[request.tabId] ?? '');
+ } else if (request.name == 'dwds.startDebugging') {
+ _startDebugging(null);
+ // TODO(grouma) - Actually determine if debugging initiated
+ // successfully.
+ sendResponse(true);
+ } else {
+ sendResponse(
+ ErrorResponse()..error = 'Unknown request name: ${request.name}');
+ }
+ }
+}
+
+void _forwardMessageToExternalExtensions(
+ Debuggee source, String method, Object params) async {
+ if (_allowedEvents.contains(method)) {
+ sendMessageToExtensions(Request(
+ name: 'chrome.debugger.event',
+ tabId: source.tabId,
+ options: DebugEvent(method: method, params: params)));
+ }
+}
+
+// Tries to remove the debug session for the specified tab. If no session is
+// found, returns -1. Otherwise returns the tab ID.
+int _maybeRemoveDebugSessionForTab(int tabId) {
+ var session = _debugSessions.firstWhere(
+ (session) => session.appTabId == tabId || session.devtoolsTabId == tabId,
+ orElse: () => null);
+ if (session != null) {
+ session.socketClient.close();
+ _debugSessions.remove(session);
+ return session.appTabId;
+ } else {
+ return -1;
+ }
}
void sendMessageToExtensions(Request request) {
@@ -400,8 +448,8 @@
..params = jsonEncode(json.decode(stringify(params)))
..method = jsonEncode(method));
-/// Forward the event if applicable.
-void _filterAndForward(Debuggee source, String method, Object params) {
+/// Forward debugger events to the backend if applicable.
+void _filterAndForwardToBackend(Debuggee source, String method, Object params) {
var debugSession = _debugSessions.firstWhere(
(session) => session.appTabId == source.tabId,
orElse: () => null);