| // Copyright (c) 2023, 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 'dart:convert'; |
| // TODO: https://github.com/dart-lang/webdev/issues/2508 |
| // ignore: deprecated_member_use |
| import 'dart:js_util'; |
| |
| // TODO: https://github.com/dart-lang/webdev/issues/2508 |
| // ignore: deprecated_member_use |
| import 'package:js/js.dart'; |
| |
| import 'chrome_api.dart'; |
| import 'debug_session.dart'; |
| import 'logger.dart'; |
| import 'storage.dart'; |
| import 'utils.dart'; |
| |
| /// Used to identify messages passed to/from Cider. |
| /// |
| /// This must match the key defined in the Cider extension. |
| const _ciderDartMessageKey = 'CIDER_DART'; |
| |
| /// Defines the message types that can be passed to/from Cider. |
| /// |
| /// The types must match those defined by ChromeExtensionMessageType in the |
| /// Cider extension. |
| enum CiderMessageType { |
| connected, |
| error, |
| inspectorUrlResponse, |
| inspectorUrlRequest, |
| ping, |
| pong, |
| startDebugResponse, |
| startDebugRequest, |
| stopDebugResponse, |
| stopDebugRequest, |
| } |
| |
| /// Defines the error types that can be sent to Cider. |
| /// |
| /// The types must match those defined by ChromeExtensionErrorType in the |
| /// Cider extension. |
| enum CiderErrorType { internalError, invalidRequest, noAppId } |
| |
| const _ciderPortName = 'cider'; |
| Port? _ciderPort; |
| |
| /// Handles a connect request from Cider. |
| /// |
| /// The only site allowed to connect with this extension is Cider. The allowed |
| /// URIs for Cider are set in the externally_connectable field in the manifest. |
| void handleCiderConnectRequest(Port port) { |
| if (port.name == _ciderPortName) { |
| debugLog('Received connect request from Cider', verbose: true); |
| _ciderPort = port; |
| |
| port.onMessage.addListener(allowInterop(_handleMessageFromCider)); |
| |
| sendMessageToCider(messageType: CiderMessageType.connected); |
| } |
| } |
| |
| /// Sends a message to the Cider-connected port. |
| void sendMessageToCider({ |
| required CiderMessageType messageType, |
| String? messageBody, |
| }) { |
| if (_ciderPort == null) return; |
| final message = jsonEncode({ |
| 'messageType': messageType.name, |
| 'messageBody': messageBody, |
| }); |
| _sendMessageToCider(message); |
| } |
| |
| /// Sends an error message to the Cider-connected port. |
| void sendErrorMessageToCider({ |
| required CiderErrorType errorType, |
| String? errorDetails, |
| }) { |
| debugWarn('CiderError.${errorType.name} $errorDetails'); |
| if (_ciderPort == null) return; |
| final message = jsonEncode({ |
| 'messageType': CiderMessageType.error.name, |
| 'errorType': errorType.name, |
| 'messageBody': errorDetails, |
| }); |
| _sendMessageToCider(message); |
| } |
| |
| void _sendMessageToCider(String json) { |
| final message = {'key': _ciderDartMessageKey, 'json': json}; |
| _ciderPort!.postMessage(jsify(message)); |
| } |
| |
| Future<void> _handleMessageFromCider(dynamic message, Port _) async { |
| final key = getProperty(message, 'key'); |
| final json = getProperty(message, 'json'); |
| if (key != _ciderDartMessageKey || json is! String) { |
| sendErrorMessageToCider( |
| errorType: CiderErrorType.invalidRequest, |
| errorDetails: 'Invalid message format: $message', |
| ); |
| return; |
| } |
| |
| final decoded = jsonDecode(json) as Map<String, dynamic>; |
| final messageType = decoded['messageType'] as String?; |
| final messageBody = decoded['messageBody'] as String?; |
| |
| if (messageType == CiderMessageType.startDebugRequest.name) { |
| await _startDebugging(appId: messageBody); |
| } else if (messageType == CiderMessageType.stopDebugRequest.name) { |
| await _stopDebugging(appId: messageBody); |
| } else if (messageType == CiderMessageType.inspectorUrlRequest.name) { |
| await _sendInspectorUrl(appId: messageBody); |
| } else if (messageType == CiderMessageType.ping.name) { |
| sendMessageToCider(messageType: CiderMessageType.pong); |
| } |
| } |
| |
| Future<void> _startDebugging({String? appId}) async { |
| if (appId == null) { |
| _sendNoAppIdError(); |
| return; |
| } |
| final tabId = _tabId(appId); |
| // TODO(https://github.com/dart-lang/webdev/issues/2198): When debugging |
| // with Cider, disable debugging with DevTools. |
| debugLog('Attach debugger to Cider', verbose: true); |
| await attachDebugger(tabId, trigger: Trigger.cider); |
| } |
| |
| Future<void> _stopDebugging({String? appId}) async { |
| if (appId == null) { |
| _sendNoAppIdError(); |
| return; |
| } |
| final tabId = _tabId(appId); |
| |
| final successfullyDetached = await detachDebugger( |
| tabId, |
| type: TabType.dartApp, |
| reason: DetachReason.canceledByUser, |
| ); |
| |
| if (successfullyDetached) { |
| sendMessageToCider(messageType: CiderMessageType.stopDebugResponse); |
| } else { |
| sendErrorMessageToCider( |
| errorType: CiderErrorType.internalError, |
| errorDetails: 'Unable to detach debugger.', |
| ); |
| } |
| } |
| |
| Future<void> _sendInspectorUrl({String? appId}) async { |
| if (appId == null) { |
| _sendNoAppIdError(); |
| return; |
| } |
| final tabId = _tabId(appId); |
| final alreadyDebugging = isActiveDebugSession(tabId); |
| if (!alreadyDebugging) { |
| sendErrorMessageToCider( |
| errorType: CiderErrorType.invalidRequest, |
| errorDetails: |
| 'Cannot send the inspector URL before ' |
| 'the debugger has been attached.', |
| ); |
| return; |
| } |
| final devToolsUri = await fetchStorageObject<String>( |
| type: StorageObject.devToolsUri, |
| tabId: tabId, |
| ); |
| if (devToolsUri == null) { |
| sendErrorMessageToCider( |
| errorType: CiderErrorType.internalError, |
| errorDetails: 'Failed to fetch the DevTools URI for the inspector.', |
| ); |
| return; |
| } |
| final inspectorUrl = addQueryParameters( |
| devToolsUri, |
| queryParameters: {'embed': 'true', 'page': 'inspector'}, |
| ); |
| sendMessageToCider( |
| messageType: CiderMessageType.inspectorUrlResponse, |
| messageBody: inspectorUrl, |
| ); |
| } |
| |
| int _tabId(String appId) { |
| // The suffix "-f" is used to tell Cider that this is a Flutter app. |
| if (appId.endsWith('-f')) { |
| appId = appId.substring(0, appId.length - 2); |
| } |
| final tabId = appId.split('-').last; |
| return int.parse(tabId); |
| } |
| |
| void _sendNoAppIdError() { |
| sendErrorMessageToCider( |
| errorType: CiderErrorType.noAppId, |
| errorDetails: 'Cannot find a debuggable Dart tab without an app ID', |
| ); |
| } |