@@ -17,6 +17,7 @@ import 'package:dwds/src/events.dart';
1717import 'package:dwds/src/readers/asset_reader.dart' ;
1818import 'package:dwds/src/services/chrome_proxy_service.dart' ;
1919import 'package:dwds/src/services/expression_compiler.dart' ;
20+ import 'package:dwds/src/services/web_socket_proxy_service.dart' ;
2021import 'package:dwds/src/utilities/server.dart' ;
2122import 'package:dwds/src/utilities/shared.dart' ;
2223import 'package:logging/logging.dart' ;
@@ -126,15 +127,28 @@ Future<void> _handleSseConnections(
126127 }
127128}
128129
130+ /// Common interface for debug services (Chrome or WebSocket based).
131+ abstract class IDebugService {
132+ String get hostname;
133+ int get port;
134+ String get uri;
135+ Future <String > get encodedUri;
136+ ServiceExtensionRegistry get serviceExtensionRegistry;
137+ Future <void > close ();
138+ }
139+
129140/// A Dart Web Debug Service.
130141///
131142/// Creates a [ChromeProxyService] from an existing Chrome instance.
132- class DebugService {
143+ class DebugService implements IDebugService {
133144 static String ? _ddsUri;
134145
135146 final VmServiceInterface chromeProxyService;
147+ @override
136148 final String hostname;
149+ @override
137150 final ServiceExtensionRegistry serviceExtensionRegistry;
151+ @override
138152 final int port;
139153 final String authToken;
140154 final HttpServer _server;
@@ -162,6 +176,7 @@ class DebugService {
162176 this ._urlEncoder,
163177 );
164178
179+ @override
165180 Future <void > close () =>
166181 _closed ?? = Future .wait ([
167182 _server.close (),
@@ -183,6 +198,7 @@ class DebugService {
183198 return _dds! ;
184199 }
185200
201+ @override
186202 String get uri {
187203 final dds = _dds;
188204 if (_spawnDds && dds != null ) {
@@ -200,6 +216,7 @@ class DebugService {
200216 }
201217
202218 String ? _encodedUri;
219+ @override
203220 Future <String > get encodedUri async {
204221 if (_encodedUri != null ) return _encodedUri! ;
205222 var encoded = uri;
@@ -303,6 +320,167 @@ class DebugService {
303320 }
304321}
305322
323+ /// Defines callbacks for sending messages to the connected client.
324+ /// Returns the number of clients the request was successfully sent to.
325+ typedef SendClientRequest = int Function (Object request);
326+
327+ /// WebSocket-based debug service for web debugging.
328+ class WebSocketDebugService implements IDebugService {
329+ @override
330+ final String hostname;
331+ @override
332+ final int port;
333+ final String authToken;
334+ final HttpServer _server;
335+ final WebSocketProxyService _webSocketProxyService;
336+ final ServiceExtensionRegistry _serviceExtensionRegistry;
337+ final UrlEncoder ? _urlEncoder;
338+
339+ Future <void >? _closed;
340+ DartDevelopmentServiceLauncher ? _dds;
341+ String ? _encodedUri;
342+
343+ WebSocketDebugService ._(
344+ this .hostname,
345+ this .port,
346+ this .authToken,
347+ this ._webSocketProxyService,
348+ this ._serviceExtensionRegistry,
349+ this ._server,
350+ this ._urlEncoder,
351+ );
352+
353+ /// Returns the WebSocketProxyService instance.
354+ WebSocketProxyService get webSocketProxyService => _webSocketProxyService;
355+
356+ /// Returns the ServiceExtensionRegistry instance.
357+ @override
358+ ServiceExtensionRegistry get serviceExtensionRegistry =>
359+ _serviceExtensionRegistry;
360+
361+ /// Closes the debug service and associated resources.
362+ @override
363+ Future <void > close () =>
364+ _closed ?? = Future .wait ([
365+ _server.close (),
366+ if (_dds != null ) _dds! .shutdown (),
367+ ]);
368+
369+ /// Starts DDS (Dart Development Service).
370+ Future <DartDevelopmentServiceLauncher > startDartDevelopmentService ({
371+ int ? ddsPort,
372+ }) async {
373+ const timeout = Duration (seconds: 10 );
374+
375+ try {
376+ _dds = await DartDevelopmentServiceLauncher .start (
377+ remoteVmServiceUri: Uri (
378+ scheme: 'http' ,
379+ host: hostname,
380+ port: port,
381+ path: authToken,
382+ ),
383+ serviceUri: Uri (scheme: 'http' , host: hostname, port: ddsPort ?? 0 ),
384+ ).timeout (timeout);
385+ } catch (e) {
386+ throw Exception ('Failed to start DDS: $e ' );
387+ }
388+ return _dds! ;
389+ }
390+
391+ @override
392+ String get uri =>
393+ Uri (scheme: 'ws' , host: hostname, port: port, path: authToken).toString ();
394+
395+ @override
396+ Future <String > get encodedUri async {
397+ if (_encodedUri != null ) return _encodedUri! ;
398+ var encoded = uri;
399+ if (_urlEncoder != null ) encoded = await _urlEncoder (encoded);
400+ return _encodedUri = encoded;
401+ }
402+
403+ static Future <WebSocketDebugService > start (
404+ String hostname,
405+ AppConnection appConnection, {
406+ required SendClientRequest sendClientRequest,
407+ UrlEncoder ? urlEncoder,
408+ }) async {
409+ final authToken = _makeAuthToken ();
410+ final serviceExtensionRegistry = ServiceExtensionRegistry ();
411+
412+ final webSocketProxyService = await WebSocketProxyService .create (
413+ sendClientRequest,
414+ appConnection,
415+ );
416+
417+ final handler = _createWebSocketHandler (
418+ serviceExtensionRegistry,
419+ webSocketProxyService,
420+ );
421+
422+ final server = await startHttpServer (hostname, port: 0 );
423+ serveHttpRequests (server, handler, (e, s) {
424+ Logger ('WebSocketDebugService' ).warning ('Error serving requests' , e);
425+ });
426+
427+ return WebSocketDebugService ._(
428+ server.address.host,
429+ server.port,
430+ authToken,
431+ webSocketProxyService,
432+ serviceExtensionRegistry,
433+ server,
434+ urlEncoder,
435+ );
436+ }
437+
438+ /// Creates the WebSocket handler for incoming connections.
439+ static dynamic _createWebSocketHandler (
440+ ServiceExtensionRegistry serviceExtensionRegistry,
441+ WebSocketProxyService webSocketProxyService,
442+ ) {
443+ return webSocketHandler ((WebSocketChannel webSocket) {
444+ if (! _acceptNewConnections) {
445+ webSocket.sink.add (
446+ jsonEncode ({
447+ 'error' : 'Cannot connect: another service has taken control.' ,
448+ }),
449+ );
450+ webSocket.sink.close ();
451+ return ;
452+ }
453+
454+ final responseController = StreamController <Map <String , Object ?>>();
455+ webSocket.sink.addStream (responseController.stream.map (jsonEncode));
456+
457+ final inputStream = webSocket.stream.map ((value) {
458+ if (value is List <int >) {
459+ value = utf8.decode (value);
460+ } else if (value is ! String ) {
461+ throw StateError (
462+ 'Unexpected value type from web socket: ${value .runtimeType }' ,
463+ );
464+ }
465+ return Map <String , Object >.from (jsonDecode (value));
466+ });
467+
468+ ++ _clientsConnected;
469+ VmServerConnection (
470+ inputStream,
471+ responseController.sink,
472+ serviceExtensionRegistry,
473+ webSocketProxyService,
474+ ).done.whenComplete (() {
475+ -- _clientsConnected;
476+ if (! _acceptNewConnections && _clientsConnected == 0 ) {
477+ _acceptNewConnections = true ;
478+ }
479+ });
480+ });
481+ }
482+ }
483+
306484// Creates a random auth token for more secure connections.
307485String _makeAuthToken () {
308486 final tokenBytes = 8 ;
0 commit comments