Skip to content

Commit 252b01a

Browse files
authored
Allow the Dart Debug Extension to communicate with Cider (#2234)
1 parent d7e0d1f commit 252b01a

File tree

10 files changed

+360
-18
lines changed

10 files changed

+360
-18
lines changed

dwds/debug_extension_mv3/web/background.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:dwds/data/debug_info.dart';
99
import 'package:js/js.dart';
1010

1111
import 'chrome_api.dart';
12+
import 'cider_connection.dart';
1213
import 'cross_extension_communication.dart';
1314
import 'data_types.dart';
1415
import 'debug_session.dart';
@@ -31,6 +32,10 @@ void _registerListeners() {
3132
chrome.runtime.onMessageExternal.addListener(
3233
allowInterop(handleMessagesFromAngularDartDevTools),
3334
);
35+
// The only external service that sends messages to the Dart Debug Extension
36+
// is Cider.
37+
chrome.runtime.onConnectExternal
38+
.addListener(allowInterop(handleCiderConnectRequest));
3439
// Update the extension icon on tab navigation:
3540
chrome.tabs.onActivated.addListener(
3641
allowInterop((ActiveInfo info) async {
@@ -105,7 +110,7 @@ Future<void> _handleRuntimeMessages(
105110
// Save the debug info for the Dart app in storage:
106111
await setStorageObject<DebugInfo>(
107112
type: StorageObject.debugInfo,
108-
value: _addTabUrl(debugInfo, tabUrl: dartTab.url),
113+
value: _addTabInfo(debugInfo, tab: dartTab),
109114
tabId: dartTab.id,
110115
);
111116
// Update the icon to show that a Dart app has been detected:
@@ -183,7 +188,7 @@ bool _isInternalNavigation(NavigationInfo navigationInfo) {
183188
].contains(navigationInfo.transitionType);
184189
}
185190

186-
DebugInfo _addTabUrl(DebugInfo debugInfo, {required String tabUrl}) {
191+
DebugInfo _addTabInfo(DebugInfo debugInfo, {required Tab tab}) {
187192
return DebugInfo(
188193
(b) => b
189194
..appEntrypointPath = debugInfo.appEntrypointPath
@@ -195,7 +200,9 @@ DebugInfo _addTabUrl(DebugInfo debugInfo, {required String tabUrl}) {
195200
..extensionUrl = debugInfo.extensionUrl
196201
..isInternalBuild = debugInfo.isInternalBuild
197202
..isFlutterApp = debugInfo.isFlutterApp
198-
..tabUrl = tabUrl,
203+
..workspaceName = debugInfo.workspaceName
204+
..tabUrl = tab.url
205+
..tabId = tab.id,
199206
);
200207
}
201208

dwds/debug_extension_mv3/web/chrome_api.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ class Runtime {
181181

182182
external ConnectionHandler get onConnect;
183183

184+
external ConnectionHandler get onConnectExternal;
185+
184186
external OnMessageHandler get onMessage;
185187

186188
external OnMessageHandler get onMessageExternal;
@@ -203,9 +205,19 @@ class ConnectInfo {
203205
class Port {
204206
external String? get name;
205207
external void disconnect();
208+
external void postMessage(Object message);
209+
external OnPortMessageHandler get onMessage;
206210
external ConnectionHandler get onDisconnect;
207211
}
208212

213+
@JS()
214+
@anonymous
215+
class OnPortMessageHandler {
216+
external void addListener(
217+
void Function(dynamic, Port) callback,
218+
);
219+
}
220+
209221
@JS()
210222
@anonymous
211223
class ConnectionHandler {
@@ -252,7 +264,10 @@ class Storage {
252264
@JS()
253265
@anonymous
254266
class StorageArea {
255-
external Object get(List<String> keys, void Function(Object result) callback);
267+
external Object get(
268+
List<String>? keys,
269+
void Function(Object result) callback,
270+
);
256271

257272
external Object set(Object items, void Function()? callback);
258273

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
@JS()
6+
library cider_connection;
7+
8+
import 'dart:convert';
9+
10+
import 'package:dwds/data/debug_info.dart';
11+
import 'package:js/js.dart';
12+
13+
import 'chrome_api.dart';
14+
import 'debug_session.dart';
15+
import 'logger.dart';
16+
import 'storage.dart';
17+
18+
/// Defines the message types that can be passed to/from Cider.
19+
///
20+
/// The types must match those defined by ChromeExtensionMessageType in the
21+
/// Cider extension.
22+
enum CiderMessageType {
23+
error,
24+
startDebugResponse,
25+
startDebugRequest,
26+
stopDebugResponse,
27+
stopDebugRequest,
28+
}
29+
30+
/// Defines the error types that can be sent to Cider.
31+
///
32+
/// The types must match those defined by ChromeExtensionErrorType in the
33+
/// Cider extension.
34+
enum CiderErrorType {
35+
chromeError,
36+
internalError,
37+
invalidRequest,
38+
multipleDartTabs,
39+
noDartTab,
40+
noWorkspace,
41+
}
42+
43+
const _ciderPortName = 'cider';
44+
Port? _ciderPort;
45+
46+
/// Handles a connect request from Cider.
47+
///
48+
/// The only site allowed to connect with this extension is Cider. The allowed
49+
/// URIs for Cider are set in the externally_connectable field in the manifest.
50+
void handleCiderConnectRequest(Port port) {
51+
if (port.name == _ciderPortName) {
52+
_ciderPort = port;
53+
54+
port.onMessage.addListener(
55+
allowInterop(_handleMessageFromCider),
56+
);
57+
}
58+
}
59+
60+
/// Sends a message to the Cider-connected port.
61+
void sendMessageToCider({
62+
required CiderMessageType messageType,
63+
String? messageBody,
64+
}) {
65+
if (_ciderPort == null) return;
66+
final message = jsonEncode({
67+
'messageType': messageType.name,
68+
'messageBody': messageBody,
69+
});
70+
_ciderPort!.postMessage(message);
71+
}
72+
73+
/// Sends an error message to the Cider-connected port.
74+
void sendErrorMessageToCider({
75+
required CiderErrorType errorType,
76+
String? errorDetails,
77+
}) {
78+
debugWarn('CiderError.${errorType.name} $errorDetails');
79+
if (_ciderPort == null) return;
80+
final message = jsonEncode({
81+
'messageType': CiderMessageType.error.name,
82+
'errorType': errorType.name,
83+
'messageBody': errorDetails,
84+
});
85+
_ciderPort!.postMessage(message);
86+
}
87+
88+
Future<void> _handleMessageFromCider(dynamic message, Port _) async {
89+
if (message is! String) {
90+
sendErrorMessageToCider(
91+
errorType: CiderErrorType.invalidRequest,
92+
errorDetails: 'Expected request to be a string: $message',
93+
);
94+
return;
95+
}
96+
97+
final decoded = jsonDecode(message) as Map<String, dynamic>;
98+
final messageType = decoded['messageType'] as String?;
99+
final messageBody = decoded['messageBody'] as String?;
100+
101+
if (messageType == CiderMessageType.startDebugRequest.name) {
102+
await _startDebugging(workspaceName: messageBody);
103+
} else if (messageType == CiderMessageType.stopDebugRequest.name) {
104+
await _stopDebugging(workspaceName: messageBody);
105+
}
106+
}
107+
108+
Future<void> _startDebugging({String? workspaceName}) async {
109+
if (workspaceName == null) {
110+
_sendNoWorkspaceError();
111+
return;
112+
}
113+
114+
final dartTab = await _findDartTabIdForWorkspace(workspaceName);
115+
if (dartTab != null) {
116+
// TODO(https://github.com/dart-lang/webdev/issues/2198): When debugging
117+
// with Cider, disable debugging with DevTools.
118+
await attachDebugger(dartTab, trigger: Trigger.cider);
119+
}
120+
}
121+
122+
Future<void> _stopDebugging({String? workspaceName}) async {
123+
if (workspaceName == null) {
124+
_sendNoWorkspaceError();
125+
return;
126+
}
127+
128+
final dartTab = await _findDartTabIdForWorkspace(workspaceName);
129+
if (dartTab == null) return;
130+
final successfullyDetached = await detachDebugger(
131+
dartTab,
132+
type: TabType.dartApp,
133+
reason: DetachReason.canceledByUser,
134+
);
135+
136+
if (successfullyDetached) {
137+
sendMessageToCider(messageType: CiderMessageType.stopDebugResponse);
138+
} else {
139+
sendErrorMessageToCider(
140+
errorType: CiderErrorType.internalError,
141+
errorDetails: 'Unable to detach debugger.',
142+
);
143+
}
144+
}
145+
146+
void _sendNoWorkspaceError() {
147+
sendErrorMessageToCider(
148+
errorType: CiderErrorType.noWorkspace,
149+
errorDetails: 'Cannot find a debuggable Dart tab without a workspace',
150+
);
151+
}
152+
153+
Future<int?> _findDartTabIdForWorkspace(String workspaceName) async {
154+
final allTabsInfo = await fetchAllStorageObjectsOfType<DebugInfo>(
155+
type: StorageObject.debugInfo,
156+
);
157+
final dartTabIds = allTabsInfo
158+
.where(
159+
(debugInfo) => debugInfo.workspaceName == workspaceName,
160+
)
161+
.map(
162+
(info) => info.tabId,
163+
)
164+
.toList();
165+
166+
if (dartTabIds.isEmpty) {
167+
sendErrorMessageToCider(
168+
errorType: CiderErrorType.noDartTab,
169+
errorDetails: 'No debuggable Dart tabs found.',
170+
);
171+
return null;
172+
}
173+
if (dartTabIds.length > 1) {
174+
sendErrorMessageToCider(
175+
errorType: CiderErrorType.noDartTab,
176+
errorDetails: 'Too many debuggable Dart tabs found.',
177+
);
178+
return null;
179+
}
180+
final tabId = dartTabIds.first;
181+
if (tabId == null) {
182+
sendErrorMessageToCider(
183+
errorType: CiderErrorType.chromeError,
184+
errorDetails: 'Debuggable Dart tab is null.',
185+
);
186+
return null;
187+
}
188+
189+
return tabId;
190+
}

dwds/debug_extension_mv3/web/debug_session.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import 'package:sse/client/sse_client.dart';
2222
import 'package:web_socket_channel/web_socket_channel.dart';
2323

2424
import 'chrome_api.dart';
25+
import 'cider_connection.dart';
2526
import 'cross_extension_communication.dart';
2627
import 'data_serializers.dart';
2728
import 'data_types.dart';
@@ -79,6 +80,7 @@ enum TabType {
7980

8081
enum Trigger {
8182
angularDartDevTools,
83+
cider,
8284
extensionPanel,
8385
extensionIcon,
8486
}
@@ -408,7 +410,15 @@ void _routeDwdsEvent(String eventData, SocketClient client, int tabId) {
408410
tabId: tabId,
409411
);
410412
if (message.method == 'dwds.devtoolsUri') {
411-
_openDevTools(message.params, dartAppTabId: tabId);
413+
if (_tabIdToTrigger[tabId] != Trigger.cider) {
414+
_openDevTools(message.params, dartAppTabId: tabId);
415+
}
416+
}
417+
if (message.method == 'dwds.plainUri') {
418+
sendMessageToCider(
419+
messageType: CiderMessageType.startDebugResponse,
420+
messageBody: message.params,
421+
);
412422
}
413423
if (message.method == 'dwds.encodedUri') {
414424
setStorageObject(
@@ -774,6 +784,8 @@ DebuggerLocation? _debuggerLocation(int dartAppTabId) {
774784
return DebuggerLocation.angularDartDevTools;
775785
case Trigger.extensionPanel:
776786
return DebuggerLocation.chromeDevTools;
787+
case Trigger.cider:
788+
return DebuggerLocation.ide;
777789
}
778790
}
779791

dwds/debug_extension_mv3/web/manifest_mv2.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@
77
"default_icon": "static_assets/dart_grey.png"
88
},
99
"externally_connectable": {
10-
"ids": ["nbkbficgbembimioedhceniahniffgpl"]
10+
"ids": ["nbkbficgbembimioedhceniahniffgpl"],
11+
"matches": [
12+
"https://cider.corp.google.com/*",
13+
"https://cider-staging.corp.google.com/*",
14+
"https://cider-test.corp.google.com/*",
15+
"https://cider-v.corp.google.com/*",
16+
"https://cider-v-staging.corp.google.com/*",
17+
"https://cider-v-test.corp.google.com/*"
18+
]
1119
},
1220
"permissions": ["debugger", "notifications", "storage", "webNavigation"],
1321
"background": {

dwds/debug_extension_mv3/web/manifest_mv3.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@
77
"default_icon": "static_assets/dart_grey.png"
88
},
99
"externally_connectable": {
10-
"ids": ["nbkbficgbembimioedhceniahniffgpl"]
10+
"ids": ["nbkbficgbembimioedhceniahniffgpl"],
11+
"matches": [
12+
"https://cider.corp.google.com/*",
13+
"https://cider-staging.corp.google.com/*",
14+
"https://cider-test.corp.google.com/*",
15+
"https://cider-v.corp.google.com/*",
16+
"https://cider-v-staging.corp.google.com/*",
17+
"https://cider-v-test.corp.google.com/*"
18+
]
1119
},
1220
"permissions": [
1321
"debugger",

dwds/debug_extension_mv3/web/storage.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,36 @@ Future<T?> fetchStorageObject<T>({required StorageObject type, int? tabId}) {
102102
return completer.future;
103103
}
104104

105+
Future<List<T>> fetchAllStorageObjectsOfType<T>({required StorageObject type}) {
106+
final completer = Completer<List<T>>();
107+
final storageArea = _getStorageArea(type.persistence);
108+
storageArea.get(
109+
null,
110+
allowInterop((Object? storageContents) {
111+
if (storageContents == null) {
112+
debugWarn('No storage objects of type exist.', prefix: type.name);
113+
completer.complete([]);
114+
return;
115+
}
116+
final allKeys = List<String>.from(objectKeys(storageContents));
117+
final storageKeys = allKeys.where((key) => key.contains(type.name));
118+
final result = <T>[];
119+
for (final key in storageKeys) {
120+
final json = getProperty(storageContents, key) as String?;
121+
if (json != null) {
122+
if (T == String) {
123+
result.add(json as T);
124+
} else {
125+
result.add(serializers.deserialize(jsonDecode(json)) as T);
126+
}
127+
}
128+
}
129+
completer.complete(result);
130+
}),
131+
);
132+
return completer.future;
133+
}
134+
105135
Future<bool> removeStorageObject<T>({required StorageObject type, int? tabId}) {
106136
final storageKey = _createStorageKey(type, tabId);
107137
final completer = Completer<bool>();

0 commit comments

Comments
 (0)