@@ -17,6 +17,7 @@ import 'package:dwds/src/events.dart';
17
17
import 'package:dwds/src/readers/asset_reader.dart' ;
18
18
import 'package:dwds/src/services/chrome_proxy_service.dart' ;
19
19
import 'package:dwds/src/services/expression_compiler.dart' ;
20
+ import 'package:dwds/src/services/web_socket_proxy_service.dart' ;
20
21
import 'package:dwds/src/utilities/server.dart' ;
21
22
import 'package:dwds/src/utilities/shared.dart' ;
22
23
import 'package:logging/logging.dart' ;
@@ -126,15 +127,28 @@ Future<void> _handleSseConnections(
126
127
}
127
128
}
128
129
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
+
129
140
/// A Dart Web Debug Service.
130
141
///
131
142
/// Creates a [ChromeProxyService] from an existing Chrome instance.
132
- class DebugService {
143
+ class DebugService implements IDebugService {
133
144
static String ? _ddsUri;
134
145
135
146
final VmServiceInterface chromeProxyService;
147
+ @override
136
148
final String hostname;
149
+ @override
137
150
final ServiceExtensionRegistry serviceExtensionRegistry;
151
+ @override
138
152
final int port;
139
153
final String authToken;
140
154
final HttpServer _server;
@@ -162,6 +176,7 @@ class DebugService {
162
176
this ._urlEncoder,
163
177
);
164
178
179
+ @override
165
180
Future <void > close () =>
166
181
_closed ?? = Future .wait ([
167
182
_server.close (),
@@ -183,6 +198,7 @@ class DebugService {
183
198
return _dds! ;
184
199
}
185
200
201
+ @override
186
202
String get uri {
187
203
final dds = _dds;
188
204
if (_spawnDds && dds != null ) {
@@ -200,6 +216,7 @@ class DebugService {
200
216
}
201
217
202
218
String ? _encodedUri;
219
+ @override
203
220
Future <String > get encodedUri async {
204
221
if (_encodedUri != null ) return _encodedUri! ;
205
222
var encoded = uri;
@@ -303,6 +320,167 @@ class DebugService {
303
320
}
304
321
}
305
322
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
+
306
484
// Creates a random auth token for more secure connections.
307
485
String _makeAuthToken () {
308
486
final tokenBytes = 8 ;
0 commit comments