| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:async'; |
| |
| // ignore: implementation_imports, acceptable since dtd_impl is not published. |
| import 'package:dds/src/utils/mutex.dart'; |
| import 'package:dtd/dtd.dart'; |
| import 'package:json_rpc_2/json_rpc_2.dart'; |
| import 'package:vm_service/vm_service.dart' hide Parameter, Success; |
| import 'package:vm_service/vm_service_io.dart'; |
| |
| import '../dtd_client.dart'; |
| import 'internal_service.dart'; |
| |
| typedef _VmServiceUpdate = ({ |
| /// The metadata describing the VM service connection that this update is for. |
| _VmServiceMetadata vmServiceMetadata, |
| |
| /// The update event kind. |
| _VmServiceUpdateKind kind, |
| }); |
| |
| typedef _VmServiceMetadata = ({ |
| /// The URI for the VM service connection. |
| String uri, |
| |
| /// The URI for the VM service connection that has been exposed to the |
| /// user/client machine if the backend VM service is running in a different |
| /// location (for example, an editor running in the user's browser with the |
| /// backend on a remote server). |
| /// |
| /// Code that runs on the user/client machine (such as DevTools and DevTools |
| /// extensions) should prefer this URI (if provided) whereas code that also |
| /// runs on the backend (such as the debug adapter) should always use [uri]. |
| /// |
| /// This value will be null or identical to [uri] in environments where |
| /// there is no exposing to do (for example, an editor running locally on the |
| /// same machine that the VM service is running). |
| String? exposedUri, |
| |
| /// The human-readable name for this VM service connection as defined by tool |
| /// or service that started it (e.g. 'Flutter - Pixel 5'). |
| /// |
| /// This is optional and may be null if the DTD client that registered the VM |
| /// service did not provide a name. |
| String? name, |
| }); |
| |
| typedef _VmServiceWithMetadata = ({ |
| VmService vmService, |
| _VmServiceMetadata metadata, |
| }); |
| |
| enum _VmServiceUpdateKind { |
| registered('VmServiceRegistered'), |
| unregistered('VmServiceUnregistered'); |
| |
| const _VmServiceUpdateKind(this.id); |
| |
| final String id; |
| } |
| |
| /// A service that stores the connections to Dart and Flutter applications that |
| /// DTD is aware of. |
| /// |
| /// This service allows priveleged clients like an IDE or DDS (the client that |
| /// started DTD) to register and unregister VM service URIs. The service |
| /// provides notifications on the [ConnectedAppServiceConstants.serviceName] |
| /// stream when VM service instances are registered or unregistered. |
| /// |
| /// DTD clients can use this service to gain access to the VM service URIs for |
| /// all running applications in the context of this DTD instance. |
| class ConnectedAppService extends InternalService { |
| ConnectedAppService({required this.secret, required this.unrestrictedMode}); |
| |
| /// The secret that a client must provide to call protected service methods |
| /// like 'registerVmService'. |
| /// |
| /// This secret is generated by DTD at startup and provided to the spawner |
| /// of DTD so that only trusted clients can call protected methods. |
| final String secret; |
| |
| /// Whether the connected app service is unrestricted, meaning that normally |
| /// protected service methods like 'registerVmService' do not require a client |
| /// secret to be called. |
| final bool unrestrictedMode; |
| |
| /// This [Mutex] is used to protect the global state of [_vmServices] so that |
| /// any read and write operations with asynchronous gaps are executed in |
| /// sequence. |
| /// |
| /// Any operation that updates the contents of [_vmServices] should be guarded |
| /// with `_mutex.runGuarded` and any operation that reads the contents of |
| /// [_vmServices] should be guarded with `_mutex.runGuardedWeak`. |
| final _mutex = Mutex(); |
| |
| @override |
| String get serviceName => ConnectedAppServiceConstants.serviceName; |
| |
| String get _streamId => serviceName; |
| |
| @override |
| void register(DTDClient client) { |
| client |
| ..registerServiceMethod( |
| serviceName, |
| ConnectedAppServiceConstants.registerVmService, |
| _registerVmService, |
| ) |
| ..registerServiceMethod( |
| serviceName, |
| ConnectedAppServiceConstants.unregisterVmService, |
| _unregisterVmService, |
| ) |
| ..registerServiceMethod( |
| serviceName, |
| ConnectedAppServiceConstants.getVmServiceUris, |
| _getVmServiceUris, |
| ); |
| |
| _vmServiceUpdatesSubscription = _vmServiceUpdates.stream.listen((update) { |
| client.streamNotify(_streamId, { |
| EventParameters.streamId: _streamId, |
| EventParameters.eventKind: update.kind.id, |
| EventParameters.eventData: { |
| EventParameters.uri: update.vmServiceMetadata.uri, |
| EventParameters.exposedUri: update.vmServiceMetadata.exposedUri, |
| EventParameters.name: update.vmServiceMetadata.name, |
| }, |
| EventParameters.timestamp: DateTime.now().millisecondsSinceEpoch, |
| }); |
| }); |
| } |
| |
| @override |
| Future<void> shutdown() async { |
| await _vmServiceUpdatesSubscription?.cancel(); |
| await _mutex.runGuarded(() async { |
| await Future.wait(_vmServices.values.map((e) => e.vmService.dispose())); |
| _vmServices.clear(); |
| }); |
| } |
| |
| /// The total set of VM service connections DTD is aware of, stored by their |
| /// URI as a String. |
| final _vmServices = <String, _VmServiceWithMetadata>{}; |
| |
| /// A [StreamController] used internally in this service to track service |
| /// registered and unregistered events. |
| final _vmServiceUpdates = StreamController<_VmServiceUpdate>.broadcast(); |
| |
| /// A [StreamSubscription] to the [_vmServiceUpdates] stream that is |
| /// responsible for passing VM service registration and unregistration updates |
| /// to the [DTDClient] that this [ConnectedAppService] was registered for. |
| /// |
| /// This [StreamSubscription] must be cancelled in `shutdown`. |
| StreamSubscription<_VmServiceUpdate>? _vmServiceUpdatesSubscription; |
| |
| /// Registers a VM service URI with the connected app service. |
| /// |
| /// Only the client that started DTD (identified by [_clientSecret]) |
| /// should be able to call this method. |
| Future<Map<String, Object?>> _registerVmService(Parameters parameters) async { |
| final incomingSecret = parameters[EventParameters.secret].asString; |
| if (!unrestrictedMode && secret != incomingSecret) { |
| throw RpcErrorCodes.buildRpcException( |
| RpcErrorCodes.kPermissionDenied, |
| ); |
| } |
| |
| return await _mutex.runGuarded(() async { |
| final uri = parameters[EventParameters.uri].asString; |
| if (_vmServices.containsKey(uri)) { |
| // We already know about this VM service instance. Exit early. |
| return Success().toJson(); |
| } |
| |
| final exposedUri = parameters[EventParameters.exposedUri].asStringOrNull; |
| final name = parameters[EventParameters.name].asStringOrNull; |
| |
| try { |
| await vmServiceConnectUri(uri).then((vmService) async { |
| final metadata = ( |
| uri: uri, |
| exposedUri: exposedUri, |
| name: name, |
| ); |
| _vmServices[uri] = (vmService: vmService, metadata: metadata); |
| _vmServiceUpdates.add( |
| ( |
| vmServiceMetadata: metadata, |
| kind: _VmServiceUpdateKind.registered, |
| ), |
| ); |
| unawaited(vmService.onDone.then((_) => _removeServiceAndNotify(uri))); |
| }); |
| } catch (e) { |
| throw RpcErrorCodes.buildRpcException( |
| RpcErrorCodes.kConnectionFailed, |
| data: {'message': 'Error connecting to VM service at $uri.\n$e'}, |
| ); |
| } |
| return Success().toJson(); |
| }); |
| } |
| |
| /// Unregisters a VM service URI from the connected app service. |
| /// |
| /// Only the client that started DTD (identified by [_clientSecret]) |
| /// should be able to call this method. |
| Future<Map<String, Object?>> _unregisterVmService( |
| Parameters parameters, |
| ) async { |
| final incomingSecret = parameters[EventParameters.secret].asString; |
| if (!unrestrictedMode && secret != incomingSecret) { |
| throw RpcErrorCodes.buildRpcException( |
| RpcErrorCodes.kPermissionDenied, |
| ); |
| } |
| |
| return await _mutex.runGuarded(() { |
| final uri = parameters[EventParameters.uri].asString; |
| if (!_vmServices.containsKey(uri)) { |
| // This VM service is not in the registry. Exit early. |
| return Success().toJson(); |
| } |
| |
| _removeServiceAndNotify(uri); |
| return Success().toJson(); |
| }); |
| } |
| |
| /// Removes the VM service with [uri] from [_vmServices] and posts an update |
| /// to the 'ConnectedApp' stream. |
| /// |
| /// This method should be called from within a `_mutex.runGuarded` block since |
| /// it performs operations on the [_vmServices] Map, which may be undergoing |
| /// updates from other asynchronous blocks. |
| void _removeServiceAndNotify(String uri) { |
| final removedService = _vmServices.remove(uri); |
| // Only send a notification if the service has not already been removed. |
| if (removedService != null) { |
| _vmServiceUpdates.add( |
| ( |
| vmServiceMetadata: removedService.metadata, |
| kind: _VmServiceUpdateKind.unregistered |
| ), |
| ); |
| } |
| } |
| |
| /// Returns a list of VM service URIs for running applications in the context |
| /// of this DTD instance. |
| Future<Map<String, Object?>> _getVmServiceUris(Parameters _) async { |
| return await _mutex.runGuardedWeak(() { |
| return StringListResponse(_vmServices.keys.toList()).toJson(); |
| }); |
| } |
| } |
| |
| extension on Parameter { |
| String? get asStringOrNull => exists ? asString : null; |
| } |