@@ -9,6 +9,7 @@ import 'package:dwds/src/events.dart';
99import 'package:dwds/src/services/chrome_debug_exception.dart' ;
1010import 'package:dwds/src/services/chrome_proxy_service.dart' ;
1111import 'package:dwds/src/services/debug_service.dart' ;
12+ import 'package:dwds/src/services/web_socket_proxy_service.dart' ;
1213import 'package:dwds/src/utilities/synchronized.dart' ;
1314import 'package:logging/logging.dart' ;
1415import 'package:uuid/uuid.dart' ;
@@ -17,8 +18,6 @@ import 'package:vm_service/vm_service_io.dart';
1718import 'package:vm_service_interface/vm_service_interface.dart' ;
1819import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' ;
1920
20- final _logger = Logger ('DwdsVmClient' );
21-
2221/// Type of requests added to the request controller.
2322typedef VmRequest = Map <String , Object >;
2423
@@ -38,9 +37,22 @@ enum _NamespacedServiceExtension {
3837 final String method;
3938}
4039
40+ /// Common interface for DWDS VM clients.
41+ abstract class IDwdsVmClient {
42+ /// The VM service client.
43+ VmService get client;
44+
45+ /// Closes the VM client and releases resources.
46+ Future <void > close ();
47+ }
48+
49+ // Chrome-based DWDS VM client logger.
50+ final _chromeLogger = Logger ('DwdsVmClient' );
51+
4152// A client of the vm service that registers some custom extensions like
4253// hotRestart.
43- class DwdsVmClient {
54+ class DwdsVmClient implements IDwdsVmClient {
55+ @override
4456 final VmService client;
4557 final StreamController <Map <String , Object >> _requestController;
4658 final StreamController <Map <String , Object ?>> _responseController;
@@ -137,7 +149,7 @@ class DwdsVmClient {
137149 }) {
138150 final client = VmService (responseStream.map (jsonEncode), (request) {
139151 if (requestController.isClosed) {
140- _logger .warning (
152+ _chromeLogger .warning (
141153 'Attempted to send a request but the connection is closed:\n\n '
142154 '$request ' ,
143155 );
@@ -331,13 +343,13 @@ void _processSendEvent(Map<String, dynamic> request, DwdsStats dwdsStats) {
331343 switch (type) {
332344 case 'DevtoolsEvent' :
333345 {
334- _logger .finest ('Received DevTools event: $event ' );
346+ _chromeLogger .finest ('Received DevTools event: $event ' );
335347 final action = payload? ['action' ] as String ? ;
336348 final screen = payload? ['screen' ] as String ? ;
337349 if (screen != null && action == 'pageReady' ) {
338350 _recordDwdsStats (dwdsStats, screen);
339351 } else {
340- _logger .finest ('Ignoring unknown event: $event ' );
352+ _chromeLogger .finest ('Ignoring unknown event: $event ' );
341353 }
342354 }
343355 }
@@ -351,16 +363,16 @@ void _recordDwdsStats(DwdsStats dwdsStats, String screen) {
351363 final devToolLoadTime =
352364 DateTime .now ().difference (devToolsStart).inMilliseconds;
353365 emitEvent (DwdsEvent .devToolsLoad (devToolLoadTime, screen));
354- _logger .fine ('DevTools load time: $devToolLoadTime ms' );
366+ _chromeLogger .fine ('DevTools load time: $devToolLoadTime ms' );
355367 }
356368 if (debuggerStart != null ) {
357369 final debuggerReadyTime =
358370 DateTime .now ().difference (debuggerStart).inMilliseconds;
359371 emitEvent (DwdsEvent .debuggerReady (debuggerReadyTime, screen));
360- _logger .fine ('Debugger ready time: $debuggerReadyTime ms' );
372+ _chromeLogger .fine ('Debugger ready time: $debuggerReadyTime ms' );
361373 }
362374 } else {
363- _logger .finest ('Debugger and DevTools stats are already recorded.' );
375+ _chromeLogger .finest ('Debugger and DevTools stats are already recorded.' );
364376 }
365377}
366378
@@ -381,14 +393,14 @@ Future<Map<String, dynamic>> _hotRestart(
381393 ChromeProxyService chromeProxyService,
382394 VmService client,
383395) async {
384- _logger .info ('Attempting a hot restart' );
396+ _chromeLogger .info ('Attempting a hot restart' );
385397
386398 chromeProxyService.terminatingIsolates = true ;
387399 await _disableBreakpointsAndResume (client, chromeProxyService);
388400 try {
389- _logger .info ('Attempting to get execution context ID.' );
401+ _chromeLogger .info ('Attempting to get execution context ID.' );
390402 await tryGetContextId (chromeProxyService);
391- _logger .info ('Got execution context ID.' );
403+ _chromeLogger .info ('Got execution context ID.' );
392404 } on StateError catch (e) {
393405 // We couldn't find the execution context. `hotRestart` may have been
394406 // triggered in the middle of a full reload.
@@ -408,12 +420,12 @@ Future<Map<String, dynamic>> _hotRestart(
408420 }
409421 // Generate run id to hot restart all apps loaded into the tab.
410422 final runId = const Uuid ().v4 ().toString ();
411- _logger .info ('Issuing \$ dartHotRestartDwds request' );
423+ _chromeLogger .info ('Issuing \$ dartHotRestartDwds request' );
412424 await chromeProxyService.inspector.jsEvaluate (
413425 '\$ dartHotRestartDwds(\' $runId \' , $pauseIsolatesOnStart );' ,
414426 awaitPromise: true ,
415427 );
416- _logger .info ('\$ dartHotRestartDwds request complete.' );
428+ _chromeLogger .info ('\$ dartHotRestartDwds request complete.' );
417429 } on WipError catch (exception) {
418430 final code = exception.error? ['code' ];
419431 final message = exception.error? ['message' ];
@@ -433,11 +445,11 @@ Future<Map<String, dynamic>> _hotRestart(
433445 },
434446 };
435447 }
436- _logger .info ('Waiting for Isolate Start event.' );
448+ _chromeLogger .info ('Waiting for Isolate Start event.' );
437449 await stream.firstWhere ((event) => event.kind == EventKind .kIsolateStart);
438450 chromeProxyService.terminatingIsolates = false ;
439451
440- _logger .info ('Successful hot restart' );
452+ _chromeLogger .info ('Successful hot restart' );
441453 return {'result' : Success ().toJson ()};
442454}
443455
@@ -455,18 +467,20 @@ void _waitForResumeEventToRunMain(ChromeProxyService chromeProxyService) {
455467Future <Map <String , dynamic >> _fullReload (
456468 ChromeProxyService chromeProxyService,
457469) async {
458- _logger .info ('Attempting a full reload' );
470+ _chromeLogger .info ('Attempting a full reload' );
459471 await chromeProxyService.remoteDebugger.enablePage ();
460472 await chromeProxyService.remoteDebugger.pageReload ();
461- _logger .info ('Successful full reload' );
473+ _chromeLogger .info ('Successful full reload' );
462474 return {'result' : Success ().toJson ()};
463475}
464476
465477Future <void > _disableBreakpointsAndResume (
466478 VmService client,
467479 ChromeProxyService chromeProxyService,
468480) async {
469- _logger.info ('Attempting to disable breakpoints and resume the isolate' );
481+ _chromeLogger.info (
482+ 'Attempting to disable breakpoints and resume the isolate' ,
483+ );
470484 final vm = await client.getVM ();
471485 final isolates = vm.isolates;
472486 if (isolates == null || isolates.isEmpty) {
@@ -495,9 +509,193 @@ Future<void> _disableBreakpointsAndResume(
495509 await client.resume (isolateId);
496510 } on RPCError catch (e, s) {
497511 if (! e.message.contains ('Can only perform operation while paused' )) {
498- _logger.severe ('Hot restart failed to resume exiting isolate' , e, s);
512+ _chromeLogger.severe (
513+ 'Hot restart failed to resume exiting isolate' ,
514+ e,
515+ s,
516+ );
499517 rethrow ;
500518 }
501519 }
502- _logger.info ('Successfully disabled breakpoints and resumed the isolate' );
520+ _chromeLogger.info (
521+ 'Successfully disabled breakpoints and resumed the isolate' ,
522+ );
523+ }
524+
525+ // WebSocket-based DWDS VM client logger.
526+ final _webSocketLogger = Logger ('WebSocketDwdsVmClient' );
527+
528+ /// WebSocket-based DWDS VM client.
529+ class WebSocketDwdsVmClient implements IDwdsVmClient {
530+ @override
531+ final VmService client;
532+ final StreamController <VmRequest > _requestController;
533+ final StreamController <VmResponse > _responseController;
534+ Future <void >? _closed;
535+
536+ WebSocketDwdsVmClient (
537+ this .client,
538+ this ._requestController,
539+ this ._responseController,
540+ );
541+
542+ @override
543+ Future <void > close () =>
544+ _closed ?? = () async {
545+ await _requestController.close ();
546+ await _responseController.close ();
547+ await client.dispose ();
548+ }();
549+
550+ static Future <WebSocketDwdsVmClient > create (
551+ WebSocketDebugService debugService,
552+ ) async {
553+ _webSocketLogger.fine ('Creating WebSocket DWDS VM client' );
554+ final webSocketProxyService = debugService.webSocketProxyService;
555+ final responseController = StreamController <VmResponse >();
556+ final responseSink = responseController.sink;
557+ final responseStream = responseController.stream.asBroadcastStream ();
558+ final requestController = StreamController <VmRequest >();
559+ final requestSink = requestController.sink;
560+ final requestStream = requestController.stream;
561+
562+ _setUpWebSocketVmServerConnection (
563+ webSocketProxyService: webSocketProxyService,
564+ debugService: debugService,
565+ responseStream: responseStream,
566+ responseSink: responseSink,
567+ requestStream: requestStream,
568+ requestSink: requestSink,
569+ );
570+
571+ final client = _setUpWebSocketVmClient (
572+ responseStream: responseStream,
573+ requestController: requestController,
574+ requestSink: requestSink,
575+ );
576+
577+ _webSocketLogger.fine ('WebSocket DWDS VM client created successfully' );
578+ return WebSocketDwdsVmClient (client, requestController, responseController);
579+ }
580+
581+ static VmService _setUpWebSocketVmClient ({
582+ required Stream <VmResponse > responseStream,
583+ required StreamSink <VmRequest > requestSink,
584+ required StreamController <VmRequest > requestController,
585+ }) {
586+ final client = VmService (responseStream.map (jsonEncode), (request) {
587+ if (requestController.isClosed) {
588+ _webSocketLogger.warning (
589+ 'Attempted to send a request but the connection is closed:\n\n $request ' ,
590+ );
591+ return ;
592+ }
593+ requestSink.add (Map <String , Object >.from (jsonDecode (request)));
594+ });
595+ return client;
596+ }
597+
598+ static void _setUpWebSocketVmServerConnection ({
599+ required WebSocketProxyService webSocketProxyService,
600+ required WebSocketDebugService debugService,
601+ required Stream <VmResponse > responseStream,
602+ required StreamSink <VmResponse > responseSink,
603+ required Stream <VmRequest > requestStream,
604+ required StreamSink <VmRequest > requestSink,
605+ }) {
606+ responseStream.listen ((request) async {
607+ final response = await _maybeHandleWebSocketServiceExtensionRequest (
608+ request,
609+ webSocketProxyService: webSocketProxyService,
610+ );
611+ if (response != null ) {
612+ requestSink.add (response);
613+ }
614+ });
615+
616+ final vmServerConnection = VmServerConnection (
617+ requestStream,
618+ responseSink,
619+ debugService.serviceExtensionRegistry,
620+ webSocketProxyService,
621+ );
622+
623+ // Register service extensions
624+ for (final extension in _NamespacedServiceExtension .values) {
625+ _webSocketLogger.finest (
626+ 'Registering service extension: ${extension .method }' ,
627+ );
628+ debugService.serviceExtensionRegistry.registerExtension (
629+ extension .method,
630+ vmServerConnection,
631+ );
632+ }
633+ }
634+
635+ static Future <VmRequest ?> _maybeHandleWebSocketServiceExtensionRequest (
636+ VmResponse request, {
637+ required WebSocketProxyService webSocketProxyService,
638+ }) async {
639+ VmRequest ? response;
640+ final method = request['method' ];
641+
642+ _webSocketLogger.finest ('Processing service extension method: $method ' );
643+
644+ if (method == _NamespacedServiceExtension .flutterListViews.method) {
645+ response = await _webSocketFlutterListViewsHandler (webSocketProxyService);
646+ } else if (method == _NamespacedServiceExtension .extDwdsEmitEvent.method) {
647+ response = _webSocketExtDwdsEmitEventHandler (request);
648+ } else if (method == _NamespacedServiceExtension .extDwdsReload.method) {
649+ response = {'result' : 'Reload not implemented' };
650+ } else if (method == _NamespacedServiceExtension .extDwdsSendEvent.method) {
651+ response = _webSocketExtDwdsSendEventHandler (request);
652+ } else if (method == _NamespacedServiceExtension .extDwdsScreenshot.method) {
653+ response = {'result' : 'Screenshot not implemented' };
654+ }
655+
656+ if (response != null ) {
657+ response['id' ] = request['id' ] as String ;
658+ response['jsonrpc' ] = '2.0' ;
659+ }
660+ return response;
661+ }
662+
663+ static Future <Map <String , Object >> _webSocketFlutterListViewsHandler (
664+ WebSocketProxyService webSocketProxyService,
665+ ) async {
666+ final vm = await webSocketProxyService.getVM ();
667+ _webSocketLogger.finest (
668+ 'Retrieved VM with ${vm .isolates ?.length ?? 0 } isolates' ,
669+ );
670+ final isolates = vm.isolates;
671+ return < String , Object > {
672+ 'result' : < String , Object > {
673+ 'views' : < Object > [
674+ for (final isolate in isolates ?? [])
675+ < String , Object > {'id' : isolate.id, 'isolate' : isolate.toJson ()},
676+ ],
677+ },
678+ };
679+ }
680+
681+ static Map <String , Object > _webSocketExtDwdsEmitEventHandler (
682+ VmResponse request,
683+ ) {
684+ final event = request['params' ] as Map <String , dynamic >? ;
685+ if (event != null ) {
686+ final type = event['type' ] as String ? ;
687+ final payload = event['payload' ] as Map <String , dynamic >? ;
688+ if (type != null && payload != null ) {
689+ _webSocketLogger.fine ('EmitEvent: $type $payload ' );
690+ }
691+ }
692+ return {'result' : 'EmitEvent handled' };
693+ }
694+
695+ static Map <String , Object > _webSocketExtDwdsSendEventHandler (
696+ VmResponse request,
697+ ) {
698+ _webSocketLogger.fine ('SendEvent: $request ' );
699+ return {'result' : 'SendEvent handled' };
700+ }
503701}
0 commit comments