From 1e84ba409966c3c709ecebe353955cdf2e6d4239 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 31 Jul 2024 11:05:49 -0400 Subject: [PATCH 1/3] [ DWDS ] Spawn DDS in a process rather than using package:dds This is part of the effort to stop shipping DDS via Pub. --- dwds/lib/src/services/debug_service.dart | 210 ++++++++++++++++++++++- 1 file changed, 206 insertions(+), 4 deletions(-) diff --git a/dwds/lib/src/services/debug_service.dart b/dwds/lib/src/services/debug_service.dart index 23279ac9e..f4d28ba8c 100644 --- a/dwds/lib/src/services/debug_service.dart +++ b/dwds/lib/src/services/debug_service.dart @@ -8,7 +8,6 @@ import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; -import 'package:dds/dds.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'; @@ -27,6 +26,8 @@ import 'package:sse/server/sse_handler.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; int _clientsConnected = 0; @@ -121,6 +122,207 @@ Future _handleSseConnections( } } +/// Wrapper around a `dart development-service` process. +class DartDevelopmentService { + static Future start({ + required Uri remoteVmServiceUri, + required Uri serviceUri, + // TODO(bkonyi): use this parameter once `dart development-service` allows + // for the flag. + required bool ipv6, + }) async { + final process = await Process.start( + Platform.executable, + [ + 'development-service', + '--vm-service-uri=$remoteVmServiceUri', + '--bind-address=${serviceUri.host}', + '--bind-port=${serviceUri.port}', + ], + ); + final completer = Completer(); + late StreamSubscription stderrSub; + stderrSub = process.stderr + .transform(utf8.decoder) + .transform(json.decoder) + .listen((Object? result) { + if (result + case { + 'state': 'started', + 'ddsUri': final String ddsUriStr, + }) { + final ddsUri = Uri.parse(ddsUriStr); + completer.complete( + DartDevelopmentService._( + process: process, + uri: ddsUri, + ), + ); + } else if (result + case { + 'state': 'error', + 'error': final String error, + }) { + final exceptionDetails = + result['ddsExceptionDetails'] as Map?; + completer.completeError( + exceptionDetails != null + ? DartDevelopmentServiceException.fromJson(exceptionDetails) + : StateError(error), + ); + } else { + throw StateError('Unexpected result from DDS: $result'); + } + stderrSub.cancel(); + }); + return completer.future; + } + + DartDevelopmentService._({ + required Process process, + required this.uri, + }) : _ddsInstance = process; + + final Process _ddsInstance; + + final Uri uri; + + Uri get sseUri => _toSse(uri)!; + Uri get wsUri => _toWebSocket(uri)!; + + List _cleanupPathSegments(Uri uri) { + final pathSegments = []; + if (uri.pathSegments.isNotEmpty) { + pathSegments.addAll( + uri.pathSegments.where( + // Strip out the empty string that appears at the end of path segments. + // Empty string elements will result in an extra '/' being added to the + // URI. + (s) => s.isNotEmpty, + ), + ); + } + return pathSegments; + } + + Uri? _toWebSocket(Uri? uri) { + if (uri == null) { + return null; + } + final pathSegments = _cleanupPathSegments(uri); + pathSegments.add('ws'); + return uri.replace(scheme: 'ws', pathSegments: pathSegments); + } + + Uri? _toSse(Uri? uri) { + if (uri == null) { + return null; + } + final pathSegments = _cleanupPathSegments(uri); + pathSegments.add(_kSseHandlerPath); + return uri.replace(scheme: 'sse', pathSegments: pathSegments); + } + + Future shutdown() { + _ddsInstance.kill(); + return _ddsInstance.exitCode; + } +} + +/// Thrown by DDS during initialization failures, unexpected connection issues, +/// and when attempting to spawn DDS when an existing DDS instance exists. +class DartDevelopmentServiceException implements Exception { + factory DartDevelopmentServiceException.fromJson(Map json) { + if (json + case { + 'error_code': final int errorCode, + 'message': final String message, + 'uri': final String? uri + }) { + return switch (errorCode) { + existingDdsInstanceError => + DartDevelopmentServiceException.existingDdsInstance( + message, + ddsUri: Uri.parse(uri!), + ), + failedToStartError => DartDevelopmentServiceException.failedToStart(), + connectionError => + DartDevelopmentServiceException.connectionIssue(message), + _ => throw StateError( + 'Invalid DartDevelopmentServiceException error_code: $errorCode', + ), + }; + } + throw StateError('Invalid DartDevelopmentServiceException JSON: $json'); + } + + /// Thrown when `DartDeveloperService.startDartDevelopmentService` is called + /// and the target VM service already has a Dart Developer Service instance + /// connected. + factory DartDevelopmentServiceException.existingDdsInstance( + String message, { + Uri? ddsUri, + }) { + return ExistingDartDevelopmentServiceException._( + message, + ddsUri: ddsUri, + ); + } + + /// Thrown when the connection to the remote VM service terminates unexpectedly + /// during Dart Development Service startup. + factory DartDevelopmentServiceException.failedToStart() { + return DartDevelopmentServiceException._( + failedToStartError, + 'Failed to start Dart Development Service', + ); + } + + /// Thrown when a connection error has occurred after startup. + factory DartDevelopmentServiceException.connectionIssue(String message) { + return DartDevelopmentServiceException._(connectionError, message); + } + + DartDevelopmentServiceException._(this.errorCode, this.message); + + /// Set when `DartDeveloperService.startDartDevelopmentService` is called and + /// the target VM service already has a Dart Developer Service instance + /// connected. + static const int existingDdsInstanceError = 1; + + /// Set when the connection to the remote VM service terminates unexpectedly + /// during Dart Development Service startup. + static const int failedToStartError = 2; + + /// Set when a connection error has occurred after startup. + static const int connectionError = 3; + + @override + String toString() => 'DartDevelopmentServiceException: $message'; + + final int errorCode; + final String message; +} + +/// Thrown when attempting to start a new DDS instance when one already exists. +class ExistingDartDevelopmentServiceException + extends DartDevelopmentServiceException { + ExistingDartDevelopmentServiceException._( + String message, { + this.ddsUri, + }) : super._( + DartDevelopmentServiceException.existingDdsInstanceError, + message, + ); + + /// The URI of the existing DDS instance, if available. + /// + /// This URI is the base HTTP URI such as `http://127.0.0.1:1234/AbcDefg=/`, + /// not the WebSocket URI (which can be obtained by mapping the scheme to + /// `ws` (or `wss`) and appending `ws` to the path segments). + final Uri? ddsUri; +} + /// A Dart Web Debug Service. /// /// Creates a [ChromeProxyService] from an existing Chrome instance. @@ -163,8 +365,8 @@ class DebugService { Future startDartDevelopmentService() async { // Note: DDS can handle both web socket and SSE connections with no // additional configuration. - _dds = await DartDevelopmentService.startDartDevelopmentService( - Uri( + _dds = await DartDevelopmentService.start( + remoteVmServiceUri: Uri( scheme: 'http', host: hostname, port: port, @@ -248,7 +450,7 @@ class DebugService { // DDS will always connect to DWDS via web sockets. if (useSse && !spawnDds) { final sseHandler = SseHandler( - Uri.parse('/$authToken/\$debugHandler'), + Uri.parse('/$authToken/$_kSseHandlerPath'), keepAlive: const Duration(seconds: 5), ); handler = sseHandler.handler; From cb4451e68b278684e21ea6a1314674a0899f96d7 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 31 Jul 2024 11:34:07 -0400 Subject: [PATCH 2/3] Remove dead code, update CHANGELOG --- dwds/CHANGELOG.md | 1 + dwds/lib/src/services/debug_service.dart | 4 ---- dwds/lib/src/utilities/server.dart | 11 ----------- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index e83dbc16e..d3d913ffa 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -7,6 +7,7 @@ - Hide more variables from the local scope when debugging. These variables were synthetically added by the compiler to support late local variables and don't appear in the original source code. - [#2445](https://github.com/dart-lang/webdev/pull/2445) - Require Dart `^3.4` +- Spawn DDS in a separate process using `dart development-service` instead of launching from `package:dds`. - [#2466](https://github.com/dart-lang/webdev/pull/2466) ## 24.0.0 diff --git a/dwds/lib/src/services/debug_service.dart b/dwds/lib/src/services/debug_service.dart index f4d28ba8c..80a0b4318 100644 --- a/dwds/lib/src/services/debug_service.dart +++ b/dwds/lib/src/services/debug_service.dart @@ -127,9 +127,6 @@ class DartDevelopmentService { static Future start({ required Uri remoteVmServiceUri, required Uri serviceUri, - // TODO(bkonyi): use this parameter once `dart development-service` allows - // for the flag. - required bool ipv6, }) async { final process = await Process.start( Platform.executable, @@ -377,7 +374,6 @@ class DebugService { host: hostname, port: 0, ), - ipv6: await useIPv6ForHost(hostname), ); return _dds!; } diff --git a/dwds/lib/src/utilities/server.dart b/dwds/lib/src/utilities/server.dart index bb442f680..92bd6ab31 100644 --- a/dwds/lib/src/utilities/server.dart +++ b/dwds/lib/src/utilities/server.dart @@ -12,17 +12,6 @@ import 'package:stack_trace/stack_trace.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' as wip; -/// Returns `true` if [hostname] is bound to an IPv6 address. -Future useIPv6ForHost(String hostname) async { - final addresses = await InternetAddress.lookup(hostname); - if (addresses.isEmpty) return false; - final address = addresses.firstWhere( - (a) => a.type == InternetAddressType.IPv6, - orElse: () => addresses.first, - ); - return address.type == InternetAddressType.IPv6; -} - /// Returns a port that is probably, but not definitely, not in use. /// /// This has a built-in race condition: another process may bind this port at From b3ffc263e0db18ac4d648560f0de8c215a99b54b Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Thu, 1 Aug 2024 10:35:50 -0400 Subject: [PATCH 3/3] Update to package:dds ^4.2.5, use DartDevelopmentServiceLauncher --- dwds/lib/src/services/debug_service.dart | 205 +---------------------- dwds/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 202 deletions(-) diff --git a/dwds/lib/src/services/debug_service.dart b/dwds/lib/src/services/debug_service.dart index 80a0b4318..e7fd46492 100644 --- a/dwds/lib/src/services/debug_service.dart +++ b/dwds/lib/src/services/debug_service.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'dart:math'; 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'; @@ -122,204 +123,6 @@ Future _handleSseConnections( } } -/// Wrapper around a `dart development-service` process. -class DartDevelopmentService { - static Future start({ - required Uri remoteVmServiceUri, - required Uri serviceUri, - }) async { - final process = await Process.start( - Platform.executable, - [ - 'development-service', - '--vm-service-uri=$remoteVmServiceUri', - '--bind-address=${serviceUri.host}', - '--bind-port=${serviceUri.port}', - ], - ); - final completer = Completer(); - late StreamSubscription stderrSub; - stderrSub = process.stderr - .transform(utf8.decoder) - .transform(json.decoder) - .listen((Object? result) { - if (result - case { - 'state': 'started', - 'ddsUri': final String ddsUriStr, - }) { - final ddsUri = Uri.parse(ddsUriStr); - completer.complete( - DartDevelopmentService._( - process: process, - uri: ddsUri, - ), - ); - } else if (result - case { - 'state': 'error', - 'error': final String error, - }) { - final exceptionDetails = - result['ddsExceptionDetails'] as Map?; - completer.completeError( - exceptionDetails != null - ? DartDevelopmentServiceException.fromJson(exceptionDetails) - : StateError(error), - ); - } else { - throw StateError('Unexpected result from DDS: $result'); - } - stderrSub.cancel(); - }); - return completer.future; - } - - DartDevelopmentService._({ - required Process process, - required this.uri, - }) : _ddsInstance = process; - - final Process _ddsInstance; - - final Uri uri; - - Uri get sseUri => _toSse(uri)!; - Uri get wsUri => _toWebSocket(uri)!; - - List _cleanupPathSegments(Uri uri) { - final pathSegments = []; - if (uri.pathSegments.isNotEmpty) { - pathSegments.addAll( - uri.pathSegments.where( - // Strip out the empty string that appears at the end of path segments. - // Empty string elements will result in an extra '/' being added to the - // URI. - (s) => s.isNotEmpty, - ), - ); - } - return pathSegments; - } - - Uri? _toWebSocket(Uri? uri) { - if (uri == null) { - return null; - } - final pathSegments = _cleanupPathSegments(uri); - pathSegments.add('ws'); - return uri.replace(scheme: 'ws', pathSegments: pathSegments); - } - - Uri? _toSse(Uri? uri) { - if (uri == null) { - return null; - } - final pathSegments = _cleanupPathSegments(uri); - pathSegments.add(_kSseHandlerPath); - return uri.replace(scheme: 'sse', pathSegments: pathSegments); - } - - Future shutdown() { - _ddsInstance.kill(); - return _ddsInstance.exitCode; - } -} - -/// Thrown by DDS during initialization failures, unexpected connection issues, -/// and when attempting to spawn DDS when an existing DDS instance exists. -class DartDevelopmentServiceException implements Exception { - factory DartDevelopmentServiceException.fromJson(Map json) { - if (json - case { - 'error_code': final int errorCode, - 'message': final String message, - 'uri': final String? uri - }) { - return switch (errorCode) { - existingDdsInstanceError => - DartDevelopmentServiceException.existingDdsInstance( - message, - ddsUri: Uri.parse(uri!), - ), - failedToStartError => DartDevelopmentServiceException.failedToStart(), - connectionError => - DartDevelopmentServiceException.connectionIssue(message), - _ => throw StateError( - 'Invalid DartDevelopmentServiceException error_code: $errorCode', - ), - }; - } - throw StateError('Invalid DartDevelopmentServiceException JSON: $json'); - } - - /// Thrown when `DartDeveloperService.startDartDevelopmentService` is called - /// and the target VM service already has a Dart Developer Service instance - /// connected. - factory DartDevelopmentServiceException.existingDdsInstance( - String message, { - Uri? ddsUri, - }) { - return ExistingDartDevelopmentServiceException._( - message, - ddsUri: ddsUri, - ); - } - - /// Thrown when the connection to the remote VM service terminates unexpectedly - /// during Dart Development Service startup. - factory DartDevelopmentServiceException.failedToStart() { - return DartDevelopmentServiceException._( - failedToStartError, - 'Failed to start Dart Development Service', - ); - } - - /// Thrown when a connection error has occurred after startup. - factory DartDevelopmentServiceException.connectionIssue(String message) { - return DartDevelopmentServiceException._(connectionError, message); - } - - DartDevelopmentServiceException._(this.errorCode, this.message); - - /// Set when `DartDeveloperService.startDartDevelopmentService` is called and - /// the target VM service already has a Dart Developer Service instance - /// connected. - static const int existingDdsInstanceError = 1; - - /// Set when the connection to the remote VM service terminates unexpectedly - /// during Dart Development Service startup. - static const int failedToStartError = 2; - - /// Set when a connection error has occurred after startup. - static const int connectionError = 3; - - @override - String toString() => 'DartDevelopmentServiceException: $message'; - - final int errorCode; - final String message; -} - -/// Thrown when attempting to start a new DDS instance when one already exists. -class ExistingDartDevelopmentServiceException - extends DartDevelopmentServiceException { - ExistingDartDevelopmentServiceException._( - String message, { - this.ddsUri, - }) : super._( - DartDevelopmentServiceException.existingDdsInstanceError, - message, - ); - - /// The URI of the existing DDS instance, if available. - /// - /// This URI is the base HTTP URI such as `http://127.0.0.1:1234/AbcDefg=/`, - /// not the WebSocket URI (which can be obtained by mapping the scheme to - /// `ws` (or `wss`) and appending `ws` to the path segments). - final Uri? ddsUri; -} - /// A Dart Web Debug Service. /// /// Creates a [ChromeProxyService] from an existing Chrome instance. @@ -335,7 +138,7 @@ class DebugService { final bool _useSse; final bool _spawnDds; final UrlEncoder? _urlEncoder; - DartDevelopmentService? _dds; + DartDevelopmentServiceLauncher? _dds; /// Null until [close] is called. /// @@ -359,10 +162,10 @@ class DebugService { if (_dds != null) _dds!.shutdown(), ]); - Future startDartDevelopmentService() async { + Future startDartDevelopmentService() async { // Note: DDS can handle both web socket and SSE connections with no // additional configuration. - _dds = await DartDevelopmentService.start( + _dds = await DartDevelopmentServiceLauncher.start( remoteVmServiceUri: Uri( scheme: 'http', host: hostname, diff --git a/dwds/pubspec.yaml b/dwds/pubspec.yaml index d39684cae..77f292bac 100644 --- a/dwds/pubspec.yaml +++ b/dwds/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: built_value: ^8.3.0 collection: ^1.15.0 crypto: ^3.0.2 - dds: ^4.1.0 + dds: ^4.2.5 file: ">=6.1.4 <8.0.0" http: ^1.0.0 http_multi_server: ^3.2.0