From d1944ff25489720809c65d37ad1753f9eb81d19e Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Fri, 31 Oct 2025 11:53:36 -0400 Subject: [PATCH 1/7] [ DWDS ] Launch DDS when using the web socket proxy service DDS is needed to serve DevTools, so the web socket proxy service should be setup to launch DDS at startup. This change also includes some significant refactors to reduce the amount of duplicate code that could be shared by the Chrome and web socket service implementations. --- dwds/lib/dwds.dart | 3 +- .../lib/src/connections/debug_connection.dart | 2 +- dwds/lib/src/debugging/classes.dart | 2 +- dwds/lib/src/debugging/debugger.dart | 2 +- dwds/lib/src/debugging/libraries.dart | 2 +- dwds/lib/src/debugging/metadata/class.dart | 2 +- .../src/debugging/web_socket_inspector.dart | 2 +- dwds/lib/src/dwds_vm_client.dart | 1008 ++++++++--------- dwds/lib/src/handlers/dev_handler.dart | 66 +- dwds/lib/src/servers/extension_debugger.dart | 2 +- dwds/lib/src/services/app_debug_services.dart | 115 +- .../{ => chrome}/chrome_debug_exception.dart | 2 +- .../services/chrome/chrome_debug_service.dart | 112 ++ .../{ => chrome}/chrome_proxy_service.dart | 119 +- dwds/lib/src/services/debug_service.dart | 589 +++------- dwds/lib/src/services/proxy_service.dart | 77 +- .../web_socket/web_socket_debug_service.dart | 58 + .../web_socket_proxy_service.dart | 36 +- dwds/lib/src/utilities/server.dart | 2 +- .../common/chrome_proxy_service_common.dart | 2 +- dwds/test/fixtures/context.dart | 2 +- dwds/test/variable_scope_test.dart | 2 +- 22 files changed, 998 insertions(+), 1209 deletions(-) rename dwds/lib/src/services/{ => chrome}/chrome_debug_exception.dart (95%) create mode 100644 dwds/lib/src/services/chrome/chrome_debug_service.dart rename dwds/lib/src/services/{ => chrome}/chrome_proxy_service.dart (95%) create mode 100644 dwds/lib/src/services/web_socket/web_socket_debug_service.dart rename dwds/lib/src/services/{ => web_socket}/web_socket_proxy_service.dart (97%) 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..788cc13fc 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'; diff --git a/dwds/lib/src/dwds_vm_client.dart b/dwds/lib/src/dwds_vm_client.dart index 72549e57f..a20669a95 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,154 @@ 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(); - static Future _registerServiceExtensions({ + final _clientCompleter = Completer(); + + @override + final logger = Logger('DwdsVmClient'); + + ChromeDwdsVmClient({required super.debugService}); + + static Future create( + ChromeDebugService debugService, + DwdsStats dwdsStats, + Uri? ddsUri, + ) async { + final dwdsVmClient = ChromeDwdsVmClient(debugService: debugService); + await dwdsVmClient.initialize(ddsUri: ddsUri); + return dwdsVmClient; + } + + @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 +340,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.'); + throw StateError('No context with the running Dart application.'); } -} -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.'); -} - -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..ade73a6c5 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, @@ -856,13 +856,11 @@ class DevHandler { dwdsStats, dds?.wsUri, ); - final appDebugService = ChromeAppDebugServices( - debugService, - vmClient, - dwdsStats, - dds?.wsUri, - dds?.devToolsUri, - dds?.dtdUri, + 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 +887,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 +972,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/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..c5e6df256 --- /dev/null +++ b/dwds/lib/src/services/chrome/chrome_debug_service.dart @@ -0,0 +1,112 @@ +// 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); + 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 95% rename from dwds/lib/src/services/chrome_proxy_service.dart rename to dwds/lib/src/services/chrome/chrome_proxy_service.dart index 7f84ac10b..bf56a9804 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; @@ -1541,25 +1515,6 @@ 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; diff --git a/dwds/lib/src/services/debug_service.dart b/dwds/lib/src/services/debug_service.dart index d6a9fa402..07aed226b 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,86 @@ 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; - }); - 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; - } +/// 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, }); - _clientConnections[clientId] = channel; -} -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, - ); - }; -} + /// The URI pointing to the VM service implementation hosted by the [DebugService]. + String get uri => _uri.toString(); -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, - ); + Uri get _uri { + final dds = _dds; + if (ddsConfig.enable && dds != null) { + return useSse ? dds.sseUri : dds.wsUri; + } + 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, + ); } -} -/// 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(); -} + String? _ddsUri; + + late final T proxyService; -/// A Dart Web Debug Service. -/// -/// Creates a [ChromeProxyService] from an existing Chrome instance. -class ChromeDebugService implements DebugService { - static String? _ddsUri; + final UrlEncoder? urlEncoder; + + late final String authToken = _makeAuthToken(); + final bool useSse; + + Future get encodedUri async { + return _encodedUri ??= await urlEncoder?.call(uri) ?? uri; + } + + String? _encodedUri; - 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; + 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 + 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 +116,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 +153,118 @@ 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, - serviceExtensionRegistry, - onRequest: onRequest, - onResponse: onResponse, - ), - ); - } else { - final innerHandler = webSocketHandler( - _createNewConnectionHandler( - chromeProxyService, + }) { + return _wrapHandler( + webSocketHandler((webSocket, subprotocol) { + handleConnection( + webSocket, + proxyService, 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!; + 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); + }; } - @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; - } - - 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 + 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..b7319cb34 100644 --- a/dwds/lib/src/services/proxy_service.dart +++ b/dwds/lib/src/services/proxy_service.dart @@ -12,16 +12,48 @@ 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/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 +82,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 +124,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 +133,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,6 +154,25 @@ 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)); @@ -135,7 +194,7 @@ abstract class ProxyService Future _getVM() { return captureElapsedTime(() async { - return _vm; + return vm; }, (result) => DwdsEvent.getVM()); } 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 97% 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..4b698e50e 100644 --- a/dwds/lib/src/services/web_socket_proxy_service.dart +++ b/dwds/lib/src/services/web_socket/web_socket_proxy_service.dart @@ -17,6 +17,7 @@ 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/services/web_socket/web_socket_debug_service.dart'; import 'package:dwds/src/utilities/dart_uri.dart'; import 'package:dwds/src/utilities/shared.dart'; import 'package:logging/logging.dart'; @@ -132,12 +133,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 +146,24 @@ 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, - String root, - this.appConnection, - ) : super(vm, root); + WebSocketProxyService._({ + required this.sendClientRequest, + required super.vm, + required super.root, + required super.debugService, + required this.appConnection, + }); // Isolate state vm_service.Event? _currentPauseEvent; @@ -431,6 +431,7 @@ class WebSocketProxyService extends ProxyService { SendClientRequest sendClientRequest, AppConnection appConnection, String root, + WebSocketDebugService debugService, ) async { final vm = vm_service.VM( name: 'WebSocketDebugProxy', @@ -447,10 +448,11 @@ class WebSocketProxyService extends ProxyService { pid: -1, ); final service = WebSocketProxyService._( - sendClientRequest, - vm, - root, - appConnection, + sendClientRequest: sendClientRequest, + vm: vm, + root: root, + debugService: debugService, + appConnection: appConnection, ); safeUnawaited(service.createIsolate(appConnection)); return service; 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/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'; From f0f1f3057cca1827342ff30da07a72770fdaa678 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Fri, 31 Oct 2025 14:45:33 -0400 Subject: [PATCH 2/7] Fix service extensions, dedup more code --- .../src/debugging/web_socket_inspector.dart | 1 + .../services/chrome/chrome_proxy_service.dart | 236 ++-------------- dwds/lib/src/services/proxy_service.dart | 173 +++++++++++- .../web_socket/web_socket_proxy_service.dart | 254 +++--------------- 4 files changed, 227 insertions(+), 437 deletions(-) diff --git a/dwds/lib/src/debugging/web_socket_inspector.dart b/dwds/lib/src/debugging/web_socket_inspector.dart index 788cc13fc..5b5a6f01b 100644 --- a/dwds/lib/src/debugging/web_socket_inspector.dart +++ b/dwds/lib/src/debugging/web_socket_inspector.dart @@ -38,6 +38,7 @@ class WebSocketAppInspector extends AppInspector { breakpoints: [], isSystemIsolate: false, isolateFlags: [], + extensionRPCs: [], ); final inspector = WebSocketAppInspector._( appConnection, diff --git a/dwds/lib/src/services/chrome/chrome_proxy_service.dart b/dwds/lib/src/services/chrome/chrome_proxy_service.dart index bf56a9804..e5745e606 100644 --- a/dwds/lib/src/services/chrome/chrome_proxy_service.dart +++ b/dwds/lib/src/services/chrome/chrome_proxy_service.dart @@ -431,7 +431,7 @@ final class ChromeProxyService extends ProxyService { int? column, }) async { await isInitialized; - _checkIsolate('addBreakpoint', isolateId); + checkIsolate('addBreakpoint', isolateId); return (await debuggerFuture).addBreakpoint(scriptId, line, column: column); } @@ -459,7 +459,7 @@ final 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 @@ -503,7 +503,7 @@ final 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( @@ -640,7 +640,7 @@ final class ChromeProxyService extends ProxyService { final evaluator = _expressionEvaluator; if (evaluator != null) { await isCompilerInitialized; - _checkIsolate('evaluate', isolateId); + checkIsolate('evaluate', isolateId); late Obj object; try { @@ -730,7 +730,7 @@ final class ChromeProxyService extends ProxyService { final evaluator = _expressionEvaluator; if (evaluator != null) { await isCompilerInitialized; - _checkIsolate('evaluateInFrame', isolateId); + checkIsolate('evaluateInFrame', isolateId); return await _getEvaluationResult( isolateId, @@ -751,18 +751,6 @@ final 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( @@ -772,7 +760,7 @@ final class ChromeProxyService extends ProxyService { Future _getMemoryUsage(String isolateId) async { await isInitialized; - _checkIsolate('getMemoryUsage', isolateId); + checkIsolate('getMemoryUsage', isolateId); return inspector.getMemoryUsage(); } @@ -799,22 +787,10 @@ final 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, @@ -840,7 +816,7 @@ final 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()); } @@ -867,20 +843,10 @@ final 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, @@ -906,7 +872,7 @@ final 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); } @@ -968,52 +934,10 @@ final 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, { @@ -1023,7 +947,7 @@ final class ChromeProxyService extends ProxyService { String? packagesUri, }) async { await isInitialized; - _checkIsolate('reloadSources', isolateId); + checkIsolate('reloadSources', isolateId); ReloadReport getFailedReloadReport(String error) => _ReloadReportWithMetadata(success: false) @@ -1141,7 +1065,7 @@ final class ChromeProxyService extends ProxyService { String breakpointId, ) async { await isInitialized; - _checkIsolate('removeBreakpoint', isolateId); + checkIsolate('removeBreakpoint', isolateId); return (await debuggerFuture).removeBreakpoint(breakpointId); } @@ -1168,7 +1092,7 @@ final 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)); @@ -1213,76 +1137,12 @@ final 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( @@ -1345,35 +1205,12 @@ final 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 @@ -1381,21 +1218,7 @@ final 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. @@ -1519,31 +1342,6 @@ final class ChromeProxyService extends ProxyService { 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/proxy_service.dart b/dwds/lib/src/services/proxy_service.dart index b7319cb34..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'; @@ -13,6 +14,7 @@ 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; @@ -178,6 +180,8 @@ abstract base class ProxyService 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(); } @@ -198,6 +202,48 @@ abstract base class ProxyService }, (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); @@ -254,6 +300,55 @@ abstract base 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) { @@ -264,11 +359,45 @@ abstract base 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. /// @@ -430,6 +559,15 @@ abstract base 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(); @@ -475,6 +613,11 @@ abstract base 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 @@ -490,6 +633,32 @@ abstract base 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_proxy_service.dart b/dwds/lib/src/services/web_socket/web_socket_proxy_service.dart index 4b698e50e..53b01c49b 100644 --- a/dwds/lib/src/services/web_socket/web_socket_proxy_service.dart +++ b/dwds/lib/src/services/web_socket/web_socket_proxy_service.dart @@ -5,20 +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/services/web_socket/web_socket_debug_service.dart'; -import 'package:dwds/src/utilities/dart_uri.dart'; import 'package:dwds/src/utilities/shared.dart'; import 'package:logging/logging.dart'; import 'package:vm_service/vm_service.dart' as vm_service; @@ -165,6 +161,37 @@ final class WebSocketProxyService extends ProxyService { required this.appConnection, }); + static Future create( + SendClientRequest sendClientRequest, + AppConnection appConnection, + String 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; bool _mainHasStarted = false; @@ -225,7 +252,7 @@ final 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 @@ final class WebSocketProxyService extends ProxyService { isolate: isolateRef, ), ); - _streamNotify( + streamNotify( vm_service.EventStreams.kIsolate, vm_service.Event( kind: vm_service.EventKind.kIsolateRunnable, @@ -250,7 +277,7 @@ final 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 @@ final 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 @@ final 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,106 +401,6 @@ final 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, - 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; - } - - /// 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, { @@ -518,7 +429,7 @@ final class WebSocketProxyService extends ProxyService { } /// Handles hot restart requests. - Future> hotRestart() async { + Future> hotRestart() async { _logger.info('Attempting a hot restart'); try { @@ -855,60 +766,6 @@ final 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)); @@ -934,31 +791,13 @@ final 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) => @@ -996,29 +835,12 @@ final 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); From 3014f487e347be5dee2790427c42d306a4e9f741 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Fri, 31 Oct 2025 15:02:07 -0400 Subject: [PATCH 3/7] Fix tests --- dwds/lib/src/dwds_vm_client.dart | 1 - dwds/lib/src/handlers/dev_handler.dart | 6 +----- dwds/lib/src/services/chrome/chrome_debug_service.dart | 6 +++++- dwds/lib/src/services/debug_service.dart | 1 + 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dwds/lib/src/dwds_vm_client.dart b/dwds/lib/src/dwds_vm_client.dart index a20669a95..a6426cbd1 100644 --- a/dwds/lib/src/dwds_vm_client.dart +++ b/dwds/lib/src/dwds_vm_client.dart @@ -307,7 +307,6 @@ final class ChromeDwdsVmClient static Future create( ChromeDebugService debugService, - DwdsStats dwdsStats, Uri? ddsUri, ) async { final dwdsVmClient = ChromeDwdsVmClient(debugService: debugService); diff --git a/dwds/lib/src/handlers/dev_handler.dart b/dwds/lib/src/handlers/dev_handler.dart index ade73a6c5..94d9e3201 100644 --- a/dwds/lib/src/handlers/dev_handler.dart +++ b/dwds/lib/src/handlers/dev_handler.dart @@ -851,11 +851,7 @@ class DevHandler { if (_ddsConfig.enable) { dds = await debugService.startDartDevelopmentService(); } - final vmClient = await ChromeDwdsVmClient.create( - debugService, - dwdsStats, - dds?.wsUri, - ); + final vmClient = await ChromeDwdsVmClient.create(debugService, dds?.wsUri); final appDebugService = AppDebugServices( debugService: debugService, dwdsVmClient: vmClient, diff --git a/dwds/lib/src/services/chrome/chrome_debug_service.dart b/dwds/lib/src/services/chrome/chrome_debug_service.dart index c5e6df256..6fa2731dd 100644 --- a/dwds/lib/src/services/chrome/chrome_debug_service.dart +++ b/dwds/lib/src/services/chrome/chrome_debug_service.dart @@ -81,7 +81,11 @@ final class ChromeDebugService extends DebugService { executionContext: executionContext, expressionCompiler: expressionCompiler, ); - await debugService.initialize(proxyService: chromeProxyService); + await debugService.initialize( + proxyService: chromeProxyService, + onRequest: onRequest, + onResponse: onResponse, + ); return debugService; } diff --git a/dwds/lib/src/services/debug_service.dart b/dwds/lib/src/services/debug_service.dart index 07aed226b..699d22b6f 100644 --- a/dwds/lib/src/services/debug_service.dart +++ b/dwds/lib/src/services/debug_service.dart @@ -190,6 +190,7 @@ abstract class DebugService { } @protected + @mustCallSuper void handleConnection( StreamChannel channel, ProxyService proxyService, From 401dcd297e8516b45f94343c4205247044d993ea Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Fri, 31 Oct 2025 16:55:30 -0400 Subject: [PATCH 4/7] Fix test failure --- dwds/lib/src/services/debug_service.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dwds/lib/src/services/debug_service.dart b/dwds/lib/src/services/debug_service.dart index 699d22b6f..c306319dc 100644 --- a/dwds/lib/src/services/debug_service.dart +++ b/dwds/lib/src/services/debug_service.dart @@ -40,7 +40,7 @@ abstract class DebugService { /// The URI pointing to the VM service implementation hosted by the [DebugService]. String get uri => _uri.toString(); - Uri get _uri { + Uri get _uri => _cachedUri ??= () { final dds = _dds; if (ddsConfig.enable && dds != null) { return useSse ? dds.sseUri : dds.wsUri; @@ -58,8 +58,9 @@ abstract class DebugService { port: _server.port, path: authToken, ); - } + }(); + Uri? _cachedUri; String? _ddsUri; late final T proxyService; From dbf17e62970936e1834f885a6fa3b421d927a8cf Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 4 Nov 2025 11:15:15 -0500 Subject: [PATCH 5/7] disable DCM error --- dwds/lib/src/services/debug_service.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dwds/lib/src/services/debug_service.dart b/dwds/lib/src/services/debug_service.dart index c306319dc..787a277a0 100644 --- a/dwds/lib/src/services/debug_service.dart +++ b/dwds/lib/src/services/debug_service.dart @@ -95,6 +95,8 @@ abstract class DebugService { @protected @mustCallSuper @mustBeOverridden + // False positive + // ignore: avoid-redundant-async Future initialize({required T proxyService}) async { this.proxyService = proxyService; } From 8ec8f5ec4e7dbdae7170b2982cb89599fc387202 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 4 Nov 2025 11:18:51 -0500 Subject: [PATCH 6/7] CHANGELOG, version reset --- dwds/CHANGELOG.md | 6 +++++- dwds/lib/src/injected/client.js | 2 +- dwds/lib/src/version.dart | 2 +- dwds/pubspec.yaml | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 1e45a48cd..18d727dd5 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 with no Chrome Debugger is available. ## 26.1.0 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/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 From 4e5390c1eac79a33fe6f8578adb8b19d6feabba1 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 4 Nov 2025 14:26:16 -0500 Subject: [PATCH 7/7] Fix typo in CHANGELOG for version 26.2.0 --- dwds/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 18d727dd5..ddfa389ea 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -4,7 +4,7 @@ ## 26.2.0 -- Add support for more service APIs over websocket connections with no Chrome Debugger is available. +- Add support for more service APIs over websocket connections when no Chrome Debugger is available. ## 26.1.0