// Copyright (c) 2020, 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 'package:json_rpc_2/json_rpc_2.dart' as json_rpc; import 'client.dart'; import 'constants.dart'; import 'dds_impl.dart'; import 'named_lookup.dart'; import 'stream_manager.dart'; /// [_ClientResumePermissions] associates a list of /// [DartDevelopmentServiceClient]s, all of the same client name, with a /// permissions mask used to determine which pause event types require approval /// from one of the listed clients before resuming an isolate. class ClientResumePermissions { final List clients = []; int permissionsMask = 0; } /// The [ClientManager] has the responsibility of managing all state and /// requests related to client connections, including: /// - A list of all currently connected clients /// - Tracking client names and associated permissions for isolate resume /// synchronization /// - Handling RPC invocations which change client state class ClientManager { ClientManager(this.dds); /// Initialize state for a newly connected client. void addClient(DartDevelopmentServiceClient client) { _setClientNameHelper( client, client.defaultClientName, ); clients.add(client); client.listen().then((_) => removeClient(client)); if (clients.length == 1) { dds.isolateManager.initialize().then((_) { dds.streamManager.streamListen( null, StreamManager.kDebugStream, ); }); } } /// Cleanup state for a disconnected client. void removeClient(DartDevelopmentServiceClient client) { _clearClientName(client); clients.remove(client); if (clients.isEmpty) { dds.streamManager.streamCancel( null, StreamManager.kDebugStream, ); } } /// Cleanup clients on DDS shutdown. Future shutdown() async { // Close all incoming websocket connections. final futures = []; // Copy `clients` to guard against modification while iterating. for (final client in clients.toList()) { futures.add(client.close()); } await Future.wait(futures); } /// Associates a name with a given client. /// /// The provided client name is used to track isolate resume approvals. Map setClientName( DartDevelopmentServiceClient client, json_rpc.Parameters parameters, ) { _setClientNameHelper(client, parameters['name'].asString); return RPCResponses.success; } /// Require permission from this client before resuming an isolate. Future> requirePermissionToResume( DartDevelopmentServiceClient client, json_rpc.Parameters parameters, ) async { int pauseTypeMask = 0; if (parameters['onPauseStart'].asBoolOr(false)) { pauseTypeMask |= PauseTypeMasks.pauseOnStartMask; } if (parameters['onPauseReload'].asBoolOr(false)) { pauseTypeMask |= PauseTypeMasks.pauseOnReloadMask; } if (parameters['onPauseExit'].asBoolOr(false)) { pauseTypeMask |= PauseTypeMasks.pauseOnExitMask; } clientResumePermissions[client.name!]!.permissionsMask = pauseTypeMask; // Check to see if any isolates should resume as a result of the // resume permissions being updated. await dds.isolateManager.maybeResumeIsolates(); return RPCResponses.success; } /// Changes `client`'s name to `name` while also updating resume permissions /// and approvals. void _setClientNameHelper( DartDevelopmentServiceClient client, String name, ) { _clearClientName(client); client.name = name.isEmpty ? client.defaultClientName : name; clientResumePermissions.putIfAbsent( client.name!, () => ClientResumePermissions(), ); clientResumePermissions[client.name!]!.clients.add(client); } /// Resets the client's name while also cleaning up resume permissions and /// approvals. void _clearClientName( DartDevelopmentServiceClient client, ) { final name = client.name; client.name = null; final clientsForName = clientResumePermissions[name]; if (clientsForName != null) { clientsForName.clients.remove(client); // If this was the last client with a given name, cleanup resume // permissions. if (clientsForName.clients.isEmpty) { clientResumePermissions.remove(name); // Check to see if we need to resume any isolates now that the last // client of a given name has disconnected or changed names. // // An isolate will be resumed in this situation if: // // 1) This client required resume approvals for the current pause event // associated with the isolate and all other required resume approvals // have been provided by other clients. // // OR // // 2) This client required resume approvals for the current pause event // associated with the isolate, no other clients require resume approvals // for the current pause event, and at least one client has issued a resume // request. dds.isolateManager.isolates.forEach( (_, isolate) async => await isolate.maybeResumeAfterClientChange(name), ); } } } DartDevelopmentServiceClient? findFirstClientThatHandlesService( String service) { for (final client in clients) { if (client.services.containsKey(service)) { return client; } } return null; } // Handles namespace generation for service extensions. static const _kServicePrologue = 's'; final NamedLookup clients = NamedLookup( prologue: _kServicePrologue, ); /// Mapping of client names to all clients of that name and their resume /// permissions. final Map clientResumePermissions = {}; final DartDevelopmentServiceImpl dds; }