Skip to content

Refactor background.dart in Dart Debug Extension #1478

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 18, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
322 changes: 185 additions & 137 deletions dwds/debug_extension/web/background.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,87 +60,53 @@ class DebugSession {
}

void main() {
var startDebugging = allowInterop((_) {
var query = QueryInfo(active: true, currentWindow: true);
Tab currentTab;

// 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;

if (_tabIdToWarning.containsKey(currentTab.id)) {
alert(_tabIdToWarning[currentTab.id]);
return;
}

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) {}));
}));
});
// Start debugging when a user clicks the Dart Debug Extension:
browserActionOnClickedAddListener(allowInterop(_startDebugging));

queryTabs(query, allowInterop((List tabs) {
callback(List.from(tabs));
}));
});
browserActionOnClickedAddListener(startDebugging);
// 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));

// For testing only.
onFakeClick = allowInterop(() {
startDebugging(null);
});
// Attaches a debug session to the app when the extension receives a
// Runtime.executionContextCreated event from DWDS:
addDebuggerListener(allowInterop(_maybeAttachDebugSession));

isDartDebugExtension = true;
// When a Dart application tab is closed, detach the corresponding debug
// session:
tabsOnRemovedAddListener(allowInterop(_maybeDetachDebugSessionForTab));

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' &&
Expand All @@ -149,84 +115,166 @@ void main() {
}
}));

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]);
}));
}

void _attachDebuggerToTab(Tab currentTab) async {
if (!_debuggableTabs.contains(currentTab.id)) return;

if (_tabIdToWarning.containsKey(currentTab.id)) {
alert(_tabIdToWarning[currentTab.id]);
return;
}

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(() {}));
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) {}));
}));
}

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);
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);
}
}));
}
}

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;
}));
void _maybeDetachDebugSessionForTab(int tabId, _) {
final removedTabId = _maybeRemoveDebugSessionForTab(tabId);

if (removedTabId != -1) {
detach(Debuggee(tabId: removedTabId), allowInterop(() {}));
}
}

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');
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);
}
}
} 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}');

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}');
}
}));
}
}

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 _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) {
Expand Down Expand Up @@ -400,8 +448,8 @@ ExtensionEvent _extensionEventFor(String method, Object params) =>
..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);
Expand Down