blob: 88a46adaf034cd03ea8708d34bb654ad9a0651e3 [file] [log] [blame]
// 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',
);
}