diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 4158ff0a3..897abde13 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,5 +1,7 @@ ## 24.1.0-wip +- Fix bug where debugging clients are not aware of service extensions when connecting to a new web app. - [#2388](https://github.com/dart-lang/webdev/pull/2388) + ## 24.0.0 - Implement `setFlag` when it is called with `pause_isolates_on_start`. - [#2373](https://github.com/dart-lang/webdev/pull/2373) diff --git a/dwds/lib/src/connections/debug_connection.dart b/dwds/lib/src/connections/debug_connection.dart index 8ecaf9985..227404031 100644 --- a/dwds/lib/src/connections/debug_connection.dart +++ b/dwds/lib/src/connections/debug_connection.dart @@ -33,6 +33,9 @@ class DebugConnection { /// The endpoint of the Dart VM Service. String get uri => _appDebugServices.debugService.uri; + // The endpoint of the Dart Development Service (DDS). + String? get ddsUri => _appDebugServices.ddsUri?.toString(); + /// A client of the Dart VM Service with DWDS specific extensions. VmService get vmService => _appDebugServices.dwdsVmClient.client; diff --git a/dwds/lib/src/dwds_vm_client.dart b/dwds/lib/src/dwds_vm_client.dart index 7c14c1233..5c1e9dce8 100644 --- a/dwds/lib/src/dwds_vm_client.dart +++ b/dwds/lib/src/dwds_vm_client.dart @@ -14,11 +14,29 @@ import 'package:dwds/src/utilities/synchronized.dart'; import 'package:logging/logging.dart'; import 'package:uuid/uuid.dart'; import 'package:vm_service/vm_service.dart'; +import 'package:vm_service/vm_service_io.dart'; import 'package:vm_service_interface/vm_service_interface.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; final _logger = Logger('DwdsVmClient'); +/// Type of requests added to the request controller. +typedef VmRequest = Map; + +/// Type of responses added to the response controller. +typedef VmResponse = Map; + +enum _NamespacedServiceExtension { + extDwdsEmitEvent(method: 'ext.dwds.emitEvent'), + extDwdsScreenshot(method: 'ext.dwds.screenshot'), + extDwdsSendEvent(method: 'ext.dwds.sendEvent'), + flutterListViews(method: '_flutter.listViews'); + + const _NamespacedServiceExtension({required this.method}); + + final String method; +} + // A client of the vm service that registers some custom extensions like // hotRestart. class DwdsVmClient { @@ -26,9 +44,6 @@ class DwdsVmClient { final StreamController> _requestController; final StreamController> _responseController; - static const int kFeatureDisabled = 100; - static const String kFeatureDisabledMessage = 'Feature is disabled.'; - /// Null until [close] is called. /// /// All subsequent calls to [close] will return this future. @@ -48,54 +63,210 @@ class DwdsVmClient { static Future create( DebugService debugService, DwdsStats dwdsStats, + Uri? ddsUri, ) async { - // Set up hot restart as an extension. - final requestController = StreamController>(); - final responseController = StreamController>(); - VmServerConnection( - requestController.stream, - responseController.sink, - debugService.serviceExtensionRegistry, - debugService.chromeProxyService, + final chromeProxyService = + debugService.chromeProxyService as 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; + + _setUpVmServerConnection( + chromeProxyService: chromeProxyService, + debugService: debugService, + responseStream: responseStream, + responseSink: responseSink, + requestStream: requestStream, + requestSink: requestSink, + dwdsStats: dwdsStats, + ); + + final client = ddsUri == null + ? _setUpVmClient( + responseStream: responseStream, + requestController: requestController, + requestSink: requestSink, + ) + : await _setUpDdsClient( + ddsUri: ddsUri, + ); + + final dwdsVmClient = + DwdsVmClient(client, requestController, responseController); + + await _registerServiceExtensions( + client: client, + chromeProxyService: chromeProxyService, + dwdsVmClient: dwdsVmClient, ); - final client = - VmService(responseController.stream.map(jsonEncode), (request) { + + 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 { + final client = await vmServiceConnectUri(ddsUri.toString()); + return client; + } + + /// Establishes a VM service client that bypasses DDS and registers service + /// 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) { _logger.warning( 'Attempted to send a request but the connection is closed:\n\n' '$request'); return; } - requestController.sink.add(Map.from(jsonDecode(request))); + requestSink.add(Map.from(jsonDecode(request))); }); - final chromeProxyService = - debugService.chromeProxyService as ChromeProxyService; - final dwdsVmClient = - DwdsVmClient(client, requestController, responseController); + return client; + } - // Register '_flutter.listViews' method on the chrome proxy service vm. - // In native world, this method is provided by the engine, but the web - // engine is not aware of the VM uri or the isolates. - // - // Issue: https://github.com/dart-lang/webdev/issues/1315 - client.registerServiceCallback('_flutter.listViews', (request) async { - final vm = await chromeProxyService.getVM(); - final isolates = vm.isolates; - return { - 'result': { - 'views': [ - for (var isolate in isolates ?? []) - { - 'id': isolate.id, - 'isolate': isolate.toJson(), - }, - ], - }, - }; + /// Establishes a direct connection with the VM Server. + /// + /// This is used to register the [_NamespacedServiceExtension]s. Because + /// namespaced service extensions are supposed to be registered by the engine, + /// we need to register them on the VM server connection instead of via DDS. + /// + /// TODO(https://github.com/dart-lang/webdev/issues/1315): Ideally the engine + /// 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 DebugService debugService, + required Stream responseStream, + required StreamSink responseSink, + required Stream requestStream, + required StreamSink requestSink, + }) { + responseStream.listen((request) async { + final response = await _maybeHandleServiceExtensionRequest( + request, + chromeProxyService: chromeProxyService, + dwdsStats: dwdsStats, + ); + if (response != null) { + requestSink.add(response); + } }); - await client.registerService('_flutter.listViews', 'DWDS'); + final vmServerConnection = VmServerConnection( + requestStream, + responseSink, + debugService.serviceExtensionRegistry, + debugService.chromeProxyService, + ); + + for (final extension in _NamespacedServiceExtension.values) { + debugService.serviceExtensionRegistry + .registerExtension(extension.method, vmServerConnection); + } + } + + static Future _maybeHandleServiceExtensionRequest( + VmResponse request, { + required ChromeProxyService chromeProxyService, + required DwdsStats dwdsStats, + }) 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); + } else if (method == _NamespacedServiceExtension.extDwdsSendEvent.method) { + response = await _extDwdsSendEventHandler(request, dwdsStats); + } else if (method == _NamespacedServiceExtension.extDwdsScreenshot.method) { + response = await _extDwdsScreenshotHandler(chromeProxyService); + } + + if (response != null) { + response['id'] = request['id'] as String; + // This is necessary even though DWDS doesn't use package:json_rpc_2. + // Without it, the response will be treated as invalid: + // https://github.com/dart-lang/json_rpc_2/blob/639857be892050159f5164c749d7947694976a4a/lib/src/server.dart#L252 + response['jsonrpc'] = '2.0'; + } + + return response; + } + + static Future> _flutterListViewsHandler( + ChromeProxyService chromeProxyService, + ) async { + final vm = await chromeProxyService.getVM(); + final isolates = vm.isolates; + return { + 'result': { + 'views': [ + for (var isolate in isolates ?? []) + { + 'id': isolate.id, + 'isolate': isolate.toJson(), + }, + ], + }, + }; + } + + static Future> _extDwdsScreenshotHandler( + ChromeProxyService chromeProxyService, + ) async { + await chromeProxyService.remoteDebugger.enablePage(); + final response = await chromeProxyService.remoteDebugger + .sendCommand('Page.captureScreenshot'); + return {'result': response.result as Object}; + } + + static Future> _extDwdsSendEventHandler( + VmResponse request, + DwdsStats dwdsStats, + ) async { + _processSendEvent(request, dwdsStats); + return {'result': Success().toJson()}; + } + + static Map _extDwdsEmitEventHandler( + VmResponse request, + ) { + 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) { + emitEvent( + DwdsEvent(type, payload), + ); + } + } + + return {'result': Success().toJson()}; + } + + static Future _registerServiceExtensions({ + required VmService client, + required ChromeProxyService chromeProxyService, + required DwdsVmClient dwdsVmClient, + }) async { client.registerServiceCallback( 'hotRestart', (request) => captureElapsedTime( @@ -113,55 +284,6 @@ class DwdsVmClient { ), ); await client.registerService('fullReload', 'DWDS'); - - client.registerServiceCallback('ext.dwds.screenshot', (_) async { - await chromeProxyService.remoteDebugger.enablePage(); - final response = await chromeProxyService.remoteDebugger - .sendCommand('Page.captureScreenshot'); - return {'result': response.result}; - }); - await client.registerService('ext.dwds.screenshot', 'DWDS'); - - client.registerServiceCallback('ext.dwds.sendEvent', (event) async { - _processSendEvent(event, dwdsStats); - return {'result': Success().toJson()}; - }); - await client.registerService('ext.dwds.sendEvent', 'DWDS'); - - client.registerServiceCallback('ext.dwds.emitEvent', (event) async { - emitEvent( - DwdsEvent( - event['type'] as String, - event['payload'] as Map, - ), - ); - return {'result': Success().toJson()}; - }); - await client.registerService('ext.dwds.emitEvent', 'DWDS'); - - client.registerServiceCallback('_yieldControlToDDS', (request) async { - final ddsUri = request['uri'] as String?; - if (ddsUri == null) { - return RPCError( - request['method'] as String, - RPCErrorKind.kInvalidParams.code, - "'Missing parameter: 'uri'", - ).toMap(); - } - return DebugService.yieldControlToDDS(ddsUri) - ? {'result': Success().toJson()} - : { - 'error': { - 'code': kFeatureDisabled, - 'message': kFeatureDisabledMessage, - 'data': - 'Existing VM service clients prevent DDS from taking control.', - }, - }; - }); - await client.registerService('_yieldControlToDDS', 'DWDS'); - - return dwdsVmClient; } Future> hotRestart( @@ -173,9 +295,11 @@ class DwdsVmClient { } void _processSendEvent( - Map event, + 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) { diff --git a/dwds/lib/src/handlers/dev_handler.dart b/dwds/lib/src/handlers/dev_handler.dart index cf3443e4f..13cbb01bb 100644 --- a/dwds/lib/src/handlers/dev_handler.dart +++ b/dwds/lib/src/handlers/dev_handler.dart @@ -509,12 +509,14 @@ class DevHandler { DebugService debugService, ) async { final dwdsStats = DwdsStats(); - final webdevClient = await DwdsVmClient.create(debugService, dwdsStats); + Uri? ddsUri; if (_spawnDds) { - await debugService.startDartDevelopmentService(); + final dds = await debugService.startDartDevelopmentService(); + ddsUri = dds.wsUri; } + final vmClient = await DwdsVmClient.create(debugService, dwdsStats, ddsUri); final appDebugService = - AppDebugServices(debugService, webdevClient, dwdsStats); + AppDebugServices(debugService, vmClient, dwdsStats, ddsUri); final encodedUri = await debugService.encodedUri; _logger.info('Debug service listening on $encodedUri\n'); await appDebugService.chromeProxyService.remoteDebugger.sendCommand( diff --git a/dwds/lib/src/services/app_debug_services.dart b/dwds/lib/src/services/app_debug_services.dart index 2e07a4479..fb9d18667 100644 --- a/dwds/lib/src/services/app_debug_services.dart +++ b/dwds/lib/src/services/app_debug_services.dart @@ -13,6 +13,7 @@ class AppDebugServices { final DebugService debugService; final DwdsVmClient dwdsVmClient; final DwdsStats dwdsStats; + final Uri? ddsUri; ChromeProxyService get chromeProxyService => debugService.chromeProxyService as ChromeProxyService; @@ -27,7 +28,12 @@ class AppDebugServices { /// We only allow a given app to be debugged in a single tab at a time. String? connectedInstanceId; - AppDebugServices(this.debugService, this.dwdsVmClient, this.dwdsStats); + AppDebugServices( + this.debugService, + this.dwdsVmClient, + this.dwdsStats, + this.ddsUri, + ); Future close() => _closed ??= Future.wait([debugService.close(), dwdsVmClient.close()]); diff --git a/dwds/lib/src/services/chrome_proxy_service.dart b/dwds/lib/src/services/chrome_proxy_service.dart index 84e9ad568..519e98118 100644 --- a/dwds/lib/src/services/chrome_proxy_service.dart +++ b/dwds/lib/src/services/chrome_proxy_service.dart @@ -21,6 +21,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/expression_compiler.dart'; import 'package:dwds/src/services/expression_evaluator.dart'; import 'package:dwds/src/utilities/dart_uri.dart'; @@ -1542,7 +1543,15 @@ ${globalToolConfiguration.loadStrategy.loadModuleSnippet}("dart_sdk").developer. @override Future yieldControlToDDS(String uri) async { - // TODO(elliette): implement + final canYield = DebugService.yieldControlToDDS(uri); + + if (!canYield) { + throw RPCError( + 'yieldControlToDDS', + RPCErrorKind.kFeatureDisabled.code, + 'Existing VM service clients prevent DDS from taking control.', + ); + } } @override diff --git a/dwds/lib/src/services/debug_service.dart b/dwds/lib/src/services/debug_service.dart index e6d4ce753..23279ac9e 100644 --- a/dwds/lib/src/services/debug_service.dart +++ b/dwds/lib/src/services/debug_service.dart @@ -160,7 +160,7 @@ 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.startDartDevelopmentService( @@ -177,6 +177,7 @@ class DebugService { ), ipv6: await useIPv6ForHost(hostname), ); + return _dds!; } String get uri { @@ -208,6 +209,8 @@ class DebugService { return _encodedUri = encoded; } + // TODO(https://github.com/dart-lang/webdev/issues/2399): yieldControlToDDS + // should disconnect existing non-DDS clients. static bool yieldControlToDDS(String uri) { if (_clientsConnected > 1) { return false; diff --git a/dwds/test/debug_service_test.dart b/dwds/test/debug_service_test.dart index cefdf47fa..d5320ec97 100644 --- a/dwds/test/debug_service_test.dart +++ b/dwds/test/debug_service_test.dart @@ -10,6 +10,7 @@ import 'dart:io'; import 'package:test/test.dart'; import 'package:test_common/test_sdk_configuration.dart'; +import 'package:vm_service/vm_service.dart'; import 'fixtures/context.dart'; import 'fixtures/project.dart'; @@ -88,8 +89,6 @@ void main() { completes, ); }, - // TODO(elliette): Re-enable test. - skip: true, ); test( @@ -124,16 +123,14 @@ void main() { expect(response.containsKey('error'), isTrue); final result = response['error'] as Map; - expect(result['message'], 'Feature is disabled.'); + expect(result['code'], RPCErrorKind.kFeatureDisabled.code); expect( - result['data'], + result['message'], 'Existing VM service clients prevent DDS from taking control.', ); await ddsWs.close(); await ws.close(); }, - // TODO(elliette): Re-enable test. - skip: true, ); } diff --git a/dwds/test/events_test.dart b/dwds/test/events_test.dart index 256a1508d..186439fa2 100644 --- a/dwds/test/events_test.dart +++ b/dwds/test/events_test.dart @@ -81,9 +81,9 @@ void main() { 'with dwds', () { Future? initialEvents; - late VmService vmService; late Keyboard keyboard; late Stream events; + late VmService fakeClient; /// Runs [action] and waits for an event matching [eventMatcher]. Future expectEventDuring( @@ -136,9 +136,9 @@ void main() { testSettings: TestSettings(enableExpressionEvaluation: true), debugSettings: TestDebugSettings.withDevTools(context), ); - vmService = context.debugConnection.vmService; keyboard = context.webDriver.driver.keyboard; events = context.testServer.dwds.events; + fakeClient = await context.connectFakeClient(); }); tearDownAll(() async { @@ -162,6 +162,7 @@ void main() { () => keyboard.sendChord([Keyboard.alt, 'd']), ); }, + skip: 'https://github.com/dart-lang/webdev/issues/2394', ); test('emits DEVTOOLS_LAUNCH event', () async { @@ -179,7 +180,7 @@ void main() { test('can emit event through service extension', () async { final response = await expectEventDuring( matchesEvent('foo-event', {'data': 1234}), - () => vmService.callServiceExtension( + () => fakeClient.callServiceExtension( 'ext.dwds.emitEvent', args: { 'type': 'foo-event', @@ -435,13 +436,14 @@ void main() { }); test('emits HOT_RESTART event', () async { - final client = context.debugConnection.vmService; + final hotRestart = + context.getRegisteredServiceExtension('hotRestart'); await expectEventDuring( matchesEvent(DwdsEventKind.hotRestart, { 'elapsedMilliseconds': isNotNull, }), - () => client.callServiceExtension('hotRestart'), + () => fakeClient.callServiceExtension(hotRestart!), ); }); }); @@ -495,13 +497,14 @@ void main() { }); test('emits FULL_RELOAD event', () async { - final client = context.debugConnection.vmService; + final fullReload = + context.getRegisteredServiceExtension('fullReload'); await expectEventDuring( matchesEvent(DwdsEventKind.fullReload, { 'elapsedMilliseconds': isNotNull, }), - () => client.callServiceExtension('fullReload'), + () => fakeClient.callServiceExtension(fullReload!), ); }); }); diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart index 7780ed271..c0b63ce64 100644 --- a/dwds/test/fixtures/context.dart +++ b/dwds/test/fixtures/context.dart @@ -37,6 +37,7 @@ import 'package:test_common/logging.dart'; import 'package:test_common/test_sdk_configuration.dart'; import 'package:test_common/utilities.dart'; import 'package:vm_service/vm_service.dart'; +import 'package:vm_service/vm_service_io.dart'; import 'package:webdriver/async_io.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; @@ -113,6 +114,8 @@ class TestContext { final _logger = logging.Logger('Context'); + final _serviceNameToMethod = {}; + /// Internal VM service. /// /// Prefer using [vmService] instead in tests when possible, to include testing @@ -470,6 +473,39 @@ class TestContext { } } + /// Creates a VM service connection connected to the debug URI. + /// + /// This can be used to test behavior that should be available to a client + /// connected to DWDS. + Future connectFakeClient() async { + final fakeClient = await vmServiceConnectUri(debugConnection.uri); + + fakeClient.onEvent(EventStreams.kService).listen(_handleServiceEvent); + await fakeClient.streamListen(EventStreams.kService); + + return fakeClient; + } + + /// Returns the service extension method given the [extensionName]. + /// + /// The extension be called by a client created with [connectFakeClient]. + String? getRegisteredServiceExtension(String extensionName) { + if (_serviceNameToMethod.isEmpty) { + throw StateError(''' + No registered service extensions. Did you call connectFakeClient? + '''); + } + + return _serviceNameToMethod[extensionName]; + } + + void _handleServiceEvent(Event e) { + if (e.kind == EventKind.kServiceRegistered) { + final serviceName = e.service!; + _serviceNameToMethod[serviceName] = e.method; + } + } + Future startDebugging() async { debugConnection = await testServer.dwds.debugConnection(appConnection); _webkitDebugger = WebkitDebugger(WipDebugger(tabConnection)); diff --git a/dwds/test/reload_correctness_test.dart b/dwds/test/reload_correctness_test.dart index 98c1648bc..95039e545 100644 --- a/dwds/test/reload_correctness_test.dart +++ b/dwds/test/reload_correctness_test.dart @@ -52,6 +52,8 @@ void main() { group( 'Injected client', () { + VmService? fakeClient; + setUp(() async { setCurrentLogWriter(debug: debug); await context.setUp( @@ -59,6 +61,8 @@ void main() { enableExpressionEvaluation: true, ), ); + + fakeClient = await context.connectFakeClient(); }); tearDown(() async { @@ -93,8 +97,9 @@ void main() { ), ); + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); expect( - await client.callServiceExtension('hotRestart'), + await fakeClient!.callServiceExtension(hotRestart!), const TypeMatcher(), ); diff --git a/dwds/test/reload_test.dart b/dwds/test/reload_test.dart index 18f9c5614..19fb076dc 100644 --- a/dwds/test/reload_test.dart +++ b/dwds/test/reload_test.dart @@ -136,6 +136,8 @@ void main() { group( 'Injected client', () { + late VmService fakeClient; + setUp(() async { setCurrentLogWriter(debug: debug); await context.setUp( @@ -143,6 +145,7 @@ void main() { enableExpressionEvaluation: true, ), ); + fakeClient = await context.connectFakeClient(); }); tearDown(() async { @@ -166,8 +169,9 @@ void main() { ), ); + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); expect( - await client.callServiceExtension('hotRestart'), + await fakeClient.callServiceExtension(hotRestart!), const TypeMatcher(), ); @@ -191,9 +195,10 @@ void main() { ); // Execute two hot restart calls in parallel. + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); final done = Future.wait([ - client.callServiceExtension('hotRestart'), - client.callServiceExtension('hotRestart'), + fakeClient.callServiceExtension(hotRestart!), + fakeClient.callServiceExtension(hotRestart), ]); expect( await done, @@ -256,9 +261,9 @@ void main() { ]), ), ); - + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); expect( - await client.callServiceExtension('hotRestart'), + await fakeClient.callServiceExtension(hotRestart!), const TypeMatcher(), ); @@ -299,8 +304,9 @@ void main() { "registerExtension('ext.foo', $callback)", ); + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); expect( - await client.callServiceExtension('hotRestart'), + await fakeClient.callServiceExtension(hotRestart!), const TypeMatcher(), ); @@ -339,7 +345,11 @@ void main() { ), ); - expect(await client.callServiceExtension('fullReload'), isA()); + final fullReload = context.getRegisteredServiceExtension('fullReload'); + expect( + await fakeClient.callServiceExtension(fullReload!), + isA(), + ); await eventsDone; @@ -365,7 +375,8 @@ void main() { .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); await makeEditAndWaitForRebuild(); - await client.callServiceExtension('hotRestart'); + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + await fakeClient.callServiceExtension(hotRestart!); final source = await context.webDriver.pageSource; // Main is re-invoked which shouldn't clear the state. @@ -383,7 +394,8 @@ void main() { test('can evaluate expressions after hot restart', () async { final client = context.debugConnection.vmService; - await client.callServiceExtension('hotRestart'); + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); + await fakeClient.callServiceExtension(hotRestart!); final vm = await client.getVM(); final isolateId = vm.isolates!.first.id!; @@ -500,6 +512,7 @@ void main() { // the FrontendServer as well. group('when isolates_paused_on_start is true', () { late VmService client; + late VmService fakeClient; setUp(() async { setCurrentLogWriter(debug: debug); @@ -509,6 +522,7 @@ void main() { ), ); client = context.debugConnection.vmService; + fakeClient = await context.connectFakeClient(); await client.setFlag('pause_isolates_on_start', 'true'); await client.streamListen('Isolate'); }); @@ -532,8 +546,9 @@ void main() { ), ); + final hotRestart = context.getRegisteredServiceExtension('hotRestart'); expect( - await client.callServiceExtension('hotRestart'), + await fakeClient.callServiceExtension(hotRestart!), const TypeMatcher(), ); diff --git a/example/web/main.dart b/example/web/main.dart index fdccc301b..151775b88 100644 --- a/example/web/main.dart +++ b/example/web/main.dart @@ -8,16 +8,16 @@ import 'dart:developer'; import 'dart:html'; void main() { - print('Initial Print'); + print('Initial Print :)'); registerExtension('ext.print', (_, __) async { - print('Hello World'); + print('Hello World !! :)'); return ServiceExtensionResponse.result(json.encode({'success': true})); }); document.body!.append(SpanElement()..text = 'Hello World!!'); var count = 0; Timer.periodic(const Duration(seconds: 1), (_) { - print('Counter is: ${++count}'); + print('Counter is now: ${++count}'); }); } diff --git a/webdev/lib/src/daemon/app_domain.dart b/webdev/lib/src/daemon/app_domain.dart index 2551269e9..cb8bd9c32 100644 --- a/webdev/lib/src/daemon/app_domain.dart +++ b/webdev/lib/src/daemon/app_domain.dart @@ -9,6 +9,7 @@ import 'dart:io'; import 'package:dwds/data/build_result.dart'; import 'package:dwds/dwds.dart'; import 'package:vm_service/vm_service.dart'; +import 'package:vm_service/vm_service_io.dart'; import '../serve/server_manager.dart'; import '../serve/webdev_server.dart'; @@ -24,6 +25,9 @@ class AppDomain extends Domain { final _appStates = {}; + // Mapping from service name to service method. + final Map _registeredMethodsForService = {}; + void _handleBuildResult(BuildResult result, String appId) { switch (result.status) { case BuildStatus.started: @@ -62,7 +66,8 @@ class AppDomain extends Domain { // The connection is established right before `main()` is called. await for (var appConnection in dwds.connectedApps) { var debugConnection = await dwds.debugConnection(appConnection); - var vmService = debugConnection.vmService; + final debugUri = debugConnection.ddsUri ?? debugConnection.uri; + final vmService = await vmServiceConnectUri(debugUri); var appId = appConnection.request.appId; unawaited(debugConnection.onDone.then((_) { sendEvent('app.log', { @@ -80,9 +85,6 @@ class AppDomain extends Domain { 'deviceId': 'chrome', 'launchMode': 'run' }); - sendEvent('app.started', { - 'appId': appId, - }); // TODO(grouma) - limit the catch to the appropriate error. try { await vmService.streamCancel('Stdout'); @@ -90,6 +92,10 @@ class AppDomain extends Domain { try { await vmService.streamListen('Stdout'); } catch (_) {} + try { + vmService.onServiceEvent.listen(_onServiceEvent); + await vmService.streamListen('Service'); + } catch (_) {} // ignore: cancel_subscriptions var stdOutSub = vmService.onStdoutEvent.listen((log) { sendEvent('app.log', { @@ -108,6 +114,9 @@ class AppDomain extends Domain { var appState = _AppState(debugConnection, resultSub, stdOutSub); _appStates[appId] = appState; + sendEvent('app.started', { + 'appId': appId, + }); appConnection.runMain(); @@ -121,6 +130,18 @@ class AppDomain extends Domain { if (_isShutdown) dispose(); } + void _onServiceEvent(Event e) { + if (e.kind == EventKind.kServiceRegistered) { + final serviceName = e.service!; + _registeredMethodsForService[serviceName] = e.method!; + } + + if (e.kind == EventKind.kServiceUnregistered) { + final serviceName = e.service!; + _registeredMethodsForService.remove(serviceName); + } + } + AppDomain(Daemon daemon, ServerManager serverManager) : super(daemon, 'app') { registerHandler('restart', _restart); registerHandler('callServiceExtension', _callServiceExtension); @@ -168,7 +189,10 @@ class AppDomain extends Domain { 'message': 'Performing hot restart...', 'progressId': 'hot.restart', }); - var response = await appState.vmService!.callServiceExtension('hotRestart'); + var restartMethod = + _registeredMethodsForService['hotRestart'] ?? 'hotRestart'; + var response = + await appState.vmService!.callServiceExtension(restartMethod); sendEvent('app.progress', { 'appId': appId, 'id': '$_progressEventId', diff --git a/webdev/test/daemon/app_domain_test.dart b/webdev/test/daemon/app_domain_test.dart index 546172400..20befa005 100644 --- a/webdev/test/daemon/app_domain_test.dart +++ b/webdev/test/daemon/app_domain_test.dart @@ -63,77 +63,101 @@ void main() { }); group('Methods', () { - test('.callServiceExtension', () async { - var webdev = await testRunner - .runWebDev(['daemon'], workingDirectory: exampleDirectory); - var appId = await waitForAppId(webdev); - if (Platform.isWindows) { - // Windows takes a bit longer to run the application and register - // the service extension. - await Future.delayed(const Duration(seconds: 5)); - } - var extensionCall = '[{"method":"app.callServiceExtension","id":0,' - '"params" : { "appId" : "$appId", "methodName" : "ext.print"}}]'; - webdev.stdin.add(utf8.encode('$extensionCall\n')); - // The example app sets up a service extension for printing. - await expectLater( - webdev.stdout, - emitsThrough( - startsWith('[{"event":"app.log","params":{"appId":"$appId",' - '"log":"Hello World\\n"}}'))); - await exitWebdev(webdev); - }); + test( + '.callServiceExtension', + () async { + var webdev = await testRunner + .runWebDev(['daemon'], workingDirectory: exampleDirectory); + var appId = await waitForAppId(webdev); + if (Platform.isWindows) { + // Windows takes a bit longer to run the application and register + // the service extension. + await Future.delayed(const Duration(seconds: 5)); + } + var extensionCall = '[{"method":"app.callServiceExtension","id":0,' + '"params" : { "appId" : "$appId", "methodName" : "ext.print"}}]'; + webdev.stdin.add(utf8.encode('$extensionCall\n')); + // The example app sets up a service extension for printing. + await expectLater( + webdev.stdout, + emitsThrough( + startsWith('[{"event":"app.log","params":{"appId":"$appId",' + '"log":"Hello World\\n"}}'))); + await exitWebdev(webdev); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); - test('.reload', () async { - var webdev = await testRunner - .runWebDev(['daemon'], workingDirectory: exampleDirectory); - var appId = await waitForAppId(webdev); - var extensionCall = '[{"method":"app.restart","id":0,' - '"params" : { "appId" : "$appId", "fullRestart" : false}}]'; - webdev.stdin.add(utf8.encode('$extensionCall\n')); - await expectLater( - webdev.stdout, - emitsThrough(startsWith( - '[{"id":0,"result":{"code":1,"message":"hot reload not yet supported', - )), - ); - await exitWebdev(webdev); - }); - - test('.restart', () async { - var webdev = await testRunner - .runWebDev(['daemon'], workingDirectory: exampleDirectory); - var appId = await waitForAppId(webdev); - var extensionCall = '[{"method":"app.restart","id":0,' - '"params" : { "appId" : "$appId", "fullRestart" : true}}]'; - webdev.stdin.add(utf8.encode('$extensionCall\n')); - await expectLater( - webdev.stdout, - emitsThrough(startsWith( - '[{"event":"app.progress","params":{"appId":"$appId","id":"1",' - '"message":"Performing hot restart..."'))); - await expectLater( + test( + '.reload', + () async { + var webdev = await testRunner + .runWebDev(['daemon'], workingDirectory: exampleDirectory); + var appId = await waitForAppId(webdev); + var extensionCall = '[{"method":"app.restart","id":0,' + '"params" : { "appId" : "$appId", "fullRestart" : false}}]'; + webdev.stdin.add(utf8.encode('$extensionCall\n')); + await expectLater( webdev.stdout, emitsThrough(startsWith( - '[{"event":"app.progress","params":{"appId":"$appId","id":"1",' - '"finished":true'))); - await exitWebdev(webdev); - }); + '[{"id":0,"result":{"code":1,"message":"hot reload not yet supported', + )), + ); + await exitWebdev(webdev); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); - test('.stop', () async { - var webdev = await testRunner - .runWebDev(['daemon'], workingDirectory: exampleDirectory); - var appId = await waitForAppId(webdev); - var stopCall = '[{"method":"app.stop","id":0,' - '"params" : { "appId" : "$appId"}}]'; - webdev.stdin.add(utf8.encode('$stopCall\n')); - await expectLater( - webdev.stdout, - emitsThrough(startsWith( - '[{"event":"app.stop","params":{"appId":"$appId"}}'))); - // This should cause webdev to exit. - expect(await webdev.exitCode, equals(0)); - }); + test( + '.restart', + () async { + var webdev = await testRunner + .runWebDev(['daemon'], workingDirectory: exampleDirectory); + var appId = await waitForAppId(webdev); + var extensionCall = '[{"method":"app.restart","id":0,' + '"params" : { "appId" : "$appId", "fullRestart" : true}}]'; + webdev.stdin.add(utf8.encode('$extensionCall\n')); + await expectLater( + webdev.stdout, + emitsThrough(startsWith( + '[{"event":"app.progress","params":{"appId":"$appId","id":"1",' + '"message":"Performing hot restart..."'))); + await expectLater( + webdev.stdout, + emitsThrough(startsWith( + '[{"event":"app.progress","params":{"appId":"$appId","id":"1",' + '"finished":true'))); + await exitWebdev(webdev); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); + + test( + '.stop', + () async { + var webdev = await testRunner + .runWebDev(['daemon'], workingDirectory: exampleDirectory); + var appId = await waitForAppId(webdev); + var stopCall = '[{"method":"app.stop","id":0,' + '"params" : { "appId" : "$appId"}}]'; + webdev.stdin.add(utf8.encode('$stopCall\n')); + await expectLater( + webdev.stdout, + emitsThrough(startsWith( + '[{"event":"app.stop","params":{"appId":"$appId"}}'))); + // This should cause webdev to exit. + expect(await webdev.exitCode, equals(0)); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); }); }); }