| // 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. |
| VmServiceInfo vmServiceInfo, |
| |
| /// The update event kind. |
| _VmServiceUpdateKind kind, |
| }); |
| |
| typedef _VmServiceWithInfo = ({VmService vmService, VmServiceInfo info}); |
| |
| enum _VmServiceUpdateKind { |
| registered(ConnectedAppServiceConstants.vmServiceRegistered), |
| unregistered(ConnectedAppServiceConstants.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.getVmServices, |
| _getVmServices, |
| ); |
| |
| _vmServiceUpdatesSubscription = _vmServiceUpdates.stream.listen((update) { |
| client.streamNotify(_streamId, { |
| DtdParameters.streamId: _streamId, |
| DtdParameters.eventKind: update.kind.id, |
| DtdParameters.eventData: { |
| DtdParameters.uri: update.vmServiceInfo.uri, |
| DtdParameters.exposedUri: update.vmServiceInfo.exposedUri, |
| DtdParameters.name: update.vmServiceInfo.name, |
| }, |
| DtdParameters.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, _VmServiceWithInfo>{}; |
| |
| /// 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[DtdParameters.secret].asString; |
| if (!unrestrictedMode && secret != incomingSecret) { |
| throw RpcErrorCodes.buildRpcException( |
| RpcErrorCodes.kPermissionDenied, |
| ); |
| } |
| |
| return await _mutex.runGuarded(() async { |
| final uri = parameters[DtdParameters.uri].asString; |
| if (_vmServices.containsKey(uri)) { |
| // We already know about this VM service instance. Exit early. |
| return Success().toJson(); |
| } |
| |
| final exposedUri = parameters[DtdParameters.exposedUri].asStringOrNull; |
| final name = parameters[DtdParameters.name].asStringOrNull; |
| |
| try { |
| await vmServiceConnectUri(uri).then((vmService) async { |
| final info = VmServiceInfo( |
| uri: uri, |
| exposedUri: exposedUri, |
| name: name, |
| ); |
| _vmServices[uri] = (vmService: vmService, info: info); |
| _vmServiceUpdates.add( |
| ( |
| vmServiceInfo: info, |
| 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[DtdParameters.secret].asString; |
| if (!unrestrictedMode && secret != incomingSecret) { |
| throw RpcErrorCodes.buildRpcException( |
| RpcErrorCodes.kPermissionDenied, |
| ); |
| } |
| |
| return await _mutex.runGuarded(() { |
| final uri = parameters[DtdParameters.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( |
| ( |
| vmServiceInfo: removedService.info, |
| kind: _VmServiceUpdateKind.unregistered |
| ), |
| ); |
| } |
| } |
| |
| /// Returns a response containing information for each VM service connection |
| /// in the context of this DTD instance. |
| Future<Map<String, Object?>> _getVmServices(Parameters _) async { |
| return await _mutex.runGuardedWeak(() { |
| return VmServicesResponse( |
| vmServicesInfos: _vmServices.values |
| .map((vmServiceWithInfo) => vmServiceWithInfo.info) |
| .toList(), |
| ).toJson(); |
| }); |
| } |
| } |
| |
| extension on Parameter { |
| String? get asStringOrNull => exists ? asString : null; |
| } |