Skip to content

[MV3 Debug Extension] Don't connect to DWDS if we already have a debugger attached #1903

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 6 commits into from
Feb 3, 2023
Merged
Show file tree
Hide file tree
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
69 changes: 2 additions & 67 deletions dwds/debug_extension_mv3/web/background.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import 'logger.dart';
import 'messaging.dart';
import 'storage.dart';
import 'utils.dart';
import 'web_api.dart';

void main() {
_registerListeners();
Expand Down Expand Up @@ -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<void> _startDebugSession(
int tabId, {
required Trigger trigger,
}) async {
final isAuthenticated = await _authenticateUser(tabId);
if (!isAuthenticated) return;

maybeCreateLifelinePort(tabId);
attachDebugger(tabId, trigger: trigger);
}

Future<bool> _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<String>(
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;
Expand Down Expand Up @@ -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);
}
});
}
Expand Down Expand Up @@ -198,33 +160,6 @@ Future<DebugInfo?> _fetchDebugInfo(int tabId) {
);
}

Future<bool> _fetchIsAuthenticated(int tabId) async {
final authenticated = await fetchStorageObject<String>(
type: StorageObject.isAuthenticated,
tabId: tabId,
);
return authenticated == 'true';
}

Future<bool> _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<Tab?> _getTab() async {
final query = QueryInfo(active: true, currentWindow: true);
final tabs = List<Tab>.from(await promiseToFuture(chrome.tabs.query(query)));
Expand Down
119 changes: 115 additions & 4 deletions dwds/debug_extension_mv3/web/debug_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -154,11 +186,15 @@ String _translateChromeError(String chromeErrorMessage) {

Future<void> _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);
Expand All @@ -183,7 +219,7 @@ Future<void> _maybeConnectToDwds(int tabId, Object? params) async {
);
if (!connected) {
debugWarn('Failed to connect to DWDS for $contextOrigin.');
sendConnectFailureMessage(ConnectFailureReason.unknown,
_sendConnectFailureMessage(ConnectFailureReason.unknown,
dartAppTabId: tabId);
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -409,6 +445,81 @@ _DebugSession? _debugSessionForTab(tabId, {required TabType type}) {
}
}

Future<bool> _authenticateUser(int tabId) async {
final isAlreadyAuthenticated = await _fetchIsAuthenticated(tabId);
if (isAlreadyAuthenticated) return true;
final debugInfo = await fetchStorageObject<DebugInfo>(
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<String>(
type: StorageObject.isAuthenticated,
value: '$isAuthenticated',
tabId: tabId,
);
} else {
_sendConnectFailureMessage(
ConnectFailureReason.authentication,
dartAppTabId: tabId,
);
await createTab(authUrl, inNewWindow: false);
}
return isAuthenticated;
}

Future<bool> _fetchIsAuthenticated(int tabId) async {
final authenticated = await fetchStorageObject<String>(
type: StorageObject.isAuthenticated,
tabId: tabId,
);
return authenticated == 'true';
}

Future<bool> _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
Expand Down
68 changes: 68 additions & 0 deletions dwds/test/puppeteer/extension_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
});
}

Expand Down Expand Up @@ -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.
Expand Down