diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 1e45a48cd..ddfa389ea 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,6 +1,10 @@ +## 26.2.1-wip + +- Add support for DDS APIs and serving Dart DevTools when no Chrome Debugger is available. + ## 26.2.0 -- Add support for more service APIs over websocket connections with no Chrome Debugger available. +- Add support for more service APIs over websocket connections when no Chrome Debugger is available. ## 26.1.0 diff --git a/dwds/lib/dwds.dart b/dwds/lib/dwds.dart index 5610ceb0e..a11ae81be 100644 --- a/dwds/lib/dwds.dart +++ b/dwds/lib/dwds.dart @@ -34,7 +34,8 @@ export 'src/readers/frontend_server_asset_reader.dart' show FrontendServerAssetReader; export 'src/readers/proxy_server_asset_reader.dart' show ProxyServerAssetReader; export 'src/servers/devtools.dart'; -export 'src/services/chrome_debug_exception.dart' show ChromeDebugException; +export 'src/services/chrome/chrome_debug_exception.dart' + show ChromeDebugException; export 'src/services/expression_compiler.dart' show ExpressionCompilationResult, diff --git a/dwds/lib/src/connections/debug_connection.dart b/dwds/lib/src/connections/debug_connection.dart index 01a9223e1..cdf315138 100644 --- a/dwds/lib/src/connections/debug_connection.dart +++ b/dwds/lib/src/connections/debug_connection.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'package:dwds/src/services/app_debug_services.dart'; -import 'package:dwds/src/services/chrome_proxy_service.dart'; +import 'package:dwds/src/services/chrome/chrome_proxy_service.dart'; import 'package:vm_service/vm_service.dart'; /// A debug connection between the application in the browser and DWDS. diff --git a/dwds/lib/src/debugging/classes.dart b/dwds/lib/src/debugging/classes.dart index 9eaf39de1..eee4fdebf 100644 --- a/dwds/lib/src/debugging/classes.dart +++ b/dwds/lib/src/debugging/classes.dart @@ -5,7 +5,7 @@ import 'package:dwds/src/config/tool_configuration.dart'; import 'package:dwds/src/debugging/chrome_inspector.dart'; import 'package:dwds/src/debugging/metadata/class.dart'; -import 'package:dwds/src/services/chrome_debug_exception.dart'; +import 'package:dwds/src/services/chrome/chrome_debug_exception.dart'; import 'package:dwds/src/utilities/shared.dart'; import 'package:vm_service/vm_service.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; diff --git a/dwds/lib/src/debugging/debugger.dart b/dwds/lib/src/debugging/debugger.dart index 832727e64..38e063e32 100644 --- a/dwds/lib/src/debugging/debugger.dart +++ b/dwds/lib/src/debugging/debugger.dart @@ -12,7 +12,7 @@ import 'package:dwds/src/debugging/inspector.dart'; import 'package:dwds/src/debugging/location.dart'; import 'package:dwds/src/debugging/remote_debugger.dart'; import 'package:dwds/src/debugging/skip_list.dart'; -import 'package:dwds/src/services/chrome_debug_exception.dart'; +import 'package:dwds/src/services/chrome/chrome_debug_exception.dart'; import 'package:dwds/src/utilities/dart_uri.dart'; import 'package:dwds/src/utilities/domain.dart'; import 'package:dwds/src/utilities/objects.dart' show Property; diff --git a/dwds/lib/src/debugging/libraries.dart b/dwds/lib/src/debugging/libraries.dart index a4ce5a080..d551f77db 100644 --- a/dwds/lib/src/debugging/libraries.dart +++ b/dwds/lib/src/debugging/libraries.dart @@ -8,7 +8,7 @@ import 'package:dwds/src/debugging/chrome_inspector.dart'; import 'package:dwds/src/debugging/inspector.dart'; import 'package:dwds/src/debugging/metadata/class.dart'; import 'package:dwds/src/debugging/metadata/provider.dart'; -import 'package:dwds/src/services/chrome_debug_exception.dart'; +import 'package:dwds/src/services/chrome/chrome_debug_exception.dart'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:vm_service/vm_service.dart'; diff --git a/dwds/lib/src/debugging/metadata/class.dart b/dwds/lib/src/debugging/metadata/class.dart index 2e336271d..79037e683 100644 --- a/dwds/lib/src/debugging/metadata/class.dart +++ b/dwds/lib/src/debugging/metadata/class.dart @@ -4,7 +4,7 @@ import 'package:dwds/src/config/tool_configuration.dart'; import 'package:dwds/src/debugging/chrome_inspector.dart'; -import 'package:dwds/src/services/chrome_debug_exception.dart'; +import 'package:dwds/src/services/chrome/chrome_debug_exception.dart'; import 'package:logging/logging.dart'; import 'package:vm_service/vm_service.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; diff --git a/dwds/lib/src/debugging/web_socket_inspector.dart b/dwds/lib/src/debugging/web_socket_inspector.dart index d7c741296..5b5a6f01b 100644 --- a/dwds/lib/src/debugging/web_socket_inspector.dart +++ b/dwds/lib/src/debugging/web_socket_inspector.dart @@ -5,7 +5,7 @@ import 'package:dwds/src/connections/app_connection.dart'; import 'package:dwds/src/debugging/inspector.dart'; import 'package:dwds/src/debugging/libraries.dart'; -import 'package:dwds/src/services/web_socket_proxy_service.dart'; +import 'package:dwds/src/services/web_socket/web_socket_proxy_service.dart'; import 'package:dwds/src/utilities/shared.dart'; import 'package:vm_service/vm_service.dart'; @@ -38,6 +38,7 @@ class WebSocketAppInspector extends AppInspector { breakpoints: [], isSystemIsolate: false, isolateFlags: [], + extensionRPCs: [], ); final inspector = WebSocketAppInspector._( appConnection, diff --git a/dwds/lib/src/dwds_vm_client.dart b/dwds/lib/src/dwds_vm_client.dart index 72549e57f..a6426cbd1 100644 --- a/dwds/lib/src/dwds_vm_client.dart +++ b/dwds/lib/src/dwds_vm_client.dart @@ -8,13 +8,16 @@ import 'dart:convert'; import 'package:dwds/src/config/tool_configuration.dart'; import 'package:dwds/src/events.dart'; import 'package:dwds/src/loaders/ddc_library_bundle.dart'; -import 'package:dwds/src/services/chrome_debug_exception.dart'; -import 'package:dwds/src/services/chrome_proxy_service.dart'; +import 'package:dwds/src/services/chrome/chrome_debug_exception.dart'; +import 'package:dwds/src/services/chrome/chrome_debug_service.dart'; +import 'package:dwds/src/services/chrome/chrome_proxy_service.dart'; import 'package:dwds/src/services/debug_service.dart'; import 'package:dwds/src/services/proxy_service.dart'; -import 'package:dwds/src/services/web_socket_proxy_service.dart'; +import 'package:dwds/src/services/web_socket/web_socket_debug_service.dart'; +import 'package:dwds/src/services/web_socket/web_socket_proxy_service.dart'; import 'package:dwds/src/utilities/synchronized.dart'; import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; import 'package:uuid/uuid.dart'; import 'package:vm_service/vm_service.dart'; import 'package:vm_service/vm_service_io.dart'; @@ -41,104 +44,57 @@ enum _NamespacedServiceExtension { } /// Common interface for DWDS VM clients. -abstract class DwdsVmClient { - /// The VM service client. - VmService get client; +abstract base class DwdsVmClient< + T extends ProxyService, + U extends DebugService +> { + late final VmService client; + final _requestController = StreamController(); + final _responseController = StreamController(); + late final _responseSink = _responseController.sink; + late final _responseStream = _responseController.stream.asBroadcastStream(); + late final _requestSink = _requestController.sink; + late final _requestStream = _requestController.stream; - /// Closes the VM client and releases resources. - Future close(); -} + Logger get logger; -// Chrome-based DWDS VM client logger. -final _chromeLogger = Logger('DwdsVmClient'); + DwdsVmClient({required this.debugService}); -// A client of the vm service that registers some custom extensions like -// hotRestart. -class ChromeDwdsVmClient implements DwdsVmClient { - @override - final VmService client; - final StreamController> _requestController; - final StreamController> _responseController; + final U debugService; - /// Null until [close] is called. - /// - /// All subsequent calls to [close] will return this future. Future? _closed; - /// Synchronizes hot restarts to avoid races. - final _hotRestartQueue = AtomicQueue(); - - ChromeDwdsVmClient( - this.client, - this._requestController, - this._responseController, - ); - - @override + /// Closes the VM client and releases resources. Future close() => _closed ??= () async { await _requestController.close(); await _responseController.close(); await client.dispose(); }(); - static Future create( - ChromeDebugService debugService, - DwdsStats dwdsStats, - Uri? ddsUri, - ) async { - final chromeProxyService = debugService.chromeProxyService; - final responseController = StreamController(); - final responseSink = responseController.sink; - // Response stream must be a broadcast stream so that it can have multiple - // listeners: - final responseStream = responseController.stream.asBroadcastStream(); - final requestController = StreamController(); - final requestSink = requestController.sink; - final requestStream = requestController.stream; - - final clientCompleter = Completer(); - - _setUpVmServerConnection( - chromeProxyService: chromeProxyService, + @mustCallSuper + Future initialize({required Uri? ddsUri}) async { + setUpVmServerConnection( + proxyService: debugService.proxyService, debugService: debugService, - responseStream: responseStream, - responseSink: responseSink, - requestStream: requestStream, - requestSink: requestSink, - dwdsStats: dwdsStats, - clientFuture: clientCompleter.future, + responseStream: _responseStream, + responseSink: _responseSink, + requestStream: _requestStream, + requestSink: _requestSink, ); - final client = ddsUri == null - ? _setUpVmClient( - responseStream: responseStream, - requestController: requestController, - requestSink: requestSink, - ) + client = ddsUri == null + ? _setUpVmClient() : await _setUpDdsClient(ddsUri: ddsUri); - if (!clientCompleter.isCompleted) { - clientCompleter.complete(client); - } - - final dwdsVmClient = ChromeDwdsVmClient( - client, - requestController, - responseController, - ); - - await _registerServiceExtensions( + await registerServiceExtensions( client: client, - chromeProxyService: chromeProxyService, - dwdsVmClient: dwdsVmClient, + proxyService: debugService.proxyService, ); - - return dwdsVmClient; } /// Establishes a VM service client that is connected via DDS and registers /// the service extensions on that client. - static Future _setUpDdsClient({required Uri ddsUri}) async { + Future _setUpDdsClient({required Uri ddsUri}) async { final client = await vmServiceConnectUri(ddsUri.toString()); return client; } @@ -147,22 +103,16 @@ class ChromeDwdsVmClient implements DwdsVmClient { /// extensions on that client. /// /// Note: This is only used in the rare cases where DDS is disabled. - static VmService _setUpVmClient({ - required Stream responseStream, - required StreamSink requestSink, - required StreamController requestController, - }) { - final client = VmService(responseStream.map(jsonEncode), (request) { - if (requestController.isClosed) { - _chromeLogger.warning( - 'Attempted to send a request but the connection is closed:\n\n' - '$request', + VmService _setUpVmClient() { + final client = VmService(_responseStream.map(jsonEncode), (request) { + if (_requestController.isClosed) { + logger.warning( + 'Attempted to send a request but the connection is closed:\n\n$request', ); return; } - requestSink.add(Map.from(jsonDecode(request))); + _requestSink.add(Map.from(jsonDecode(request))); }); - return client; } @@ -176,20 +126,20 @@ class ChromeDwdsVmClient implements DwdsVmClient { /// should register all Flutter service extensions. However, to do so we will /// need to implement the missing isolate-related dart:developer APIs so that /// the engine has access to this information. - static void _setUpVmServerConnection({ - required ChromeProxyService chromeProxyService, - required DwdsStats dwdsStats, - required ChromeDebugService debugService, + void setUpVmServerConnection({ + required T proxyService, + required DebugService debugService, required Stream responseStream, required StreamSink responseSink, required Stream requestStream, required StreamSink requestSink, - required Future clientFuture, + DwdsStats? dwdsStats, + Future? clientFuture, }) { responseStream.listen((request) async { final response = await _maybeHandleServiceExtensionRequest( request, - chromeProxyService: chromeProxyService, + proxyService: proxyService, dwdsStats: dwdsStats, clientFuture: clientFuture, ); @@ -202,10 +152,12 @@ class ChromeDwdsVmClient implements DwdsVmClient { requestStream, responseSink, debugService.serviceExtensionRegistry, - debugService.chromeProxyService, + proxyService, ); + // Register service extensions for (final extension in _NamespacedServiceExtension.values) { + logger.finest('Registering service extension: ${extension.method}'); debugService.serviceExtensionRegistry.registerExtension( extension.method, vmServerConnection, @@ -213,33 +165,18 @@ class ChromeDwdsVmClient implements DwdsVmClient { } } - static Future _maybeHandleServiceExtensionRequest( + Future _maybeHandleServiceExtensionRequest( VmResponse request, { - required ChromeProxyService chromeProxyService, - required DwdsStats dwdsStats, - required Future clientFuture, + required T proxyService, + required DwdsStats? dwdsStats, + required Future? clientFuture, }) async { - VmRequest? response; - final method = request['method']; - if (method == _NamespacedServiceExtension.flutterListViews.method) { - response = await flutterListViewsHandler(chromeProxyService); - } else if (method == _NamespacedServiceExtension.extDwdsEmitEvent.method) { - response = extDwdsEmitEventHandler(request, _chromeLogger); - } else if (method == _NamespacedServiceExtension.extDwdsReload.method) { - response = await _extDwdsReloadHandler(chromeProxyService); - } else if (method == _NamespacedServiceExtension.extDwdsRestart.method) { - final client = await clientFuture; - response = await _extDwdsRestartHandler(chromeProxyService, client); - } else if (method == _NamespacedServiceExtension.extDwdsSendEvent.method) { - response = await extDwdsSendEventHandler( - request, - dwdsStats, - _chromeLogger, - ); - } else if (method == _NamespacedServiceExtension.extDwdsScreenshot.method) { - response = await _extDwdsScreenshotHandler(chromeProxyService); - } - + final response = await maybeHandleServiceExtensionRequestImpl( + request, + proxyService: proxyService, + dwdsStats: dwdsStats, + clientFuture: clientFuture, + ); if (response != null) { response['id'] = request['id'] as String; // This is necessary even though DWDS doesn't use package:json_rpc_2. @@ -247,44 +184,153 @@ class ChromeDwdsVmClient implements DwdsVmClient { // https://github.com/dart-lang/json_rpc_2/blob/639857be892050159f5164c749d7947694976a4a/lib/src/server.dart#L252 response['jsonrpc'] = '2.0'; } - return response; } - static Future> _extDwdsScreenshotHandler( - ChromeProxyService chromeProxyService, + @mustBeOverridden + Future maybeHandleServiceExtensionRequestImpl( + VmResponse request, { + required T proxyService, + DwdsStats? dwdsStats, + Future? clientFuture, + }); + + @mustBeOverridden + Future registerServiceExtensions({ + required VmService client, + required T proxyService, + }); + + /// Shared handler for DWDS send event service extension. + Future> extDwdsSendEventHandler( + VmResponse request, + DwdsStats? dwdsStats, + Logger logger, ) async { - await chromeProxyService.remoteDebugger.enablePage(); - final response = await chromeProxyService.remoteDebugger.sendCommand( - 'Page.captureScreenshot', - ); - return {'result': response.result as Object}; + logger.fine('SendEvent: $request'); + if (dwdsStats != null) { + _processSendEvent(request, dwdsStats); + } + return {'result': Success().toJson()}; } - static Future> _extDwdsReloadHandler( - ChromeProxyService chromeProxyService, - ) async { - await _fullReload(chromeProxyService); + void _processSendEvent(Map request, DwdsStats dwdsStats) { + final event = request['params'] as Map?; + if (event == null) return; + final type = event['type'] as String?; + final payload = event['payload'] as Map?; + switch (type) { + case 'DevtoolsEvent': + { + logger.finest('Received DevTools event: $event'); + final action = payload?['action'] as String?; + final screen = payload?['screen'] as String?; + if (screen != null && action == 'pageReady') { + _recordDwdsStats(dwdsStats, screen); + } else { + logger.finest('Ignoring unknown event: $event'); + } + } + } + } + + void _recordDwdsStats(DwdsStats dwdsStats, String screen) { + if (dwdsStats.isFirstDebuggerReady) { + final devToolsStart = dwdsStats.devToolsStart; + final debuggerStart = dwdsStats.debuggerStart; + if (devToolsStart != null) { + final devToolLoadTime = DateTime.now() + .difference(devToolsStart) + .inMilliseconds; + emitEvent(DwdsEvent.devToolsLoad(devToolLoadTime, screen)); + logger.fine('DevTools load time: $devToolLoadTime ms'); + } + if (debuggerStart != null) { + final debuggerReadyTime = DateTime.now() + .difference(debuggerStart) + .inMilliseconds; + emitEvent(DwdsEvent.debuggerReady(debuggerReadyTime, screen)); + logger.fine('Debugger ready time: $debuggerReadyTime ms'); + } + } else { + logger.finest('Debugger and DevTools stats are already recorded.'); + } + } + + /// Shared handler for DWDS emit event service extension. + Map extDwdsEmitEventHandler( + VmResponse request, + Logger logger, + ) { + final event = request['params'] as Map?; + if (event != null) { + final type = event['type'] as String?; + final payload = event['payload'] as Map?; + if (type != null && payload != null) { + logger.fine('EmitEvent: $type $payload'); + emitEvent(DwdsEvent(type, payload)); + } + } return {'result': Success().toJson()}; } - static Future> _extDwdsRestartHandler( - ChromeProxyService chromeProxyService, - VmService client, + /// Shared handler for Flutter list views service extension. + Future> flutterListViewsHandler( + ProxyService proxyService, ) async { - await _hotRestart(chromeProxyService, client); - return {'result': Success().toJson()}; + final vm = await proxyService.getVM(); + final isolates = vm.isolates; + return { + 'result': { + 'views': [ + for (final isolate in isolates ?? []) + {'id': isolate.id, 'isolate': isolate.toJson()}, + ], + }, + }; + } +} + +// A client of the vm service that registers some custom extensions like +// hotRestart. +final class ChromeDwdsVmClient + extends DwdsVmClient { + /// Synchronizes hot restarts to avoid races. + final _hotRestartQueue = AtomicQueue(); + + final _clientCompleter = Completer(); + + @override + final logger = Logger('DwdsVmClient'); + + ChromeDwdsVmClient({required super.debugService}); + + static Future create( + ChromeDebugService debugService, + Uri? ddsUri, + ) async { + final dwdsVmClient = ChromeDwdsVmClient(debugService: debugService); + await dwdsVmClient.initialize(ddsUri: ddsUri); + return dwdsVmClient; } - static Future _registerServiceExtensions({ + @override + Future initialize({required Uri? ddsUri}) async { + await super.initialize(ddsUri: ddsUri); + if (!_clientCompleter.isCompleted) { + _clientCompleter.complete(client); + } + } + + @override + Future registerServiceExtensions({ required VmService client, - required ChromeProxyService chromeProxyService, - required ChromeDwdsVmClient dwdsVmClient, + required ChromeProxyService proxyService, }) async { client.registerServiceCallback( 'hotRestart', (request) => captureElapsedTime( - () => dwdsVmClient.hotRestart(chromeProxyService, client), + () => hotRestart(proxyService, client), (_) => DwdsEvent.hotRestart(), ), ); @@ -293,458 +339,323 @@ class ChromeDwdsVmClient implements DwdsVmClient { client.registerServiceCallback( 'fullReload', (request) => captureElapsedTime( - () => _fullReload(chromeProxyService), + () => _fullReload(proxyService), (_) => DwdsEvent.fullReload(), ), ); await client.registerService('fullReload', 'DWDS'); } - Future> hotRestart( - ChromeProxyService chromeProxyService, - VmService client, - ) { - return _hotRestartQueue.run(() => _hotRestart(chromeProxyService, client)); - } -} - -// WebSocket-based DWDS VM client logger. -final _webSocketLogger = Logger('WebSocketDwdsVmClient'); - -/// WebSocket-based DWDS VM client. -class WebSocketDwdsVmClient implements DwdsVmClient { - @override - final VmService client; - final StreamController _requestController; - final StreamController _responseController; - Future? _closed; - - WebSocketDwdsVmClient( - this.client, - this._requestController, - this._responseController, - ); - @override - Future close() => _closed ??= () async { - await _requestController.close(); - await _responseController.close(); - await client.dispose(); - }(); - - static Future create( - WebSocketDebugService debugService, - ) async { - _webSocketLogger.fine('Creating WebSocket DWDS VM client'); - final webSocketProxyService = debugService.webSocketProxyService; - final responseController = StreamController(); - final responseSink = responseController.sink; - final responseStream = responseController.stream.asBroadcastStream(); - final requestController = StreamController(); - final requestSink = requestController.sink; - final requestStream = requestController.stream; - - _setUpWebSocketVmServerConnection( - webSocketProxyService: webSocketProxyService, - debugService: debugService, - responseStream: responseStream, - responseSink: responseSink, - requestStream: requestStream, - requestSink: requestSink, - ); - - final client = _setUpWebSocketVmClient( - responseStream: responseStream, - requestController: requestController, - requestSink: requestSink, - ); - - await _registerServiceExtensions( - client: client, - webSocketProxyService: webSocketProxyService, - ); - - _webSocketLogger.fine('WebSocket DWDS VM client created successfully'); - return WebSocketDwdsVmClient(client, requestController, responseController); - } - - static VmService _setUpWebSocketVmClient({ - required Stream responseStream, - required StreamSink requestSink, - required StreamController requestController, - }) { - final client = VmService(responseStream.map(jsonEncode), (request) { - if (requestController.isClosed) { - _webSocketLogger.warning( - 'Attempted to send a request but the connection is closed:\n\n$request', - ); - return; - } - requestSink.add(Map.from(jsonDecode(request))); - }); - return client; - } - - static void _setUpWebSocketVmServerConnection({ - required WebSocketProxyService webSocketProxyService, - required WebSocketDebugService debugService, - required Stream responseStream, - required StreamSink responseSink, - required Stream requestStream, - required StreamSink requestSink, - }) { - responseStream.listen((request) async { - final response = await _maybeHandleWebSocketServiceExtensionRequest( - request, - webSocketProxyService: webSocketProxyService, - ); - if (response != null) { - requestSink.add(response); - } - }); - - final vmServerConnection = VmServerConnection( - requestStream, - responseSink, - debugService.serviceExtensionRegistry, - webSocketProxyService, - ); - - // Register service extensions - for (final extension in _NamespacedServiceExtension.values) { - _webSocketLogger.finest( - 'Registering service extension: ${extension.method}', - ); - debugService.serviceExtensionRegistry.registerExtension( - extension.method, - vmServerConnection, - ); - } - } - - static Future _maybeHandleWebSocketServiceExtensionRequest( + Future maybeHandleServiceExtensionRequestImpl( VmResponse request, { - required WebSocketProxyService webSocketProxyService, + required ChromeProxyService proxyService, + DwdsStats? dwdsStats, + Future? clientFuture, }) async { VmRequest? response; final method = request['method']; - - _webSocketLogger.finest('Processing service extension method: $method'); - if (method == _NamespacedServiceExtension.flutterListViews.method) { - response = await flutterListViewsHandler(webSocketProxyService); + response = await flutterListViewsHandler(proxyService); } else if (method == _NamespacedServiceExtension.extDwdsEmitEvent.method) { - response = extDwdsEmitEventHandler(request, _webSocketLogger); + response = extDwdsEmitEventHandler(request, logger); } else if (method == _NamespacedServiceExtension.extDwdsReload.method) { - response = {'result': 'Reload not implemented'}; + response = await _extDwdsReloadHandler(proxyService); + } else if (method == _NamespacedServiceExtension.extDwdsRestart.method) { + final client = await clientFuture; + response = await _extDwdsRestartHandler(proxyService, client!); } else if (method == _NamespacedServiceExtension.extDwdsSendEvent.method) { - response = await extDwdsSendEventHandler(request, null, _webSocketLogger); + response = await extDwdsSendEventHandler(request, dwdsStats, logger); } else if (method == _NamespacedServiceExtension.extDwdsScreenshot.method) { - response = {'result': 'Screenshot not implemented'}; - } - - if (response != null) { - response['id'] = request['id'] as String; - response['jsonrpc'] = '2.0'; + response = await _extDwdsScreenshotHandler(proxyService); } return response; } - static Future _registerServiceExtensions({ - required VmService client, - required WebSocketProxyService webSocketProxyService, - }) async { - client.registerServiceCallback( - 'hotRestart', - (request) => captureElapsedTime( - () => webSocketProxyService.hotRestart(), - (_) => DwdsEvent.hotRestart(), - ), + static Future> _extDwdsScreenshotHandler( + ChromeProxyService chromeProxyService, + ) async { + await chromeProxyService.remoteDebugger.enablePage(); + final response = await chromeProxyService.remoteDebugger.sendCommand( + 'Page.captureScreenshot', ); - await client.registerService('hotRestart', 'DWDS'); + return {'result': response.result as Object}; } -} -/// Shared handler for Flutter list views service extension. -Future> flutterListViewsHandler( - ProxyService proxyService, -) async { - final vm = await proxyService.getVM(); - final isolates = vm.isolates; - return { - 'result': { - 'views': [ - for (final isolate in isolates ?? []) - {'id': isolate.id, 'isolate': isolate.toJson()}, - ], - }, - }; -} - -/// Shared handler for DWDS emit event service extension. -Map extDwdsEmitEventHandler(VmResponse request, Logger logger) { - final event = request['params'] as Map?; - if (event != null) { - final type = event['type'] as String?; - final payload = event['payload'] as Map?; - if (type != null && payload != null) { - logger.fine('EmitEvent: $type $payload'); - emitEvent(DwdsEvent(type, payload)); - } + Future> _extDwdsReloadHandler( + ChromeProxyService chromeProxyService, + ) async { + await _fullReload(chromeProxyService); + return {'result': Success().toJson()}; } - return {'result': Success().toJson()}; -} -/// Shared handler for DWDS send event service extension. -Future> extDwdsSendEventHandler( - VmResponse request, - DwdsStats? dwdsStats, - Logger logger, -) async { - logger.fine('SendEvent: $request'); - if (dwdsStats != null) { - _processSendEvent(request, dwdsStats); + Future> _extDwdsRestartHandler( + ChromeProxyService chromeProxyService, + VmService client, + ) async { + await _hotRestart(chromeProxyService, client); + return {'result': Success().toJson()}; } - return {'result': Success().toJson()}; -} -void _processSendEvent(Map request, DwdsStats dwdsStats) { - final event = request['params'] as Map?; - if (event == null) return; - final type = event['type'] as String?; - final payload = event['payload'] as Map?; - switch (type) { - case 'DevtoolsEvent': - { - _chromeLogger.finest('Received DevTools event: $event'); - final action = payload?['action'] as String?; - final screen = payload?['screen'] as String?; - if (screen != null && action == 'pageReady') { - _recordDwdsStats(dwdsStats, screen); - } else { - _chromeLogger.finest('Ignoring unknown event: $event'); - } - } + Future> hotRestart( + ChromeProxyService chromeProxyService, + VmService client, + ) { + return _hotRestartQueue.run(() => _hotRestart(chromeProxyService, client)); } -} -void _recordDwdsStats(DwdsStats dwdsStats, String screen) { - if (dwdsStats.isFirstDebuggerReady) { - final devToolsStart = dwdsStats.devToolsStart; - final debuggerStart = dwdsStats.debuggerStart; - if (devToolsStart != null) { - final devToolLoadTime = DateTime.now() - .difference(devToolsStart) - .inMilliseconds; - emitEvent(DwdsEvent.devToolsLoad(devToolLoadTime, screen)); - _chromeLogger.fine('DevTools load time: $devToolLoadTime ms'); - } - if (debuggerStart != null) { - final debuggerReadyTime = DateTime.now() - .difference(debuggerStart) - .inMilliseconds; - emitEvent(DwdsEvent.debuggerReady(debuggerReadyTime, screen)); - _chromeLogger.fine('Debugger ready time: $debuggerReadyTime ms'); + Future tryGetContextId( + ChromeProxyService chromeProxyService, { + int retries = 3, + }) async { + const waitInMs = 50; + for (var retry = 0; retry < retries; retry++) { + final tryId = await chromeProxyService.executionContext.id; + if (tryId != null) return tryId; + await Future.delayed(const Duration(milliseconds: waitInMs)); } - } else { - _chromeLogger.finest('Debugger and DevTools stats are already recorded.'); - } -} - -Future tryGetContextId( - ChromeProxyService chromeProxyService, { - int retries = 3, -}) async { - const waitInMs = 50; - for (var retry = 0; retry < retries; retry++) { - final tryId = await chromeProxyService.executionContext.id; - if (tryId != null) return tryId; - await Future.delayed(const Duration(milliseconds: waitInMs)); + throw StateError('No context with the running Dart application.'); } - throw StateError('No context with the running Dart application.'); -} -Future> _hotRestart( - ChromeProxyService chromeProxyService, - VmService client, -) async { - _chromeLogger.info('Attempting a hot restart'); - - chromeProxyService.terminatingIsolates = true; - await _disableBreakpointsAndResume(client, chromeProxyService); - try { - _chromeLogger.info('Attempting to get execution context ID.'); - await tryGetContextId(chromeProxyService); - _chromeLogger.info('Got execution context ID.'); - } on StateError catch (e) { - // We couldn't find the execution context. `hotRestart` may have been - // triggered in the middle of a full reload. - return { - 'error': {'code': RPCErrorKind.kInternalError.code, 'message': e.message}, - }; - } - // Start listening for isolate create events before issuing a hot - // restart. Only return success after the isolate has fully started. - final stream = chromeProxyService.onEvent('Isolate'); - final waitForIsolateStarted = stream.firstWhere( - (event) => event.kind == EventKind.kIsolateStart, - ); - try { - // If we should pause isolates on start, then only run main once we get a - // resume event. - final pauseIsolatesOnStart = chromeProxyService.pauseIsolatesOnStart; - if (pauseIsolatesOnStart) { - _waitForResumeEventToRunMain(chromeProxyService); - } - // Generate run id to hot restart all apps loaded into the tab. - final runId = const Uuid().v4().toString(); - - // When using the DDC library bundle format, we determine the sources that - // were reloaded during a hot restart to then wait until all the sources are - // parsed before finishing hot restart. This is necessary before we can - // recompute any source location metadata in the `ChromeProxyService`. - // TODO(srujzs): We don't do this for the AMD module format, should we? It - // would require adding an extra parameter in the AMD strategy. As we're - // planning to deprecate it, for now, do nothing. - final isDdcLibraryBundle = - globalToolConfiguration.loadStrategy is DdcLibraryBundleStrategy; - final computedReloadedSrcs = Completer(); - final reloadedSrcs = {}; - late StreamSubscription parsedScriptsSubscription; - if (isDdcLibraryBundle) { - // Injected client should send a request to recreate the isolate after the - // hot restart. The creation of the isolate should in turn wait until all - // scripts are parsed. - chromeProxyService.allowedToCreateIsolate = Completer(); - final debugger = await chromeProxyService.debuggerFuture; - parsedScriptsSubscription = debugger.parsedScriptsController.stream - .listen((url) { - computedReloadedSrcs.future.then((_) async { - reloadedSrcs.remove(Uri.parse(url).normalizePath().path); - if (reloadedSrcs.isEmpty && - !chromeProxyService.allowedToCreateIsolate.isCompleted) { - chromeProxyService.allowedToCreateIsolate.complete(); - } - }); - }); + Future> _hotRestart( + ChromeProxyService chromeProxyService, + VmService client, + ) async { + logger.info('Attempting a hot restart'); + + chromeProxyService.terminatingIsolates = true; + await _disableBreakpointsAndResume(client, chromeProxyService); + try { + logger.info('Attempting to get execution context ID.'); + await tryGetContextId(chromeProxyService); + logger.info('Got execution context ID.'); + } on StateError catch (e) { + // We couldn't find the execution context. `hotRestart` may have been + // triggered in the middle of a full reload. + return { + 'error': { + 'code': RPCErrorKind.kInternalError.code, + 'message': e.message, + }, + }; } - _chromeLogger.info('Issuing \$dartHotRestartDwds request'); - final remoteObject = await chromeProxyService.inspector.jsEvaluate( - '\$dartHotRestartDwds(\'$runId\', $pauseIsolatesOnStart);', - awaitPromise: true, - returnByValue: true, + // Start listening for isolate create events before issuing a hot + // restart. Only return success after the isolate has fully started. + final stream = chromeProxyService.onEvent('Isolate'); + final waitForIsolateStarted = stream.firstWhere( + (event) => event.kind == EventKind.kIsolateStart, ); - if (isDdcLibraryBundle) { - final reloadedSrcModuleLibraries = (remoteObject.value as List) - .cast(); - for (final srcModuleLibrary in reloadedSrcModuleLibraries) { - final srcModuleLibraryCast = srcModuleLibrary.cast(); - reloadedSrcs.add( - Uri.parse(srcModuleLibraryCast['src'] as String).normalizePath().path, - ); + try { + // If we should pause isolates on start, then only run main once we get a + // resume event. + final pauseIsolatesOnStart = chromeProxyService.pauseIsolatesOnStart; + if (pauseIsolatesOnStart) { + _waitForResumeEventToRunMain(chromeProxyService); } - if (reloadedSrcs.isEmpty) { - chromeProxyService.allowedToCreateIsolate.complete(); + // Generate run id to hot restart all apps loaded into the tab. + final runId = const Uuid().v4().toString(); + + // When using the DDC library bundle format, we determine the sources that + // were reloaded during a hot restart to then wait until all the sources are + // parsed before finishing hot restart. This is necessary before we can + // recompute any source location metadata in the `ChromeProxyService`. + // TODO(srujzs): We don't do this for the AMD module format, should we? It + // would require adding an extra parameter in the AMD strategy. As we're + // planning to deprecate it, for now, do nothing. + final isDdcLibraryBundle = + globalToolConfiguration.loadStrategy is DdcLibraryBundleStrategy; + final computedReloadedSrcs = Completer(); + final reloadedSrcs = {}; + late StreamSubscription parsedScriptsSubscription; + if (isDdcLibraryBundle) { + // Injected client should send a request to recreate the isolate after the + // hot restart. The creation of the isolate should in turn wait until all + // scripts are parsed. + chromeProxyService.allowedToCreateIsolate = Completer(); + final debugger = await chromeProxyService.debuggerFuture; + parsedScriptsSubscription = debugger.parsedScriptsController.stream + .listen((url) { + computedReloadedSrcs.future.then((_) async { + reloadedSrcs.remove(Uri.parse(url).normalizePath().path); + if (reloadedSrcs.isEmpty && + !chromeProxyService.allowedToCreateIsolate.isCompleted) { + chromeProxyService.allowedToCreateIsolate.complete(); + } + }); + }); } - computedReloadedSrcs.complete(); - await chromeProxyService.allowedToCreateIsolate.future; - await parsedScriptsSubscription.cancel(); - } else { - assert(remoteObject.value == null); - } - _chromeLogger.info('\$dartHotRestartDwds request complete.'); - } on WipError catch (exception) { - final code = exception.error?['code']; - final message = exception.error?['message']; - // This corresponds to `Execution context was destroyed` which can - // occur during a hot restart that must fall back to a full reload. - if (code != RPCErrorKind.kServerError.code) { + logger.info('Issuing \$dartHotRestartDwds request'); + final remoteObject = await chromeProxyService.inspector.jsEvaluate( + '\$dartHotRestartDwds(\'$runId\', $pauseIsolatesOnStart);', + awaitPromise: true, + returnByValue: true, + ); + if (isDdcLibraryBundle) { + final reloadedSrcModuleLibraries = (remoteObject.value as List) + .cast(); + for (final srcModuleLibrary in reloadedSrcModuleLibraries) { + final srcModuleLibraryCast = srcModuleLibrary.cast(); + reloadedSrcs.add( + Uri.parse( + srcModuleLibraryCast['src'] as String, + ).normalizePath().path, + ); + } + if (reloadedSrcs.isEmpty) { + chromeProxyService.allowedToCreateIsolate.complete(); + } + computedReloadedSrcs.complete(); + await chromeProxyService.allowedToCreateIsolate.future; + await parsedScriptsSubscription.cancel(); + } else { + assert(remoteObject.value == null); + } + logger.info('\$dartHotRestartDwds request complete.'); + } on WipError catch (exception) { + final code = exception.error?['code']; + final message = exception.error?['message']; + // This corresponds to `Execution context was destroyed` which can + // occur during a hot restart that must fall back to a full reload. + if (code != RPCErrorKind.kServerError.code) { + return { + 'error': {'code': code, 'message': message, 'data': exception}, + }; + } + } on ChromeDebugException catch (exception) { + // Exceptions thrown by the injected client during hot restart. return { - 'error': {'code': code, 'message': message, 'data': exception}, + 'error': { + 'code': RPCErrorKind.kInternalError.code, + 'message': '$exception', + }, }; } - } on ChromeDebugException catch (exception) { - // Exceptions thrown by the injected client during hot restart. - return { - 'error': { - 'code': RPCErrorKind.kInternalError.code, - 'message': '$exception', - }, - }; + logger.info('Waiting for Isolate Start event.'); + await waitForIsolateStarted; + chromeProxyService.terminatingIsolates = false; + + logger.info('Successful hot restart'); + return {'result': Success().toJson()}; } - _chromeLogger.info('Waiting for Isolate Start event.'); - await waitForIsolateStarted; - chromeProxyService.terminatingIsolates = false; - _chromeLogger.info('Successful hot restart'); - return {'result': Success().toJson()}; -} + void _waitForResumeEventToRunMain(ChromeProxyService chromeProxyService) { + StreamSubscription? resumeEventsSubscription; + resumeEventsSubscription = chromeProxyService.resumeAfterRestartEventsStream + .listen((_) async { + await resumeEventsSubscription!.cancel(); + await chromeProxyService.inspector.jsEvaluate( + '\$dartReadyToRunMain();', + ); + }); + } -void _waitForResumeEventToRunMain(ChromeProxyService chromeProxyService) { - StreamSubscription? resumeEventsSubscription; - resumeEventsSubscription = chromeProxyService.resumeAfterRestartEventsStream - .listen((_) async { - await resumeEventsSubscription!.cancel(); - await chromeProxyService.inspector.jsEvaluate( - '\$dartReadyToRunMain();', - ); - }); -} + Future> _fullReload( + ChromeProxyService chromeProxyService, + ) async { + logger.info('Attempting a full reload'); + await chromeProxyService.remoteDebugger.enablePage(); + await chromeProxyService.remoteDebugger.pageReload(); + logger.info('Successful full reload'); + return {'result': Success().toJson()}; + } -Future> _fullReload( - ChromeProxyService chromeProxyService, -) async { - _chromeLogger.info('Attempting a full reload'); - await chromeProxyService.remoteDebugger.enablePage(); - await chromeProxyService.remoteDebugger.pageReload(); - _chromeLogger.info('Successful full reload'); - return {'result': Success().toJson()}; + Future _disableBreakpointsAndResume( + VmService client, + ChromeProxyService chromeProxyService, + ) async { + logger.info('Attempting to disable breakpoints and resume the isolate'); + final vm = await client.getVM(); + final isolates = vm.isolates; + if (isolates == null || isolates.isEmpty) { + throw StateError('No active isolate to resume.'); + } + final isolateId = isolates.first.id; + if (isolateId == null) { + throw StateError('No active isolate to resume.'); + } + await chromeProxyService.disableBreakpoints(); + try { + // Any checks for paused status result in race conditions or hangs + // at this point: + // + // - `getIsolate()` and check for status: + // the app might still pause on existing breakpoint. + // + // - `pause()` and wait for `Debug.paused` event: + // chrome does not send the `Debug.Paused `notification + // without shifting focus to chrome. + // + // Instead, just try resuming and + // ignore failures indicating that the app is already running: + // + // WipError -32000 Can only perform operation while paused. + await client.resume(isolateId); + } on RPCError catch (e, s) { + if (!e.message.contains('Can only perform operation while paused')) { + logger.severe('Hot restart failed to resume exiting isolate', e, s); + rethrow; + } + } + logger.info('Successfully disabled breakpoints and resumed the isolate'); + } } -Future _disableBreakpointsAndResume( - VmService client, - ChromeProxyService chromeProxyService, -) async { - _chromeLogger.info( - 'Attempting to disable breakpoints and resume the isolate', - ); - final vm = await client.getVM(); - final isolates = vm.isolates; - if (isolates == null || isolates.isEmpty) { - throw StateError('No active isolate to resume.'); +/// WebSocket-based DWDS VM client. +final class WebSocketDwdsVmClient + extends DwdsVmClient { + @override + final logger = Logger('WebSocketDwdsVmClient'); + + static Future create( + WebSocketDebugService debugService, + Uri? ddsUri, + ) async { + final dwdsVmClient = WebSocketDwdsVmClient(debugService: debugService); + dwdsVmClient.logger.fine('Creating WebSocket DWDS VM client'); + await dwdsVmClient.initialize(ddsUri: ddsUri); + return dwdsVmClient; } - final isolateId = isolates.first.id; - if (isolateId == null) { - throw StateError('No active isolate to resume.'); + + WebSocketDwdsVmClient({required super.debugService}); + + @override + Future registerServiceExtensions({ + required VmService client, + required WebSocketProxyService proxyService, + }) async { + client.registerServiceCallback( + 'hotRestart', + (request) => captureElapsedTime( + () => proxyService.hotRestart(), + (_) => DwdsEvent.hotRestart(), + ), + ); + await client.registerService('hotRestart', 'DWDS'); } - await chromeProxyService.disableBreakpoints(); - try { - // Any checks for paused status result in race conditions or hangs - // at this point: - // - // - `getIsolate()` and check for status: - // the app might still pause on existing breakpoint. - // - // - `pause()` and wait for `Debug.paused` event: - // chrome does not send the `Debug.Paused `notification - // without shifting focus to chrome. - // - // Instead, just try resuming and - // ignore failures indicating that the app is already running: - // - // WipError -32000 Can only perform operation while paused. - await client.resume(isolateId); - } on RPCError catch (e, s) { - if (!e.message.contains('Can only perform operation while paused')) { - _chromeLogger.severe( - 'Hot restart failed to resume exiting isolate', - e, - s, - ); - rethrow; + + @override + Future maybeHandleServiceExtensionRequestImpl( + VmResponse request, { + required WebSocketProxyService proxyService, + DwdsStats? dwdsStats, + Future? clientFuture, + }) async { + VmRequest? response; + final method = request['method']; + + logger.finest('Processing service extension method: $method'); + + if (method == _NamespacedServiceExtension.flutterListViews.method) { + response = await flutterListViewsHandler(proxyService); + } else if (method == _NamespacedServiceExtension.extDwdsEmitEvent.method) { + response = extDwdsEmitEventHandler(request, logger); + } else if (method == _NamespacedServiceExtension.extDwdsReload.method) { + response = {'result': 'Reload not implemented'}; + } else if (method == _NamespacedServiceExtension.extDwdsSendEvent.method) { + response = await extDwdsSendEventHandler(request, null, logger); + } else if (method == _NamespacedServiceExtension.extDwdsScreenshot.method) { + response = {'result': 'Screenshot not implemented'}; } + return response; } - _chromeLogger.info( - 'Successfully disabled breakpoints and resumed the isolate', - ); } diff --git a/dwds/lib/src/handlers/dev_handler.dart b/dwds/lib/src/handlers/dev_handler.dart index a5d25e080..94d9e3201 100644 --- a/dwds/lib/src/handlers/dev_handler.dart +++ b/dwds/lib/src/handlers/dev_handler.dart @@ -34,10 +34,11 @@ import 'package:dwds/src/servers/devtools.dart'; import 'package:dwds/src/servers/extension_backend.dart'; import 'package:dwds/src/servers/extension_debugger.dart'; import 'package:dwds/src/services/app_debug_services.dart'; -import 'package:dwds/src/services/chrome_proxy_service.dart'; -import 'package:dwds/src/services/debug_service.dart'; +import 'package:dwds/src/services/chrome/chrome_debug_service.dart'; +import 'package:dwds/src/services/chrome/chrome_proxy_service.dart'; import 'package:dwds/src/services/expression_compiler.dart'; -import 'package:dwds/src/services/web_socket_proxy_service.dart'; +import 'package:dwds/src/services/web_socket/web_socket_debug_service.dart'; +import 'package:dwds/src/services/web_socket/web_socket_proxy_service.dart'; import 'package:dwds/src/utilities/shared.dart'; import 'package:logging/logging.dart'; import 'package:shelf/shelf.dart'; @@ -120,9 +121,7 @@ class DevHandler { }; Future close() => _closed ??= () async { - for (final sub in _subs) { - await sub.cancel(); - } + await Future.wait([for (final sub in _subs) sub.cancel()]); for (final handler in _sseHandlers.values) { handler.shutdown(); } @@ -230,12 +229,12 @@ class DevHandler { // machine. This allows consumers of DWDS to provide a `hostname` for // debugging through the Dart Debug Extension without impacting the local // debug workflow. - 'localhost', - webkitDebugger, - executionContext, - _assetReader, - appConnection, - _urlEncoder, + hostname: 'localhost', + remoteDebugger: webkitDebugger, + executionContext: executionContext, + assetReader: _assetReader, + appConnection: appConnection, + urlEncoder: _urlEncoder, onResponse: (response) { if (response['error'] == null) return; _logger.finest('VmService proxy responded with an error:\n$response'); @@ -285,10 +284,11 @@ class DevHandler { AppConnection appConnection, ) async { final webSocketDebugService = await WebSocketDebugService.start( - 'localhost', - appConnection, - _assetReader, + hostname: 'localhost', + appConnection: appConnection, + assetReader: _assetReader, sendClientRequest: _sendRequestToClients, + ddsConfig: _ddsConfig, ); return _createAppDebugServicesWebSocketMode( webSocketDebugService, @@ -851,18 +851,12 @@ class DevHandler { if (_ddsConfig.enable) { dds = await debugService.startDartDevelopmentService(); } - final vmClient = await ChromeDwdsVmClient.create( - debugService, - dwdsStats, - dds?.wsUri, - ); - final appDebugService = ChromeAppDebugServices( - debugService, - vmClient, - dwdsStats, - dds?.wsUri, - dds?.devToolsUri, - dds?.dtdUri, + final vmClient = await ChromeDwdsVmClient.create(debugService, dds?.wsUri); + final appDebugService = AppDebugServices( + debugService: debugService, + dwdsVmClient: vmClient, + dwdsStats: dwdsStats, + dds: dds, ); final encodedUri = await debugService.encodedUri; _logger.info('Debug service listening on $encodedUri\n'); @@ -889,12 +883,18 @@ class DevHandler { WebSocketDebugService webSocketDebugService, AppConnection appConnection, ) async { + DartDevelopmentServiceLauncher? dds; + if (_ddsConfig.enable) { + dds = await webSocketDebugService.startDartDevelopmentService(); + } final wsVmClient = await WebSocketDwdsVmClient.create( webSocketDebugService, + dds?.wsUri, ); - final wsAppDebugService = WebSocketAppDebugServices( - webSocketDebugService, - wsVmClient, + final wsAppDebugService = AppDebugServices( + debugService: webSocketDebugService, + dwdsVmClient: wsVmClient, + dds: dds, ); safeUnawaited(_handleIsolateStart(appConnection)); @@ -968,12 +968,12 @@ class DevHandler { var appServices = _servicesByAppId[appId]; if (appServices == null) { final debugService = await ChromeDebugService.start( - _hostname, - extensionDebugger, - executionContext, - _assetReader, - connection, - _urlEncoder, + hostname: _hostname, + remoteDebugger: extensionDebugger, + executionContext: executionContext, + assetReader: _assetReader, + appConnection: connection, + urlEncoder: _urlEncoder, onResponse: (response) { if (response['error'] == null) return; _logger.finest('VmService proxy responded with an error:\n$response'); diff --git a/dwds/lib/src/injected/client.js b/dwds/lib/src/injected/client.js index 001713afd..f89987f57 100644 --- a/dwds/lib/src/injected/client.js +++ b/dwds/lib/src/injected/client.js @@ -1,4 +1,4 @@ -// Generated by dart2js (, csp, intern-composite-values), the Dart to JavaScript compiler version: 3.11.0-13.0.dev. +// Generated by dart2js (, csp, intern-composite-values), the Dart to JavaScript compiler version: 3.11.0-88.0.dev. // The code supports the following hooks: // dartPrint(message): // if this function is defined it is called instead of the Dart [print] diff --git a/dwds/lib/src/servers/extension_debugger.dart b/dwds/lib/src/servers/extension_debugger.dart index 11f9a32f6..3c8d24230 100644 --- a/dwds/lib/src/servers/extension_debugger.dart +++ b/dwds/lib/src/servers/extension_debugger.dart @@ -12,7 +12,7 @@ import 'package:dwds/data/serializers.dart'; import 'package:dwds/src/debugging/execution_context.dart'; import 'package:dwds/src/debugging/remote_debugger.dart'; import 'package:dwds/src/handlers/socket_connections.dart'; -import 'package:dwds/src/services/chrome_debug_exception.dart'; +import 'package:dwds/src/services/chrome/chrome_debug_exception.dart'; import 'package:logging/logging.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' hide StackTrace; diff --git a/dwds/lib/src/services/app_debug_services.dart b/dwds/lib/src/services/app_debug_services.dart index fe3a2e04f..dd2f1ab42 100644 --- a/dwds/lib/src/services/app_debug_services.dart +++ b/dwds/lib/src/services/app_debug_services.dart @@ -2,111 +2,38 @@ // 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. +import 'package:dds/dds_launcher.dart'; import 'package:dwds/src/dwds_vm_client.dart'; import 'package:dwds/src/events.dart'; import 'package:dwds/src/services/debug_service.dart'; import 'package:dwds/src/services/proxy_service.dart'; /// Common interface for debug service containers. -abstract class AppDebugServices { - DebugService get debugService; - DwdsVmClient get dwdsVmClient; - DwdsStats? get dwdsStats; - Uri? get ddsUri; - Uri? get devToolsUri; - Uri? get dtdUri; - String? get connectedInstanceId; - set connectedInstanceId(String? id); - Future close(); - ProxyService get proxyService; -} - -/// Chrome-based debug services container. -class ChromeAppDebugServices implements AppDebugServices { - final ChromeDebugService _debugService; - final ChromeDwdsVmClient _dwdsVmClient; - final DwdsStats _dwdsStats; - final Uri? _ddsUri; - final Uri? _devToolsUri; - final Uri? _dtdUri; - Future? _closed; - String? _connectedInstanceId; - - ChromeAppDebugServices( - this._debugService, - this._dwdsVmClient, - this._dwdsStats, - this._ddsUri, - this._devToolsUri, - this._dtdUri, - ); - - @override - ChromeDebugService get debugService => _debugService; - - @override - DwdsVmClient get dwdsVmClient => _dwdsVmClient; - - @override - DwdsStats get dwdsStats => _dwdsStats; - - @override - Uri? get ddsUri => _ddsUri; - - @override - Uri? get devToolsUri => _devToolsUri; - - @override - Uri? get dtdUri => _dtdUri; - - @override - String? get connectedInstanceId => _connectedInstanceId; - - @override - set connectedInstanceId(String? id) => _connectedInstanceId = id; - - @override - ProxyService get proxyService => debugService.chromeProxyService; - - @override - Future close() => - _closed ??= Future.wait([debugService.close(), dwdsVmClient.close()]); -} - -/// WebSocket-based implementation of app debug services. -class WebSocketAppDebugServices implements AppDebugServices { - final WebSocketDebugService _debugService; - final WebSocketDwdsVmClient _dwdsVmClient; - Future? _closed; - @override +class AppDebugServices< + T extends DebugService, + U extends ProxyService, + V extends DwdsVmClient +> { + final T debugService; + final V dwdsVmClient; + final DartDevelopmentServiceLauncher? _dds; + Uri? get ddsUri => _dds?.wsUri; + Uri? get devToolsUri => _dds?.devToolsUri; + Uri? get dtdUri => _dds?.dtdUri; + final DwdsStats? dwdsStats; String? connectedInstanceId; - WebSocketAppDebugServices(this._debugService, this._dwdsVmClient); - - @override - WebSocketDebugService get debugService => _debugService; - - @override - DwdsVmClient get dwdsVmClient => _dwdsVmClient; - - // WebSocket-only service - Chrome/DDS features not available - @override - DwdsStats? get dwdsStats => null; - - @override - // TODO(bkonyi): DDS should still start in WebSocket mode. - Uri? get ddsUri => null; - - @override - Uri? get devToolsUri => null; + Future? _closed; - @override - Uri? get dtdUri => null; + AppDebugServices({ + required this.debugService, + required this.dwdsVmClient, + required DartDevelopmentServiceLauncher? dds, + this.dwdsStats, + }) : _dds = dds; - @override - ProxyService get proxyService => _debugService.webSocketProxyService; + ProxyService get proxyService => debugService.proxyService; - @override Future close() { return _closed ??= Future.wait([ debugService.close(), diff --git a/dwds/lib/src/services/chrome_debug_exception.dart b/dwds/lib/src/services/chrome/chrome_debug_exception.dart similarity index 95% rename from dwds/lib/src/services/chrome_debug_exception.dart rename to dwds/lib/src/services/chrome/chrome_debug_exception.dart index 4f8e8b595..3a33022cc 100644 --- a/dwds/lib/src/services/chrome_debug_exception.dart +++ b/dwds/lib/src/services/chrome/chrome_debug_exception.dart @@ -4,7 +4,7 @@ import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; -class ChromeDebugException extends ExceptionDetails implements Exception { +final class ChromeDebugException extends ExceptionDetails implements Exception { /// Optional, additional information about the exception. final Object? additionalDetails; diff --git a/dwds/lib/src/services/chrome/chrome_debug_service.dart b/dwds/lib/src/services/chrome/chrome_debug_service.dart new file mode 100644 index 000000000..6fa2731dd --- /dev/null +++ b/dwds/lib/src/services/chrome/chrome_debug_service.dart @@ -0,0 +1,116 @@ +// Copyright (c) 2025, 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. + +import 'package:dwds/asset_reader.dart'; +import 'package:dwds/src/config/tool_configuration.dart'; +import 'package:dwds/src/connections/app_connection.dart'; +import 'package:dwds/src/debugging/execution_context.dart'; +import 'package:dwds/src/debugging/remote_debugger.dart'; +import 'package:dwds/src/services/chrome/chrome_proxy_service.dart'; +import 'package:dwds/src/services/debug_service.dart'; +import 'package:dwds/src/services/expression_compiler.dart'; +import 'package:dwds/src/utilities/shared.dart'; +import 'package:meta/meta.dart'; +import 'package:shelf/shelf.dart' as shelf; +import 'package:sse/server/sse_handler.dart'; + +/// A Dart Web Debug Service. +/// +/// Creates a [ChromeProxyService] from an existing Chrome instance. +final class ChromeDebugService extends DebugService { + ChromeDebugService._({ + required super.serverHostname, + required super.useSse, + required super.ddsConfig, + required super.urlEncoder, + }); + + static const _kSseHandlerPath = '\$debugHandler'; + + @protected + @override + Future initialize({ + required ChromeProxyService proxyService, + void Function(Map)? onRequest, + void Function(Map)? onResponse, + }) async { + await super.initialize(proxyService: proxyService); + shelf.Handler handler; + // DDS will always connect to DWDS via web sockets. + if (useSse && !ddsConfig.enable) { + handler = _initializeSSEHandler( + chromeProxyService: proxyService, + onRequest: onRequest, + onResponse: onResponse, + ); + } else { + handler = initializeWebSocketHandler( + proxyService: proxyService, + onRequest: onRequest, + onResponse: onResponse, + ); + } + await serve(handler: handler); + } + + static Future start({ + required String hostname, + required RemoteDebugger remoteDebugger, + required ExecutionContext executionContext, + required AssetReader assetReader, + required AppConnection appConnection, + UrlEncoder? urlEncoder, + void Function(Map)? onRequest, + void Function(Map)? onResponse, + required DartDevelopmentServiceConfiguration ddsConfig, + bool useSse = false, + ExpressionCompiler? expressionCompiler, + }) async { + final debugService = ChromeDebugService._( + serverHostname: hostname, + useSse: useSse, + ddsConfig: ddsConfig, + urlEncoder: urlEncoder, + ); + final chromeProxyService = await ChromeProxyService.create( + remoteDebugger: remoteDebugger, + debugService: debugService, + assetReader: assetReader, + appConnection: appConnection, + executionContext: executionContext, + expressionCompiler: expressionCompiler, + ); + await debugService.initialize( + proxyService: chromeProxyService, + onRequest: onRequest, + onResponse: onResponse, + ); + return debugService; + } + + shelf.Handler _initializeSSEHandler({ + required ChromeProxyService chromeProxyService, + void Function(Map)? onRequest, + void Function(Map)? onResponse, + }) { + final sseHandler = SseHandler( + Uri.parse('/$authToken/$_kSseHandlerPath'), + keepAlive: const Duration(seconds: 5), + ); + final handler = sseHandler.handler; + safeUnawaited(() async { + while (await sseHandler.connections.hasNext) { + final connection = await sseHandler.connections.next; + handleConnection( + connection, + chromeProxyService, + serviceExtensionRegistry, + onRequest: onRequest, + onResponse: onResponse, + ); + } + }()); + return handler; + } +} diff --git a/dwds/lib/src/services/chrome_proxy_service.dart b/dwds/lib/src/services/chrome/chrome_proxy_service.dart similarity index 82% rename from dwds/lib/src/services/chrome_proxy_service.dart rename to dwds/lib/src/services/chrome/chrome_proxy_service.dart index 7f84ac10b..e5745e606 100644 --- a/dwds/lib/src/services/chrome_proxy_service.dart +++ b/dwds/lib/src/services/chrome/chrome_proxy_service.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2025, 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. @@ -22,7 +22,7 @@ import 'package:dwds/src/debugging/skip_list.dart'; import 'package:dwds/src/events.dart'; import 'package:dwds/src/readers/asset_reader.dart'; import 'package:dwds/src/services/batched_expression_evaluator.dart'; -import 'package:dwds/src/services/debug_service.dart'; +import 'package:dwds/src/services/chrome/chrome_debug_service.dart'; import 'package:dwds/src/services/expression_compiler.dart'; import 'package:dwds/src/services/expression_evaluator.dart'; import 'package:dwds/src/services/proxy_service.dart'; @@ -33,40 +33,8 @@ import 'package:vm_service/vm_service.dart' hide vmServiceVersion; import 'package:vm_service_interface/vm_service_interface.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; -// This event is identical to the one sent by the VM service from -// sdk/lib/vmservice/vmservice.dart before existing VM service clients are -// disconnected. -final class DartDevelopmentServiceConnectedEvent extends Event { - DartDevelopmentServiceConnectedEvent({ - required super.timestamp, - required this.uri, - }) : message = - 'A Dart Developer Service instance has connected and this direct ' - 'connection to the VM service will now be closed. Please reconnect to ' - 'the Dart Development Service at $uri.', - super(kind: 'DartDevelopmentServiceConnected'); - - final String message; - final String uri; - - @override - Map toJson() => { - ...super.toJson(), - 'uri': uri, - 'message': message, - }; -} - -final class DisconnectNonDartDevelopmentServiceClients extends RPCError { - DisconnectNonDartDevelopmentServiceClients() - : super('_yieldControlToDDS', kErrorCode); - - // Arbitrary error code that's unlikely to be used elsewhere. - static const kErrorCode = -199328; -} - /// A proxy from the chrome debug protocol to the dart vm service protocol. -class ChromeProxyService extends ProxyService { +final class ChromeProxyService extends ProxyService { /// Signals when isolate starts. Future get isStarted => _startedCompleter.future; Completer _startedCompleter = Completer(); @@ -111,17 +79,22 @@ class ChromeProxyService extends ProxyService { bool terminatingIsolates = false; - ChromeProxyService._( - super.vm, - super.root, - this._assetReader, - this.remoteDebugger, - this._modules, - this._locations, - this._skipLists, - this.executionContext, - this._compiler, - ) { + ChromeProxyService._({ + required super.vm, + required super.debugService, + required AssetReader assetReader, + required this.remoteDebugger, + required Modules modules, + required Locations locations, + required SkipLists skipLists, + required this.executionContext, + required ExpressionCompiler? compiler, + }) : _assetReader = assetReader, + _modules = modules, + _locations = locations, + _skipLists = skipLists, + _compiler = compiler, + super(root: assetReader.basePath) { final debugger = Debugger.create( remoteDebugger, streamNotify, @@ -132,14 +105,14 @@ class ChromeProxyService extends ProxyService { debugger.then(_debuggerCompleter.complete); } - static Future create( - RemoteDebugger remoteDebugger, - String root, - AssetReader assetReader, - AppConnection appConnection, - ExecutionContext executionContext, - ExpressionCompiler? expressionCompiler, - ) async { + static Future create({ + required RemoteDebugger remoteDebugger, + required ChromeDebugService debugService, + required AssetReader assetReader, + required AppConnection appConnection, + required ExecutionContext executionContext, + required ExpressionCompiler? expressionCompiler, + }) async { final vm = VM( name: 'ChromeDebugProxy', operatingSystem: Platform.operatingSystem, @@ -155,19 +128,20 @@ class ChromeProxyService extends ProxyService { pid: -1, ); + final root = assetReader.basePath; final modules = Modules(root); final locations = Locations(assetReader, modules, root); final skipLists = SkipLists(root); final service = ChromeProxyService._( - vm, - root, - assetReader, - remoteDebugger, - modules, - locations, - skipLists, - executionContext, - expressionCompiler, + vm: vm, + debugService: debugService, + assetReader: assetReader, + remoteDebugger: remoteDebugger, + modules: modules, + locations: locations, + skipLists: skipLists, + executionContext: executionContext, + compiler: expressionCompiler, ); safeUnawaited(service.createIsolate(appConnection, newConnection: true)); return service; @@ -457,7 +431,7 @@ class ChromeProxyService extends ProxyService { int? column, }) async { await isInitialized; - _checkIsolate('addBreakpoint', isolateId); + checkIsolate('addBreakpoint', isolateId); return (await debuggerFuture).addBreakpoint(scriptId, line, column: column); } @@ -485,7 +459,7 @@ class ChromeProxyService extends ProxyService { int? column, }) async { await isInitialized; - _checkIsolate('addBreakpointWithScriptUri', isolateId); + checkIsolate('addBreakpointWithScriptUri', isolateId); if (Uri.parse(scriptUri).scheme == 'dart') { // TODO(annagrin): Support setting breakpoints in dart SDK locations. // Issue: https://github.com/dart-lang/webdev/issues/1584 @@ -529,7 +503,7 @@ class ChromeProxyService extends ProxyService { }) async { await isInitialized; isolateId ??= inspector.isolate.id; - _checkIsolate('callServiceExtension', isolateId); + checkIsolate('callServiceExtension', isolateId); args ??= {}; final stringArgs = args.map( (k, v) => MapEntry( @@ -666,7 +640,7 @@ class ChromeProxyService extends ProxyService { final evaluator = _expressionEvaluator; if (evaluator != null) { await isCompilerInitialized; - _checkIsolate('evaluate', isolateId); + checkIsolate('evaluate', isolateId); late Obj object; try { @@ -756,7 +730,7 @@ class ChromeProxyService extends ProxyService { final evaluator = _expressionEvaluator; if (evaluator != null) { await isCompilerInitialized; - _checkIsolate('evaluateInFrame', isolateId); + checkIsolate('evaluateInFrame', isolateId); return await _getEvaluationResult( isolateId, @@ -777,18 +751,6 @@ class ChromeProxyService extends ProxyService { }, (result) => DwdsEvent.evaluateInFrame(expression, result)); } - @override - Future getIsolate(String isolateId) => - wrapInErrorHandlerAsync('getIsolate', () => _getIsolate(isolateId)); - - Future _getIsolate(String isolateId) { - return captureElapsedTime(() async { - await isInitialized; - _checkIsolate('getIsolate', isolateId); - return inspector.isolate; - }, (result) => DwdsEvent.getIsolate()); - } - @override Future getMemoryUsage(String isolateId) => wrapInErrorHandlerAsync( @@ -798,7 +760,7 @@ class ChromeProxyService extends ProxyService { Future _getMemoryUsage(String isolateId) async { await isInitialized; - _checkIsolate('getMemoryUsage', isolateId); + checkIsolate('getMemoryUsage', isolateId); return inspector.getMemoryUsage(); } @@ -825,22 +787,10 @@ class ChromeProxyService extends ProxyService { int? count, }) async { await isInitialized; - _checkIsolate('getObject', isolateId); + checkIsolate('getObject', isolateId); return inspector.getObject(objectId, offset: offset, count: count); } - @override - Future getScripts(String isolateId) => - wrapInErrorHandlerAsync('getScripts', () => _getScripts(isolateId)); - - Future _getScripts(String isolateId) { - return captureElapsedTime(() async { - await isInitialized; - _checkIsolate('getScripts', isolateId); - return inspector.getScripts(); - }, (result) => DwdsEvent.getScripts()); - } - @override Future getSourceReport( String isolateId, @@ -866,7 +816,7 @@ class ChromeProxyService extends ProxyService { }) { return captureElapsedTime(() async { await isInitialized; - _checkIsolate('getSourceReport', isolateId); + checkIsolate('getSourceReport', isolateId); return await inspector.getSourceReport(reports, scriptId: scriptId); }, (result) => DwdsEvent.getSourceReport()); } @@ -893,20 +843,10 @@ class ChromeProxyService extends ProxyService { Future _getStack(String isolateId, {int? limit}) async { await isInitialized; await isStarted; - _checkIsolate('getStack', isolateId); + checkIsolate('getStack', isolateId); return (await debuggerFuture).getStack(limit: limit); } - @override - Future getVM() => wrapInErrorHandlerAsync('getVM', _getVM); - - Future _getVM() { - return captureElapsedTime(() async { - await isInitialized; - return vm; - }, (result) => DwdsEvent.getVM()); - } - @override Future invoke( String isolateId, @@ -932,7 +872,7 @@ class ChromeProxyService extends ProxyService { List argumentIds, ) async { await isInitialized; - _checkIsolate('invoke', isolateId); + checkIsolate('invoke', isolateId); final remote = await inspector.invoke(targetId, selector, argumentIds); return _instanceRef(remote); } @@ -994,52 +934,10 @@ class ChromeProxyService extends ProxyService { required bool internalPause, }) async { await isInitialized; - _checkIsolate('pause', isolateId); + checkIsolate('pause', isolateId); return (await debuggerFuture).pause(internalPause: internalPause); } - // Note: Ignore the optional local parameter, when it is set to `true` the - // request is intercepted and handled by DDS. - @override - Future lookupResolvedPackageUris( - String isolateId, - List uris, { - bool? local, - }) => wrapInErrorHandlerAsync( - 'lookupResolvedPackageUris', - () => _lookupResolvedPackageUris(isolateId, uris), - ); - - Future _lookupResolvedPackageUris( - String isolateId, - List uris, - ) async { - await isInitialized; - _checkIsolate('lookupResolvedPackageUris', isolateId); - return UriList(uris: uris.map(DartUri.toResolvedUri).toList()); - } - - @override - Future lookupPackageUris(String isolateId, List uris) => - wrapInErrorHandlerAsync( - 'lookupPackageUris', - () => _lookupPackageUris(isolateId, uris), - ); - - Future _lookupPackageUris( - String isolateId, - List uris, - ) async { - await isInitialized; - _checkIsolate('lookupPackageUris', isolateId); - return UriList(uris: uris.map(DartUri.toPackageUri).toList()); - } - - @override - Future registerService(String service, String alias) { - return rpcNotSupportedFuture('registerService'); - } - @override Future reloadSources( String isolateId, { @@ -1049,7 +947,7 @@ class ChromeProxyService extends ProxyService { String? packagesUri, }) async { await isInitialized; - _checkIsolate('reloadSources', isolateId); + checkIsolate('reloadSources', isolateId); ReloadReport getFailedReloadReport(String error) => _ReloadReportWithMetadata(success: false) @@ -1167,7 +1065,7 @@ class ChromeProxyService extends ProxyService { String breakpointId, ) async { await isInitialized; - _checkIsolate('removeBreakpoint', isolateId); + checkIsolate('removeBreakpoint', isolateId); return (await debuggerFuture).removeBreakpoint(breakpointId); } @@ -1194,7 +1092,7 @@ class ChromeProxyService extends ProxyService { await captureElapsedTime(() async { await isInitialized; await isStarted; - _checkIsolate('resume', isolateId); + checkIsolate('resume', isolateId); final debugger = await debuggerFuture; return await debugger.resume(step: step, frameIndex: frameIndex); }, (result) => DwdsEvent.resume(step)); @@ -1239,76 +1137,12 @@ class ChromeProxyService extends ProxyService { String? exceptionPauseMode, }) async { await isInitialized; - _checkIsolate('setIsolatePauseMode', isolateId); + checkIsolate('setIsolatePauseMode', isolateId); return (await debuggerFuture).setExceptionPauseMode( exceptionPauseMode ?? ExceptionPauseMode.kNone, ); } - @override - Future setFlag(String name, String value) => - wrapInErrorHandlerAsync('setFlag', () => _setFlag(name, value)); - - Future _setFlag(String name, String value) async { - if (!currentVmServiceFlags.containsKey(name)) { - return rpcNotSupportedFuture('setFlag'); - } - - assert(value == 'true' || value == 'false'); - currentVmServiceFlags[name] = value == 'true'; - - return Success(); - } - - @override - Future setLibraryDebuggable( - String isolateId, - String libraryId, - bool isDebuggable, - ) { - return rpcNotSupportedFuture('setLibraryDebuggable'); - } - - @override - Future setName(String isolateId, String name) => - wrapInErrorHandlerAsync('setName', () => _setName(isolateId, name)); - - Future _setName(String isolateId, String name) async { - await isInitialized; - _checkIsolate('setName', isolateId); - inspector.isolate.name = name; - return Success(); - } - - @override - Future setVMName(String name) => - wrapInErrorHandlerAsync('setVMName', () => _setVMName(name)); - - Future _setVMName(String name) async { - vm.name = name; - streamNotify( - 'VM', - Event( - kind: EventKind.kVMUpdate, - timestamp: DateTime.now().millisecondsSinceEpoch, - // We are not guaranteed to have an isolate at this point in time. - isolate: null, - )..vm = toVMRef(vm), - ); - return Success(); - } - - @override - Future streamListen(String streamId) => - wrapInErrorHandlerAsync('streamListen', () => _streamListen(streamId)); - - Future _streamListen(String streamId) async { - // TODO: This should return an error if the stream is already being listened - // to. - onEvent(streamId); - return Success(); - } - /// Returns a streamController that listens for console logs from chrome and /// adds all events passing [filter] to the stream. StreamController _chromeConsoleStreamController( @@ -1371,35 +1205,12 @@ class ChromeProxyService extends ProxyService { return controller; } - /// Parses the [BatchedDebugEvents] and emits corresponding Dart VM Service - /// protocol [Event]s. - @override - void parseBatchedDebugEvents(BatchedDebugEvents debugEvents) { - for (final debugEvent in debugEvents.events) { - parseDebugEvent(debugEvent); - } - } - /// Parses the [DebugEvent] and emits a corresponding Dart VM Service /// protocol [Event]. @override void parseDebugEvent(DebugEvent debugEvent) { if (terminatingIsolates) return; - if (!isIsolateRunning) return; - final isolateRef = inspector.isolateRef; - - streamNotify( - EventStreams.kExtension, - Event( - kind: EventKind.kExtension, - timestamp: DateTime.now().millisecondsSinceEpoch, - isolate: isolateRef, - ) - ..extensionKind = debugEvent.kind - ..extensionData = ExtensionData.parse( - jsonDecode(debugEvent.eventData) as Map, - ), - ); + super.parseDebugEvent(debugEvent); } /// Parses the [RegisterEvent] and emits a corresponding Dart VM Service @@ -1407,21 +1218,7 @@ class ChromeProxyService extends ProxyService { @override void parseRegisterEvent(RegisterEvent registerEvent) { if (terminatingIsolates) return; - if (!isIsolateRunning) return; - - final isolate = inspector.isolate; - final isolateRef = inspector.isolateRef; - final service = registerEvent.eventData; - isolate.extensionRPCs?.add(service); - - streamNotify( - EventStreams.kIsolate, - Event( - kind: EventKind.kServiceExtensionAdded, - timestamp: DateTime.now().millisecondsSinceEpoch, - isolate: isolateRef, - )..extensionRPC = service, - ); + super.parseRegisterEvent(registerEvent); } /// Listens for chrome console events and handles the ones we care about. @@ -1541,54 +1338,10 @@ class ChromeProxyService extends ProxyService { return logParams; } - @override - Future yieldControlToDDS(String uri) async { - // This will throw an RPCError if there's already an existing DDS instance. - ChromeDebugService.yieldControlToDDS(uri); - - // Notify existing clients that DDS has connected and they're about to be - // disconnected. - final event = DartDevelopmentServiceConnectedEvent( - timestamp: DateTime.now().millisecondsSinceEpoch, - uri: uri, - ); - streamNotify(EventStreams.kService, event); - - // We throw since we have no other way to control what the response content - // is for this RPC. The debug service will check for this particular - // exception as a signal to close connections to all other clients. - throw DisconnectNonDartDevelopmentServiceClients(); - } - Future _instanceRef(RemoteObject? obj) async { final instance = obj == null ? null : await inspector.instanceRefFor(obj); return instance ?? ChromeAppInstanceHelper.kNullInstanceRef; } - - /// Validate that isolateId matches the current isolate we're connected to and - /// return that isolate. - /// - /// This is useful to call at the beginning of API methods that are passed an - /// isolate id. - Isolate _checkIsolate(String methodName, String? isolateId) { - final currentIsolateId = inspector.isolate.id; - if (currentIsolateId == null) { - throw StateError('No running isolate ID'); - } - if (isolateId != currentIsolateId) { - _throwSentinel( - methodName, - SentinelKind.kCollected, - 'Unrecognized isolateId: $isolateId', - ); - } - return inspector.isolate; - } - - static Never _throwSentinel(String method, String kind, String message) { - final data = {'kind': kind, 'valueAsString': message}; - throw SentinelException.parse(method, data); - } } // The default `ReloadReport`'s `toJson` only emits the type and success of the diff --git a/dwds/lib/src/services/debug_service.dart b/dwds/lib/src/services/debug_service.dart index d6a9fa402..787a277a0 100644 --- a/dwds/lib/src/services/debug_service.dart +++ b/dwds/lib/src/services/debug_service.dart @@ -10,27 +10,16 @@ import 'dart:typed_data'; import 'package:dds/dds_launcher.dart'; import 'package:dwds/src/config/tool_configuration.dart'; -import 'package:dwds/src/connections/app_connection.dart'; -import 'package:dwds/src/debugging/execution_context.dart'; -import 'package:dwds/src/debugging/remote_debugger.dart'; import 'package:dwds/src/events.dart'; -import 'package:dwds/src/readers/asset_reader.dart'; -import 'package:dwds/src/services/chrome_proxy_service.dart'; -import 'package:dwds/src/services/expression_compiler.dart'; -import 'package:dwds/src/services/web_socket_proxy_service.dart'; +import 'package:dwds/src/services/proxy_service.dart'; import 'package:dwds/src/utilities/server.dart'; -import 'package:dwds/src/utilities/shared.dart'; import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; import 'package:shelf/shelf.dart' as shelf; -import 'package:shelf/shelf.dart' hide Response; import 'package:shelf_web_socket/shelf_web_socket.dart'; -import 'package:sse/server/sse_handler.dart'; import 'package:stream_channel/stream_channel.dart'; import 'package:vm_service/vm_service.dart'; import 'package:vm_service_interface/vm_service_interface.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; - -const _kSseHandlerPath = '\$debugHandler'; bool _acceptNewConnections = true; @@ -39,157 +28,89 @@ int _clientId = 0; Logger _logger = Logger('DebugService'); -void _handleConnection( - StreamChannel channel, - ChromeProxyService chromeProxyService, - ServiceExtensionRegistry serviceExtensionRegistry, { - void Function(Map)? onRequest, - void Function(Map)? onResponse, -}) { - final clientId = _clientId++; - final responseController = StreamController>(); - responseController.stream - .asyncMap((response) async { - // This error indicates a successful invocation to _yieldControlToDDS. - // We don't have a good way to access the list of connected clients - // while also being able to determine which client invoked the RPC - // without some form of client ID. - // - // We can probably do better than this, but it will likely involve some - // refactoring. - if (response case { - 'error': { - 'code': DisconnectNonDartDevelopmentServiceClients.kErrorCode, - }, - }) { - final nonDdsClients = _clientConnections.entries - .where((MapEntry e) => e.key != clientId) - .map((e) => e.value); - await Future.wait([ - for (final client in nonDdsClients) client.sink.close(), - ]); - // Remove the artificial error and return Success. - response.remove('error'); - response['result'] = Success().toJson(); - } - if (onResponse != null) onResponse(response); - return jsonEncode(response); - }) - .listen(channel.sink.add, onError: channel.sink.addError); - final inputStream = channel.stream.map((value) { - if (value is List) { - value = utf8.decode(value); - } else if (value is! String) { - throw StateError( - 'Got value with unexpected type ${value.runtimeType} from web ' - 'socket, expected a List or String.', - ); - } - final request = Map.from(jsonDecode(value)); - if (onRequest != null) onRequest(request); - return request; +/// Common interface for debug services (Chrome or WebSocket based). +abstract class DebugService { + DebugService({ + required this.serverHostname, + required this.ddsConfig, + required this.urlEncoder, + required this.useSse, }); - VmServerConnection( - inputStream, - responseController.sink, - serviceExtensionRegistry, - chromeProxyService, - ).done.whenComplete(() { - _clientConnections.remove(clientId); - if (!_acceptNewConnections && _clientConnections.isEmpty) { - // DDS has disconnected so we can allow for clients to connect directly - // to DWDS. - ChromeDebugService._ddsUri = null; - _acceptNewConnections = true; + + /// The URI pointing to the VM service implementation hosted by the [DebugService]. + String get uri => _uri.toString(); + + Uri get _uri => _cachedUri ??= () { + final dds = _dds; + if (ddsConfig.enable && dds != null) { + return useSse ? dds.sseUri : dds.wsUri; } - }); - _clientConnections[clientId] = channel; -} + return useSse + ? Uri( + scheme: 'sse', + host: _server.address.host, + port: _server.port, + path: '$authToken/\$debugHandler', + ) + : Uri( + scheme: 'ws', + host: _server.address.host, + port: _server.port, + path: authToken, + ); + }(); -void Function(WebSocketChannel, String?) _createNewConnectionHandler( - ChromeProxyService chromeProxyService, - ServiceExtensionRegistry serviceExtensionRegistry, { - void Function(Map)? onRequest, - void Function(Map)? onResponse, -}) { - return (webSocket, subprotocol) { - _handleConnection( - webSocket, - chromeProxyService, - serviceExtensionRegistry, - onRequest: onRequest, - onResponse: onResponse, - ); - }; -} + Uri? _cachedUri; + String? _ddsUri; -Future _handleSseConnections( - SseHandler handler, - ChromeProxyService chromeProxyService, - ServiceExtensionRegistry serviceExtensionRegistry, { - void Function(Map)? onRequest, - void Function(Map)? onResponse, -}) async { - while (await handler.connections.hasNext) { - final connection = await handler.connections.next; - _handleConnection( - connection, - chromeProxyService, - serviceExtensionRegistry, - onRequest: onRequest, - onResponse: onResponse, - ); - } -} + late final T proxyService; -/// Common interface for debug services (Chrome or WebSocket based). -abstract class DebugService { - String get hostname; - int get port; - String get uri; - Future get encodedUri; - ServiceExtensionRegistry get serviceExtensionRegistry; - Future close(); -} + final UrlEncoder? urlEncoder; -/// A Dart Web Debug Service. -/// -/// Creates a [ChromeProxyService] from an existing Chrome instance. -class ChromeDebugService implements DebugService { - static String? _ddsUri; + late final String authToken = _makeAuthToken(); + final bool useSse; - final ChromeProxyService chromeProxyService; - @override - final String hostname; - @override - final ServiceExtensionRegistry serviceExtensionRegistry; - @override - final int port; - final String authToken; - final HttpServer _server; - final bool _useSse; - final DartDevelopmentServiceConfiguration _ddsConfig; - final UrlEncoder? _urlEncoder; + Future get encodedUri async { + return _encodedUri ??= await urlEncoder?.call(uri) ?? uri; + } + + String? _encodedUri; + + DartDevelopmentServiceConfiguration ddsConfig; DartDevelopmentServiceLauncher? _dds; + final String serverHostname; + late final HttpServer _server; + + String get hostname => _uri.host; + int get port => _uri.port; + + final serviceExtensionRegistry = ServiceExtensionRegistry(); + /// Null until [close] is called. /// /// All subsequent calls to [close] will return this future. Future? _closed; - ChromeDebugService._( - this.chromeProxyService, - this.hostname, - this.port, - this.authToken, - this.serviceExtensionRegistry, - this._server, - this._useSse, - this._ddsConfig, - this._urlEncoder, - ); + @protected + @mustCallSuper + @mustBeOverridden + // False positive + // ignore: avoid-redundant-async + Future initialize({required T proxyService}) async { + this.proxyService = proxyService; + } + + @protected + Future serve({required shelf.Handler handler}) async { + _server = await startHttpServer(serverHostname, port: 44456); + serveHttpRequests(_server, handler, (e, s) { + _logger.warning('Error serving requests', e); + emitEvent(DwdsEvent.httpRequestException('$runtimeType', '$e:$s')); + }); + } - @override + /// Closes the debug service and associated resources. Future close() => _closed ??= Future.wait([ _server.close(), if (_dds != null) _dds!.shutdown(), @@ -198,51 +119,29 @@ class ChromeDebugService implements DebugService { Future startDartDevelopmentService() async { // Note: DDS can handle both web socket and SSE connections with no // additional configuration. + final hostname = _server.address.host; _dds = await DartDevelopmentServiceLauncher.start( remoteVmServiceUri: Uri( scheme: 'http', host: hostname, - port: port, + port: _server.port, path: authToken, ), serviceUri: Uri( scheme: 'http', host: hostname, - port: _ddsConfig.port ?? 0, + port: ddsConfig.port ?? 0, ), - devToolsServerAddress: _ddsConfig.devToolsServerAddress, - serveDevTools: _ddsConfig.serveDevTools, + devToolsServerAddress: ddsConfig.devToolsServerAddress, + serveDevTools: ddsConfig.serveDevTools, ); return _dds!; } - @override - String get uri { - final dds = _dds; - if (_ddsConfig.enable && dds != null) { - return (_useSse ? dds.sseUri : dds.wsUri).toString(); - } - return (_useSse - ? Uri( - scheme: 'sse', - host: hostname, - port: port, - path: '$authToken/\$debugHandler', - ) - : Uri(scheme: 'ws', host: hostname, port: port, path: authToken)) - .toString(); - } - - String? _encodedUri; - @override - Future get encodedUri async { - if (_encodedUri != null) return _encodedUri!; - var encoded = uri; - if (_urlEncoder != null) encoded = await _urlEncoder(encoded); - return _encodedUri = encoded; - } - - static void yieldControlToDDS(String uri) { + void yieldControlToDDS(String uri) { + // We track the URI of the connected DDS instance seperately instead of + // relying on _dds being non-null as there's no guarantee that DWDS is the + // tool starting DDS. if (_ddsUri != null) { // This exception is identical to the one thrown from // sdk/lib/vmservice/vmservice.dart @@ -257,255 +156,119 @@ class ChromeDebugService implements DebugService { _ddsUri = uri; } - static Future start( - String hostname, - RemoteDebugger remoteDebugger, - ExecutionContext executionContext, - AssetReader assetReader, - AppConnection appConnection, - UrlEncoder? urlEncoder, { + @protected + shelf.Handler initializeWebSocketHandler({ + required ProxyService proxyService, void Function(Map)? onRequest, void Function(Map)? onResponse, - required DartDevelopmentServiceConfiguration ddsConfig, - bool useSse = false, - ExpressionCompiler? expressionCompiler, - }) async { - final root = assetReader.basePath; - final chromeProxyService = await ChromeProxyService.create( - remoteDebugger, - root, - assetReader, - appConnection, - executionContext, - expressionCompiler, - ); - final authToken = _makeAuthToken(); - final serviceExtensionRegistry = ServiceExtensionRegistry(); - Handler handler; - // DDS will always connect to DWDS via web sockets. - if (useSse && !ddsConfig.enable) { - final sseHandler = SseHandler( - Uri.parse('/$authToken/$_kSseHandlerPath'), - keepAlive: const Duration(seconds: 5), - ); - handler = sseHandler.handler; - safeUnawaited( - _handleSseConnections( - sseHandler, - chromeProxyService, + }) { + return _wrapHandler( + webSocketHandler((webSocket, subprotocol) { + handleConnection( + webSocket, + proxyService, serviceExtensionRegistry, onRequest: onRequest, onResponse: onResponse, - ), - ); - } else { - final innerHandler = webSocketHandler( - _createNewConnectionHandler( - chromeProxyService, - serviceExtensionRegistry, - onRequest: onRequest, - onResponse: onResponse, - ), - ); - handler = (shelf.Request request) { - if (!_acceptNewConnections) { - return shelf.Response.forbidden( - 'Cannot connect directly to the VM service as a Dart Development ' - 'Service (DDS) instance has taken control and can be found at ' - '$_ddsUri.', - ); - } - if (request.url.pathSegments.first != authToken) { - return shelf.Response.forbidden('Incorrect auth token'); - } - return innerHandler(request); - }; - } - final server = await startHttpServer(hostname, port: 44456); - serveHttpRequests(server, handler, (e, s) { - _logger.warning('Error serving requests', e); - emitEvent(DwdsEvent.httpRequestException('DebugService', '$e:$s')); - }); - return ChromeDebugService._( - chromeProxyService, - server.address.host, - server.port, - authToken, - serviceExtensionRegistry, - server, - useSse, - ddsConfig, - urlEncoder, + ); + }), + authToken: authToken, ); } -} - -/// Defines callbacks for sending messages to the connected client. -/// Returns the number of clients the request was successfully sent to. -typedef SendClientRequest = int Function(Object request); - -/// WebSocket-based debug service for web debugging. -class WebSocketDebugService implements DebugService { - @override - final String hostname; - @override - final int port; - final String authToken; - final HttpServer _server; - final WebSocketProxyService _webSocketProxyService; - final ServiceExtensionRegistry _serviceExtensionRegistry; - final UrlEncoder? _urlEncoder; - - Future? _closed; - DartDevelopmentServiceLauncher? _dds; - String? _encodedUri; - - WebSocketDebugService._( - this.hostname, - this.port, - this.authToken, - this._webSocketProxyService, - this._serviceExtensionRegistry, - this._server, - this._urlEncoder, - ); - - /// Returns the WebSocketProxyService instance. - WebSocketProxyService get webSocketProxyService => _webSocketProxyService; - - /// Returns the ServiceExtensionRegistry instance. - @override - ServiceExtensionRegistry get serviceExtensionRegistry => - _serviceExtensionRegistry; - - /// Closes the debug service and associated resources. - @override - Future close() => _closed ??= Future.wait([ - _server.close(), - if (_dds != null) _dds!.shutdown(), - ]); - - /// Starts DDS (Dart Development Service). - Future startDartDevelopmentService({ - int? ddsPort, - }) async { - const timeout = Duration(seconds: 10); - - try { - _dds = await DartDevelopmentServiceLauncher.start( - remoteVmServiceUri: Uri( - scheme: 'http', - host: hostname, - port: port, - path: authToken, - ), - serviceUri: Uri(scheme: 'http', host: hostname, port: ddsPort ?? 0), - ).timeout(timeout); - } catch (e) { - throw Exception('Failed to start DDS: $e'); - } - return _dds!; - } - - @override - String get uri => - Uri(scheme: 'ws', host: hostname, port: port, path: authToken).toString(); - @override - Future get encodedUri async { - if (_encodedUri != null) return _encodedUri!; - var encoded = uri; - if (_urlEncoder != null) encoded = await _urlEncoder(encoded); - return _encodedUri = encoded; + shelf.Handler _wrapHandler(shelf.Handler innerHandler, {String? authToken}) { + return (shelf.Request request) { + if (!_acceptNewConnections) { + return shelf.Response.forbidden( + 'Cannot connect directly to the VM service as a Dart Development ' + 'Service (DDS) instance has taken control and can be found at $_ddsUri.' + '$_ddsUri.', + ); + } + if (authToken != null && request.url.pathSegments.first != authToken) { + return shelf.Response.forbidden('Incorrect auth token'); + } + return innerHandler(request); + }; } - static Future start( - String hostname, - AppConnection appConnection, - AssetReader assetReader, { - required SendClientRequest sendClientRequest, - UrlEncoder? urlEncoder, - }) async { - final authToken = _makeAuthToken(); - final serviceExtensionRegistry = ServiceExtensionRegistry(); - - final webSocketProxyService = await WebSocketProxyService.create( - sendClientRequest, - appConnection, - assetReader.basePath, - ); - - final handler = _createWebSocketHandler( - serviceExtensionRegistry, - webSocketProxyService, - ); - - final server = await startHttpServer(hostname, port: 44456); - serveHttpRequests(server, handler, (e, s) { - Logger('WebSocketDebugService').warning('Error serving requests', e); + @protected + @mustCallSuper + void handleConnection( + StreamChannel channel, + ProxyService proxyService, + ServiceExtensionRegistry serviceExtensionRegistry, { + void Function(Map)? onRequest, + void Function(Map)? onResponse, + }) { + final clientId = _clientId++; + final responseController = StreamController>(); + responseController.stream + .asyncMap((response) async { + // This error indicates a successful invocation to _yieldControlToDDS. + // We don't have a good way to access the list of connected clients + // while also being able to determine which client invoked the RPC + // without some form of client ID. + // + // We can probably do better than this, but it will likely involve some + // refactoring. + if (response case { + 'error': { + 'code': DisconnectNonDartDevelopmentServiceClients.kErrorCode, + }, + }) { + final nonDdsClients = _clientConnections.entries + .where((MapEntry e) => e.key != clientId) + .map((e) => e.value); + await Future.wait([ + for (final client in nonDdsClients) client.sink.close(), + ]); + // Remove the artificial error and return Success. + response.remove('error'); + response['result'] = Success().toJson(); + } + if (onResponse != null) onResponse(response); + return jsonEncode(response); + }) + .listen(channel.sink.add, onError: channel.sink.addError); + final inputStream = channel.stream.map((value) { + if (value is List) { + value = utf8.decode(value); + } else if (value is! String) { + throw StateError( + 'Got value with unexpected type ${value.runtimeType} from web ' + 'socket, expected a List or String.', + ); + } + final request = Map.from(jsonDecode(value)); + if (onRequest != null) onRequest(request); + return request; }); - - return WebSocketDebugService._( - server.address.host, - server.port, - authToken, - webSocketProxyService, + VmServerConnection( + inputStream, + responseController.sink, serviceExtensionRegistry, - server, - urlEncoder, - ); - } - - /// Creates the WebSocket handler for incoming connections. - static Handler _createWebSocketHandler( - ServiceExtensionRegistry serviceExtensionRegistry, - WebSocketProxyService webSocketProxyService, - ) { - return webSocketHandler(( - WebSocketChannel webSocket, - String? subprotocol, - ) async { - final clientId = _clientId++; - final responseController = StreamController>(); - unawaited( - webSocket.sink.addStream(responseController.stream.map(jsonEncode)), - ); - - final inputStream = webSocket.stream.map((value) { - if (value is List) { - value = utf8.decode(value); - } else if (value is! String) { - throw StateError( - 'Unexpected value type from web socket: ${value.runtimeType}', - ); - } - return Map.from(jsonDecode(value)); - }); - - _clientConnections[clientId] = webSocket; - - unawaited( - VmServerConnection( - inputStream, - responseController.sink, - serviceExtensionRegistry, - webSocketProxyService, - ).done.whenComplete(() { - _clientConnections.remove(clientId); - }), - ); - await webSocketProxyService.sendServiceExtensionRegisteredEvents(); + proxyService, + ).done.whenComplete(() { + _clientConnections.remove(clientId); + if (!_acceptNewConnections && _clientConnections.isEmpty) { + // DDS has disconnected so we can allow for clients to connect directly + // to DWDS. + _ddsUri = null; + _acceptNewConnections = true; + } }); + _clientConnections[clientId] = channel; } -} -// Creates a random auth token for more secure connections. -String _makeAuthToken() { - final tokenBytes = 8; - final bytes = Uint8List(tokenBytes); - final random = Random.secure(); - for (var i = 0; i < tokenBytes; i++) { - bytes[i] = random.nextInt(256); + // Creates a random auth token for more secure connections. + String _makeAuthToken() { + final tokenBytes = 8; + final bytes = Uint8List(tokenBytes); + final random = Random.secure(); + for (var i = 0; i < tokenBytes; i++) { + bytes[i] = random.nextInt(256); + } + return base64Url.encode(bytes); } - return base64Url.encode(bytes); } diff --git a/dwds/lib/src/services/proxy_service.dart b/dwds/lib/src/services/proxy_service.dart index 6140cf0a2..5e551a3ed 100644 --- a/dwds/lib/src/services/proxy_service.dart +++ b/dwds/lib/src/services/proxy_service.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:dwds/data/debug_event.dart'; import 'package:dwds/data/hot_reload_response.dart'; @@ -12,16 +13,49 @@ import 'package:dwds/data/service_extension_response.dart'; import 'package:dwds/src/connections/app_connection.dart'; import 'package:dwds/src/debugging/inspector.dart'; import 'package:dwds/src/events.dart'; +import 'package:dwds/src/services/debug_service.dart'; +import 'package:dwds/src/utilities/dart_uri.dart'; import 'package:dwds/src/utilities/shared.dart'; import 'package:meta/meta.dart'; import 'package:pub_semver/pub_semver.dart' as semver; import 'package:vm_service/vm_service.dart' as vm_service; +import 'package:vm_service/vm_service.dart'; import 'package:vm_service_interface/vm_service_interface.dart'; -const pauseIsolatesOnStartFlag = 'pause_isolates_on_start'; +// This event is identical to the one sent by the VM service from +// sdk/lib/vmservice/vmservice.dart before existing VM service clients are +// disconnected. +final class DartDevelopmentServiceConnectedEvent extends Event { + DartDevelopmentServiceConnectedEvent({ + required super.timestamp, + required this.uri, + }) : message = + 'A Dart Developer Service instance has connected and this direct ' + 'connection to the VM service will now be closed. Please reconnect to ' + 'the Dart Development Service at $uri.', + super(kind: 'DartDevelopmentServiceConnected'); + + final String message; + final String uri; + + @override + Map toJson() => { + ...super.toJson(), + 'uri': uri, + 'message': message, + }; +} + +final class DisconnectNonDartDevelopmentServiceClients extends RPCError { + DisconnectNonDartDevelopmentServiceClients() + : super('_yieldControlToDDS', kErrorCode); + + // Arbitrary error code that's unlikely to be used elsewhere. + static const kErrorCode = -199328; +} /// Abstract base class for VM service proxy implementations. -abstract class ProxyService +abstract base class ProxyService implements VmServiceInterface { /// Cache of all existing StreamControllers. /// @@ -50,24 +84,29 @@ abstract class ProxyService /// a hot restart. bool get isIsolateRunning => _inspector != null; + /// The [DebugService] implementation. + final DebugService debugService; + /// The root `VM` instance. - final vm_service.VM _vm; + final vm_service.VM vm; /// Signals when isolate is initialized. Future get isInitialized => initializedCompleter.future; Completer initializedCompleter = Completer(); + static const _kPauseIsolatesOnStartFlag = 'pause_isolates_on_start'; + /// The flags that can be set at runtime via [setFlag] and their respective /// values. final Map _currentVmServiceFlags = { - pauseIsolatesOnStartFlag: false, + _kPauseIsolatesOnStartFlag: false, }; - /// The value of the [pauseIsolatesOnStartFlag]. + /// The value of the [_kPauseIsolatesOnStartFlag]. /// /// This value can be updated at runtime via [setFlag]. bool get pauseIsolatesOnStart => - _currentVmServiceFlags[pauseIsolatesOnStartFlag] ?? false; + _currentVmServiceFlags[_kPauseIsolatesOnStartFlag] ?? false; /// Stream controller for resume events after restart. final _resumeAfterRestartEventsController = @@ -87,7 +126,6 @@ abstract class ProxyService bool get hasPendingRestart => _resumeAfterRestartEventsController.hasListener; // Protected accessors for subclasses - vm_service.VM get vm => _vm; Map> get streamControllers => _streamControllers; StreamController get resumeAfterRestartEventsController => @@ -97,7 +135,11 @@ abstract class ProxyService /// The root at which we're serving. final String root; - ProxyService(this._vm, this.root); + ProxyService({ + required this.vm, + required this.root, + required this.debugService, + }); /// Sends events to stream controllers. void streamNotify(String streamId, vm_service.Event event) { @@ -114,11 +156,32 @@ abstract class ProxyService }).stream; } + @override + Future yieldControlToDDS(String uri) async { + // This will throw an RPCError if there's already an existing DDS instance. + debugService.yieldControlToDDS(uri); + + // Notify existing clients that DDS has connected and they're about to be + // disconnected. + final event = DartDevelopmentServiceConnectedEvent( + timestamp: DateTime.now().millisecondsSinceEpoch, + uri: uri, + ); + streamNotify(EventStreams.kService, event); + + // We throw since we have no other way to control what the response content + // is for this RPC. The debug service will check for this particular + // exception as a signal to close connections to all other clients. + throw DisconnectNonDartDevelopmentServiceClients(); + } + @override Future streamListen(String streamId) => wrapInErrorHandlerAsync('streamListen', () => _streamListen(streamId)); Future _streamListen(String streamId) async { + // TODO: This should return an error if the stream is already being listened + // to. onEvent(streamId); return vm_service.Success(); } @@ -135,10 +198,52 @@ abstract class ProxyService Future _getVM() { return captureElapsedTime(() async { - return _vm; + return vm; }, (result) => DwdsEvent.getVM()); } + @override + Future getIsolate(String isolateId) => + wrapInErrorHandlerAsync('getIsolate', () => _getIsolate(isolateId)); + + Future _getIsolate(String isolateId) { + return captureElapsedTime(() async { + await isInitialized; + checkIsolate('getIsolate', isolateId); + return inspector.isolate; + }, (result) => DwdsEvent.getIsolate()); + } + + @override + Future setName(String isolateId, String name) => + wrapInErrorHandlerAsync('setName', () => _setName(isolateId, name)); + + Future _setName(String isolateId, String name) async { + await isInitialized; + checkIsolate('setName', isolateId); + inspector.isolate.name = name; + return Success(); + } + + @override + Future setVMName(String name) => + wrapInErrorHandlerAsync('setVMName', () => _setVMName(name)); + + Future _setVMName(String name) async { + vm.name = name; + streamNotify( + 'VM', + Event( + kind: EventKind.kVMUpdate, + timestamp: DateTime.now().millisecondsSinceEpoch, + // We are not guaranteed to have an isolate at this point in time. + isolate: null, + vm: toVMRef(vm), + ), + ); + return Success(); + } + @override Future getFlagList() => wrapInErrorHandlerAsync('getFlagList', _getFlagList); @@ -195,6 +300,55 @@ abstract class ProxyService return vm_service.Version(major: version.major, minor: version.minor); } + // Note: Ignore the optional local parameter, when it is set to `true` the + // request is intercepted and handled by DDS. + @override + Future lookupResolvedPackageUris( + String isolateId, + List uris, { + bool? local, + }) => wrapInErrorHandlerAsync( + 'lookupResolvedPackageUris', + () => _lookupResolvedPackageUris(isolateId, uris), + ); + + Future _lookupResolvedPackageUris( + String isolateId, + List uris, + ) async { + await isInitialized; + checkIsolate('lookupResolvedPackageUris', isolateId); + return UriList(uris: uris.map(DartUri.toResolvedUri).toList()); + } + + @override + Future lookupPackageUris(String isolateId, List uris) => + wrapInErrorHandlerAsync( + 'lookupPackageUris', + () => _lookupPackageUris(isolateId, uris), + ); + + Future _lookupPackageUris( + String isolateId, + List uris, + ) async { + await isInitialized; + checkIsolate('lookupPackageUris', isolateId); + return UriList(uris: uris.map(DartUri.toPackageUri).toList()); + } + + @override + Future getScripts(String isolateId) => + wrapInErrorHandlerAsync('getScripts', () => _getScripts(isolateId)); + + Future _getScripts(String isolateId) { + return captureElapsedTime(() async { + await isInitialized; + checkIsolate('getScripts', isolateId); + return inspector.getScripts(); + }, (result) => DwdsEvent.getScripts()); + } + /// Parses the [BatchedDebugEvents] and emits corresponding Dart VM Service /// protocol [Event]s. void parseBatchedDebugEvents(BatchedDebugEvents debugEvents) { @@ -205,11 +359,45 @@ abstract class ProxyService /// Parses the [DebugEvent] and emits a corresponding Dart VM Service /// protocol [Event]. - void parseDebugEvent(DebugEvent debugEvent); + @mustCallSuper + void parseDebugEvent(DebugEvent debugEvent) { + if (!isIsolateRunning) return; + final isolateRef = inspector.isolateRef; + + streamNotify( + EventStreams.kExtension, + Event( + kind: EventKind.kExtension, + timestamp: DateTime.now().millisecondsSinceEpoch, + isolate: isolateRef, + ) + ..extensionKind = debugEvent.kind + ..extensionData = ExtensionData.parse( + jsonDecode(debugEvent.eventData) as Map, + ), + ); + } /// Parses the [RegisterEvent] and emits a corresponding Dart VM Service /// protocol [Event]. - void parseRegisterEvent(RegisterEvent registerEvent); + @mustCallSuper + void parseRegisterEvent(RegisterEvent registerEvent) { + if (!isIsolateRunning) return; + + final isolate = inspector.isolate; + final isolateRef = inspector.isolateRef; + final service = registerEvent.eventData; + isolate.extensionRPCs?.add(service); + + streamNotify( + EventStreams.kIsolate, + Event( + kind: EventKind.kServiceExtensionAdded, + timestamp: DateTime.now().millisecondsSinceEpoch, + isolate: isolateRef, + )..extensionRPC = service, + ); + } /// Completes hot reload with response from client. /// @@ -371,6 +559,15 @@ abstract class ProxyService Future getProcessMemoryUsage() => _rpcNotSupportedFuture('getProcessMemoryUsage'); + @override + Future setLibraryDebuggable( + String isolateId, + String libraryId, + bool isDebuggable, + ) { + return rpcNotSupportedFuture('setLibraryDebuggable'); + } + @override Future getPorts(String isolateId) => throw UnimplementedError(); @@ -416,6 +613,11 @@ abstract class ProxyService return _rpcNotSupportedFuture('clearCpuSamples'); } + @override + Future registerService(String service, String alias) { + return rpcNotSupportedFuture('registerService'); + } + /// Creates a new isolate for debugging. /// /// Implementations should handle isolate lifecycle management according to @@ -431,6 +633,32 @@ abstract class ProxyService /// debugging mode and connection management strategy. void destroyIsolate(); + /// Validate that isolateId matches the current isolate we're connected to and + /// return that isolate. + /// + /// This is useful to call at the beginning of API methods that are passed an + /// isolate id. + @protected + Isolate checkIsolate(String methodName, String? isolateId) { + final currentIsolateId = inspector.isolate.id; + if (currentIsolateId == null) { + throw StateError('No running isolate ID'); + } + if (isolateId != currentIsolateId) { + _throwSentinel( + methodName, + SentinelKind.kCollected, + 'Unrecognized isolateId: $isolateId', + ); + } + return inspector.isolate; + } + + static Never _throwSentinel(String method, String kind, String message) { + final data = {'kind': kind, 'valueAsString': message}; + throw SentinelException.parse(method, data); + } + /// Prevent DWDS from blocking Dart SDK rolls if changes in package:vm_service /// are unimplemented in DWDS. @override diff --git a/dwds/lib/src/services/web_socket/web_socket_debug_service.dart b/dwds/lib/src/services/web_socket/web_socket_debug_service.dart new file mode 100644 index 000000000..36ab4da3d --- /dev/null +++ b/dwds/lib/src/services/web_socket/web_socket_debug_service.dart @@ -0,0 +1,58 @@ +// Copyright (c) 2025, 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. + +import 'package:dwds/asset_reader.dart'; +import 'package:dwds/src/config/tool_configuration.dart'; +import 'package:dwds/src/connections/app_connection.dart'; +import 'package:dwds/src/services/debug_service.dart'; +import 'package:dwds/src/services/web_socket/web_socket_proxy_service.dart'; +import 'package:meta/meta.dart'; + +/// Defines callbacks for sending messages to the connected client. +/// Returns the number of clients the request was successfully sent to. +typedef SendClientRequest = int Function(Object request); + +/// WebSocket-based debug service for web debugging. +final class WebSocketDebugService extends DebugService { + WebSocketDebugService._({ + required super.serverHostname, + required super.ddsConfig, + required super.urlEncoder, + }) : super( + // The web socket debug service doesn't support SSE connections. + useSse: false, + ); + + @protected + @override + Future initialize({required WebSocketProxyService proxyService}) async { + await super.initialize(proxyService: proxyService); + await serve( + handler: initializeWebSocketHandler(proxyService: proxyService), + ); + } + + static Future start({ + required String hostname, + required AppConnection appConnection, + required AssetReader assetReader, + required SendClientRequest sendClientRequest, + required DartDevelopmentServiceConfiguration ddsConfig, + UrlEncoder? urlEncoder, + }) async { + final debugService = WebSocketDebugService._( + serverHostname: hostname, + ddsConfig: ddsConfig, + urlEncoder: urlEncoder, + ); + final webSocketProxyService = await WebSocketProxyService.create( + sendClientRequest, + appConnection, + assetReader.basePath, + debugService, + ); + await debugService.initialize(proxyService: webSocketProxyService); + return debugService; + } +} diff --git a/dwds/lib/src/services/web_socket_proxy_service.dart b/dwds/lib/src/services/web_socket/web_socket_proxy_service.dart similarity index 81% rename from dwds/lib/src/services/web_socket_proxy_service.dart rename to dwds/lib/src/services/web_socket/web_socket_proxy_service.dart index 49faa71b5..53b01c49b 100644 --- a/dwds/lib/src/services/web_socket_proxy_service.dart +++ b/dwds/lib/src/services/web_socket/web_socket_proxy_service.dart @@ -5,19 +5,16 @@ import 'dart:async'; import 'dart:convert'; -import 'package:dwds/data/debug_event.dart'; import 'package:dwds/data/hot_reload_request.dart'; import 'package:dwds/data/hot_reload_response.dart'; import 'package:dwds/data/hot_restart_request.dart'; import 'package:dwds/data/hot_restart_response.dart'; -import 'package:dwds/data/register_event.dart'; import 'package:dwds/data/service_extension_request.dart'; import 'package:dwds/data/service_extension_response.dart'; import 'package:dwds/src/connections/app_connection.dart'; import 'package:dwds/src/debugging/web_socket_inspector.dart'; -import 'package:dwds/src/events.dart'; import 'package:dwds/src/services/proxy_service.dart'; -import 'package:dwds/src/utilities/dart_uri.dart'; +import 'package:dwds/src/services/web_socket/web_socket_debug_service.dart'; import 'package:dwds/src/utilities/shared.dart'; import 'package:logging/logging.dart'; import 'package:vm_service/vm_service.dart' as vm_service; @@ -132,12 +129,11 @@ class NoClientsAvailableException implements Exception { } /// WebSocket-based VM service proxy for web debugging. -class WebSocketProxyService extends ProxyService { +final class WebSocketProxyService extends ProxyService { final _logger = Logger('WebSocketProxyService'); /// Active service extension trackers by request ID. - final Map _pendingServiceExtensionTrackers = - {}; + final _pendingServiceExtensionTrackers = {}; /// Sends messages to the client. final SendClientRequest sendClientRequest; @@ -146,24 +142,55 @@ class WebSocketProxyService extends ProxyService { AppConnection appConnection; /// Active hot reload trackers by request ID. - final Map _pendingHotReloads = {}; + final _pendingHotReloads = {}; /// Active hot restart trackers by request ID. - final Map _pendingHotRestarts = {}; + final _pendingHotRestarts = {}; /// App connection cleanup subscriptions by connection instance ID. - final Map> _appConnectionDoneSubscriptions = - {}; + final _appConnectionDoneSubscriptions = >{}; /// Active connection count for this service. int _activeConnectionCount = 0; - WebSocketProxyService._( - this.sendClientRequest, - vm_service.VM vm, + WebSocketProxyService._({ + required this.sendClientRequest, + required super.vm, + required super.root, + required super.debugService, + required this.appConnection, + }); + + static Future create( + SendClientRequest sendClientRequest, + AppConnection appConnection, String root, - this.appConnection, - ) : super(vm, root); + WebSocketDebugService debugService, + ) async { + final vm = vm_service.VM( + name: 'WebSocketDebugProxy', + operatingSystem: 'web', + startTime: DateTime.now().millisecondsSinceEpoch, + version: 'unknown', + isolates: [], + isolateGroups: [], + systemIsolates: [], + systemIsolateGroups: [], + targetCPU: 'Web', + hostCPU: 'DWDS', + architectureBits: -1, + pid: -1, + ); + final service = WebSocketProxyService._( + sendClientRequest: sendClientRequest, + vm: vm, + root: root, + debugService: debugService, + appConnection: appConnection, + ); + safeUnawaited(service.createIsolate(appConnection)); + return service; + } // Isolate state vm_service.Event? _currentPauseEvent; @@ -225,7 +252,7 @@ class WebSocketProxyService extends ProxyService { ); // Send lifecycle events - _streamNotify( + streamNotify( vm_service.EventStreams.kIsolate, vm_service.Event( kind: vm_service.EventKind.kIsolateStart, @@ -233,7 +260,7 @@ class WebSocketProxyService extends ProxyService { isolate: isolateRef, ), ); - _streamNotify( + streamNotify( vm_service.EventStreams.kIsolate, vm_service.Event( kind: vm_service.EventKind.kIsolateRunnable, @@ -250,7 +277,7 @@ class WebSocketProxyService extends ProxyService { isolate: isolateRef, ); _currentPauseEvent = pauseEvent; - _streamNotify(vm_service.EventStreams.kDebug, pauseEvent); + streamNotify(vm_service.EventStreams.kDebug, pauseEvent); } // Complete initialization after isolate is set up @@ -343,7 +370,7 @@ class WebSocketProxyService extends ProxyService { final isolateRef = inspector.isolateRef; // Send exit event - _streamNotify( + streamNotify( vm_service.EventStreams.kIsolate, vm_service.Event( kind: vm_service.EventKind.kIsolateExit, @@ -364,22 +391,6 @@ class WebSocketProxyService extends ProxyService { } } - /// Sends events to stream controllers. - void _streamNotify(String streamId, vm_service.Event event) { - final controller = streamControllers[streamId]; - if (controller == null) return; - controller.add(event); - } - - @override - Future setLibraryDebuggable( - String isolateId, - String libraryId, - bool isDebuggable, - ) { - return rpcNotSupportedFuture('setLibraryDebuggable'); - } - @override Future setIsolatePauseMode( String isolateId, { @@ -390,104 +401,6 @@ class WebSocketProxyService extends ProxyService { return Success(); } - @override - Future getIsolate(String isolateId) => - wrapInErrorHandlerAsync('getIsolate', () => _getIsolate(isolateId)); - - Future _getIsolate(String isolateId) async { - final isolate = inspector.isolate; - if (!isIsolateRunning) { - throw vm_service.RPCError( - 'getIsolate', - vm_service.RPCErrorKind.kInvalidParams.code, - 'No running isolate found for id: $isolateId', - ); - } - if (isolate.id != isolateId) { - throw vm_service.RPCError( - 'getIsolate', - vm_service.RPCErrorKind.kInvalidParams.code, - 'Isolate with id $isolateId not found.', - ); - } - - return isolate; - } - - @override - Future getScripts(String isolateId) => inspector.getScripts(); - - /// Adds events to stream controllers. - void addEvent(String streamId, vm_service.Event event) { - final controller = streamControllers[streamId]; - if (controller != null && !controller.isClosed) { - controller.add(event); - } else { - _logger.warning('Cannot add event to closed/missing stream: $streamId'); - } - } - - static Future create( - SendClientRequest sendClientRequest, - AppConnection appConnection, - String root, - ) async { - final vm = vm_service.VM( - name: 'WebSocketDebugProxy', - operatingSystem: 'web', - startTime: DateTime.now().millisecondsSinceEpoch, - version: 'unknown', - isolates: [], - isolateGroups: [], - systemIsolates: [], - systemIsolateGroups: [], - targetCPU: 'Web', - hostCPU: 'DWDS', - architectureBits: -1, - pid: -1, - ); - final service = WebSocketProxyService._( - sendClientRequest, - vm, - root, - appConnection, - ); - safeUnawaited(service.createIsolate(appConnection)); - return service; - } - - /// Returns the root VM object. - @override - Future getVM() => wrapInErrorHandlerAsync('getVM', _getVM); - - Future _getVM() { - return captureElapsedTime(() async { - // Ensure the VM's isolate list is synchronized with our actual state - if (isIsolateRunning) { - final isolateRef = inspector.isolateRef; - // Make sure our isolate is in the VM's isolate list - final isolateExists = - vm.isolates?.any((ref) => ref.id == isolateRef.id) ?? false; - if (!isolateExists) { - vm.isolates?.add(isolateRef); - } - } else { - // If no isolate is running, make sure the list is empty - vm.isolates?.clear(); - } - - return vm; - }, (result) => DwdsEvent.getVM()); - } - - /// Not available in WebSocket mode. - dynamic get remoteDebugger { - throw UnsupportedError( - 'remoteDebugger not available in WebSocketProxyService.\n' - 'Called from:\n${StackTrace.current}', - ); - } - @override Future reloadSources( String isolateId, { @@ -516,7 +429,7 @@ class WebSocketProxyService extends ProxyService { } /// Handles hot restart requests. - Future> hotRestart() async { + Future> hotRestart() async { _logger.info('Attempting a hot restart'); try { @@ -853,60 +766,6 @@ class WebSocketProxyService extends ProxyService { } } - /// Parses the [RegisterEvent] and emits a corresponding Dart VM Service - /// protocol [Event]. - @override - void parseRegisterEvent(RegisterEvent registerEvent) { - if (!isIsolateRunning) { - _logger.warning('Cannot register service extension - no isolate running'); - return; - } - - final service = registerEvent.eventData; - - // Emit ServiceExtensionAdded event for tooling - final event = vm_service.Event( - kind: vm_service.EventKind.kServiceExtensionAdded, - timestamp: DateTime.now().millisecondsSinceEpoch, - isolate: inspector.isolateRef, - ); - event.extensionRPC = service; - - _streamNotify(vm_service.EventStreams.kIsolate, event); - } - - /// Parses the [BatchedDebugEvents] and emits corresponding Dart VM Service - /// protocol [Event]s. - @override - void parseBatchedDebugEvents(BatchedDebugEvents debugEvents) { - for (final debugEvent in debugEvents.events) { - parseDebugEvent(debugEvent); - } - } - - /// Parses the [DebugEvent] and emits a corresponding Dart VM Service - /// protocol [Event]. - @override - void parseDebugEvent(DebugEvent debugEvent) { - if (!isIsolateRunning) { - _logger.warning('Cannot parse debug event - no isolate running'); - return; - } - - _streamNotify( - vm_service.EventStreams.kExtension, - vm_service.Event( - kind: vm_service.EventKind.kExtension, - timestamp: DateTime.now().millisecondsSinceEpoch, - isolate: inspector.isolateRef, - ) - ..extensionKind = debugEvent.kind - ..extensionData = vm_service.ExtensionData.parse( - jsonDecode(debugEvent.eventData) as Map, - ), - ); - } - @override Future setFlag(String name, String value) => wrapInErrorHandlerAsync('setFlag', () => _setFlag(name, value)); @@ -932,31 +791,13 @@ class WebSocketProxyService extends ProxyService { isolate: inspector.isolateRef, ); _currentPauseEvent = pauseEvent; - _streamNotify(vm_service.EventStreams.kDebug, pauseEvent); + streamNotify(vm_service.EventStreams.kDebug, pauseEvent); } } return Success(); } - @override - Future lookupResolvedPackageUris( - String isolateId, - List uris, { - bool? local, - }) => wrapInErrorHandlerAsync( - 'lookupResolvedPackageUris', - () => _lookupResolvedPackageUris(isolateId, uris), - ); - - Future _lookupResolvedPackageUris( - String _, - List uris, - ) async { - await isInitialized; - return UriList(uris: uris.map(DartUri.toResolvedUri).toList()); - } - /// Pauses execution of the isolate. @override Future pause(String isolateId) => @@ -994,29 +835,12 @@ class WebSocketProxyService extends ProxyService { timestamp: DateTime.now().millisecondsSinceEpoch, isolate: inspector.isolateRef, ); - _streamNotify(vm_service.EventStreams.kDebug, resumeEvent); + streamNotify(vm_service.EventStreams.kDebug, resumeEvent); } return Success(); } - @override - Future lookupPackageUris(String isolateId, List uris) => - wrapInErrorHandlerAsync( - 'lookupPackageUris', - () => _lookupPackageUris(isolateId, uris), - ); - - Future _lookupPackageUris(String _, List uris) async { - await isInitialized; - return UriList(uris: uris.map(DartUri.toPackageUri).toList()); - } - - @override - Future registerService(String service, String alias) { - return rpcNotSupportedFuture('registerService'); - } - @override Future getFlagList() => wrapInErrorHandlerAsync('getFlagList', _getFlagList); diff --git a/dwds/lib/src/utilities/server.dart b/dwds/lib/src/utilities/server.dart index 81a07636b..e67dfe355 100644 --- a/dwds/lib/src/utilities/server.dart +++ b/dwds/lib/src/utilities/server.dart @@ -4,7 +4,7 @@ import 'dart:io'; -import 'package:dwds/src/services/chrome_debug_exception.dart'; +import 'package:dwds/src/services/chrome/chrome_debug_exception.dart'; import 'package:http_multi_server/http_multi_server.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart'; diff --git a/dwds/lib/src/version.dart b/dwds/lib/src/version.dart index ef59e4b8a..78eae7b12 100644 --- a/dwds/lib/src/version.dart +++ b/dwds/lib/src/version.dart @@ -1,2 +1,2 @@ // Generated code. Do not modify. -const packageVersion = '26.2.0'; +const packageVersion = '26.2.1-wip'; diff --git a/dwds/pubspec.yaml b/dwds/pubspec.yaml index 05edd4e5a..738e97049 100644 --- a/dwds/pubspec.yaml +++ b/dwds/pubspec.yaml @@ -1,6 +1,6 @@ name: dwds # Every time this changes you need to run `dart run build_runner build`. -version: 26.2.0 +version: 26.2.1-wip description: >- A service that proxies between the Chrome debug protocol and the Dart VM diff --git a/dwds/test/common/chrome_proxy_service_common.dart b/dwds/test/common/chrome_proxy_service_common.dart index 4affe62c0..0c403d48d 100644 --- a/dwds/test/common/chrome_proxy_service_common.dart +++ b/dwds/test/common/chrome_proxy_service_common.dart @@ -12,7 +12,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:dwds/expression_compiler.dart'; -import 'package:dwds/src/services/chrome_proxy_service.dart'; +import 'package:dwds/src/services/chrome/chrome_proxy_service.dart'; import 'package:dwds/src/utilities/dart_uri.dart'; import 'package:dwds/src/utilities/shared.dart'; import 'package:http/http.dart' as http; diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart index 22e5326d1..5b7f4da55 100644 --- a/dwds/test/fixtures/context.dart +++ b/dwds/test/fixtures/context.dart @@ -18,7 +18,7 @@ import 'package:dwds/src/loaders/build_runner_require.dart'; import 'package:dwds/src/loaders/frontend_server_strategy_provider.dart'; import 'package:dwds/src/loaders/strategy.dart'; import 'package:dwds/src/readers/proxy_server_asset_reader.dart'; -import 'package:dwds/src/services/chrome_proxy_service.dart'; +import 'package:dwds/src/services/chrome/chrome_proxy_service.dart'; import 'package:dwds/src/services/expression_compiler.dart'; import 'package:dwds/src/services/expression_compiler_service.dart'; import 'package:dwds/src/utilities/dart_uri.dart'; diff --git a/dwds/test/variable_scope_test.dart b/dwds/test/variable_scope_test.dart index 7f56319c3..14bd0cf0d 100644 --- a/dwds/test/variable_scope_test.dart +++ b/dwds/test/variable_scope_test.dart @@ -7,7 +7,7 @@ library; import 'package:dwds/src/debugging/dart_scope.dart'; -import 'package:dwds/src/services/chrome_proxy_service.dart'; +import 'package:dwds/src/services/chrome/chrome_proxy_service.dart'; import 'package:test/test.dart'; import 'package:test_common/logging.dart'; import 'package:test_common/test_sdk_configuration.dart';