mirror of
https://github.com/dart-lang/sdk
synced 2024-09-16 03:17:55 +00:00
[ package:dds / Service ] Added support for client naming and resume permissions
Also marked `getClientName`, `setClientName`, `requirePermissionToResume` and `ClientName` as deprecated in the VM service specification as these features will be moving to DDS and will be removed in VM service protocol 4.0. Change-Id: I4628ece96349a9883ee9d726d82e5cfae028a826 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/144986 Reviewed-by: Ryan Macnak <rmacnak@google.com> Commit-Queue: Ben Konyi <bkonyi@google.com>
This commit is contained in:
parent
8d16dbb17f
commit
bb959fc0de
|
@ -8,14 +8,19 @@ The Service Protocol uses [JSON-RPC 2.0][].
|
|||
|
||||
[JSON-RPC 2.0]: http://www.jsonrpc.org/specification
|
||||
|
||||
|
||||
**Table of Contents**
|
||||
## Table of Contents
|
||||
|
||||
- [RPCs, Requests, and Responses](#rpcs-requests-and-responses)
|
||||
- [Events](#events)
|
||||
- [Types](#types)
|
||||
- [IDs and Names](#ids-and-names)
|
||||
- [Revision History](#revision-history)
|
||||
- [Public RPCs](#public-rpcs)
|
||||
- [getClientName](#getclientname)
|
||||
- [requirePermissionToResume](#requirepermissiontoresume)
|
||||
- [setClientName](#setclientname)
|
||||
- [Public Types](#public-types)
|
||||
- [ClientName](#clientname)
|
||||
|
||||
## RPCs, Requests, and Responses
|
||||
|
||||
|
@ -41,16 +46,95 @@ See the corresponding section in the VM Service protocol [here][service-protocol
|
|||
|
||||
The DDS Protocol supports all [public RPCs defined in the VM Service protocol][service-protocol-public-rpcs].
|
||||
|
||||
### getClientName
|
||||
|
||||
```
|
||||
ClientName getClientName()
|
||||
```
|
||||
|
||||
The _getClientName_ RPC is used to retrieve the name associated with the currently
|
||||
connected VM service client. If no name was previously set through the
|
||||
[setClientName](#setclientname) RPC, a default name will be returned.
|
||||
|
||||
See [ClientName](#clientname)
|
||||
|
||||
### requirePermissionToResume
|
||||
|
||||
```
|
||||
Success requirePermissionToResume(bool onPauseStart [optional],
|
||||
bool onPauseReload[optional],
|
||||
bool onPauseExit [optional])
|
||||
```
|
||||
|
||||
The _requirePermissionToResume_ RPC is used to change the pause/resume behavior
|
||||
of isolates by providing a way for the VM service to wait for approval to resume
|
||||
from some set of clients. This is useful for clients which want to perform some
|
||||
operation on an isolate after a pause without it being resumed by another client.
|
||||
|
||||
If the _onPauseStart_ parameter is `true`, isolates will not resume after pausing
|
||||
on start until the client sends a `resume` request and all other clients which
|
||||
need to provide resume approval for this pause type have done so.
|
||||
|
||||
If the _onPauseReload_ parameter is `true`, isolates will not resume after pausing
|
||||
after a reload until the client sends a `resume` request and all other clients
|
||||
which need to provide resume approval for this pause type have done so.
|
||||
|
||||
If the _onPauseExit_ parameter is `true`, isolates will not resume after pausing
|
||||
on exit until the client sends a `resume` request and all other clients which
|
||||
need to provide resume approval for this pause type have done so.
|
||||
|
||||
**Important Notes:**
|
||||
|
||||
- All clients with the same client name share resume permissions. Only a
|
||||
single client of a given name is required to provide resume approval.
|
||||
- When a client requiring approval disconnects from the service, a paused
|
||||
isolate may resume if all other clients requiring resume approval have
|
||||
already given approval. In the case that no other client requires resume
|
||||
approval for the current pause event, the isolate will be resumed if at
|
||||
least one other client has attempted to [resume](resume) the isolate.
|
||||
|
||||
### setClientName
|
||||
|
||||
```
|
||||
Success setClientName(string name)
|
||||
```
|
||||
|
||||
The _setClientName_ RPC is used to set a name to be associated with the currently
|
||||
connected VM service client. If the _name_ parameter is a non-empty string, _name_
|
||||
will become the new name associated with the client. If _name_ is an empty string,
|
||||
the client's name will be reset to its default name.
|
||||
|
||||
See [Success](#success).
|
||||
|
||||
## Public Types
|
||||
|
||||
The DDS Protocol supports all [public types defined in the VM Service protocol][service-protocol-public-types].
|
||||
|
||||
### ClientName
|
||||
|
||||
```
|
||||
class ClientName extends Response {
|
||||
// The name of the currently connected VM service client.
|
||||
string name;
|
||||
}
|
||||
```
|
||||
|
||||
See [getClientName](#getclientname) and [setClientName](#setclientname).
|
||||
|
||||
## Revision History
|
||||
|
||||
version | comments
|
||||
------- | --------
|
||||
0.x | Initial revision
|
||||
|
||||
[resume]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#resume
|
||||
[success]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#success
|
||||
|
||||
[service-protocol]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md
|
||||
[service-protocol-rpcs-requests-and-responses]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#rpcs-requests-and-responses
|
||||
[service-protocol-events]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#events
|
||||
[service-protocol-binary-events]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#binary-events
|
||||
[service-protocol-types]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#types
|
||||
[service-protocol-ids-and-names]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#ids-and-names
|
||||
[service-protocol-public-rpcs]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#public-rpcs
|
||||
[service-protocol-public-rpcs]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#public-rpcs
|
||||
[service-protocol-public-types]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#public-types
|
|
@ -25,7 +25,10 @@ import 'package:web_socket_channel/web_socket_channel.dart';
|
|||
|
||||
part 'src/binary_compatible_peer.dart';
|
||||
part 'src/client.dart';
|
||||
part 'src/client_manager.dart';
|
||||
part 'src/constants.dart';
|
||||
part 'src/dds_impl.dart';
|
||||
part 'src/isolate_manager.dart';
|
||||
part 'src/named_lookup.dart';
|
||||
part 'src/rpc_error_codes.dart';
|
||||
part 'src/stream_manager.dart';
|
||||
|
|
|
@ -51,13 +51,13 @@ class _DartDevelopmentServiceClient {
|
|||
_clientPeer.registerMethod('streamListen', (parameters) async {
|
||||
final streamId = parameters['streamId'].asString;
|
||||
await dds.streamManager.streamListen(this, streamId);
|
||||
return _success;
|
||||
return _RPCResponses.success;
|
||||
});
|
||||
|
||||
_clientPeer.registerMethod('streamCancel', (parameters) async {
|
||||
final streamId = parameters['streamId'].asString;
|
||||
await dds.streamManager.streamCancel(this, streamId);
|
||||
return _success;
|
||||
return _RPCResponses.success;
|
||||
});
|
||||
|
||||
_clientPeer.registerMethod('registerService', (parameters) async {
|
||||
|
@ -75,9 +75,30 @@ class _DartDevelopmentServiceClient {
|
|||
serviceId,
|
||||
alias,
|
||||
);
|
||||
return _success;
|
||||
return _RPCResponses.success;
|
||||
});
|
||||
|
||||
_clientPeer.registerMethod(
|
||||
'getClientName',
|
||||
(parameters) => {'type': 'ClientName', 'name': name},
|
||||
);
|
||||
|
||||
_clientPeer.registerMethod(
|
||||
'setClientName',
|
||||
(parameters) => dds.clientManager.setClientName(this, parameters),
|
||||
);
|
||||
|
||||
_clientPeer.registerMethod(
|
||||
'requirePermissionToResume',
|
||||
(parameters) =>
|
||||
dds.clientManager.requirePermissionToResume(this, parameters),
|
||||
);
|
||||
|
||||
_clientPeer.registerMethod(
|
||||
'resume',
|
||||
(parameters) => dds.isolateManager.resumeIsolate(this, parameters),
|
||||
);
|
||||
|
||||
// When invoked within a fallback, the next fallback will start executing.
|
||||
// The final fallback forwards the request to the VM service directly.
|
||||
@alwaysThrows
|
||||
|
@ -96,7 +117,7 @@ class _DartDevelopmentServiceClient {
|
|||
// method, forward the request to that client.
|
||||
final method = getMethod(parameters.method);
|
||||
final namespace = getNamespace(parameters.method);
|
||||
final serviceClient = dds._clients[namespace];
|
||||
final serviceClient = dds.clientManager.clients[namespace];
|
||||
if (serviceClient != null && serviceClient.services.containsKey(method)) {
|
||||
return await Future.any(
|
||||
[
|
||||
|
@ -123,9 +144,20 @@ class _DartDevelopmentServiceClient {
|
|||
await _vmServicePeer.sendRequest(parameters.method, parameters.asMap));
|
||||
}
|
||||
|
||||
static const _success = <String, dynamic>{
|
||||
'type': 'Success',
|
||||
};
|
||||
static int _idCounter = 0;
|
||||
final int _id = ++_idCounter;
|
||||
|
||||
/// The name given to the client upon its creation.
|
||||
String get defaultClientName => 'client$_id';
|
||||
|
||||
/// The current name associated with this client.
|
||||
String get name => _name;
|
||||
|
||||
// NOTE: this should not be called directly except from:
|
||||
// - `_ClientManager._clearClientName`
|
||||
// - `_ClientManager._setClientNameHelper`
|
||||
set name(String n) => _name = n ?? defaultClientName;
|
||||
String _name;
|
||||
|
||||
final _DartDevelopmentService dds;
|
||||
final Map<String, String> services = {};
|
||||
|
|
146
pkg/dds/lib/src/client_manager.dart
Normal file
146
pkg/dds/lib/src/client_manager.dart
Normal file
|
@ -0,0 +1,146 @@
|
|||
// 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.
|
||||
|
||||
part of dds;
|
||||
|
||||
/// [_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<_DartDevelopmentServiceClient> 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));
|
||||
}
|
||||
|
||||
/// Cleanup state for a disconnected client.
|
||||
void removeClient(_DartDevelopmentServiceClient client) {
|
||||
_clearClientName(client);
|
||||
clients.remove(client);
|
||||
}
|
||||
|
||||
/// Cleanup clients on DDS shutdown.
|
||||
Future<void> shutdown() async {
|
||||
// Close all incoming websocket connections.
|
||||
final futures = <Future>[];
|
||||
for (final client in clients) {
|
||||
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<String, dynamic> setClientName(
|
||||
_DartDevelopmentServiceClient client,
|
||||
json_rpc.Parameters parameters,
|
||||
) {
|
||||
_setClientNameHelper(client, parameters['name'].asString);
|
||||
return _RPCResponses.success;
|
||||
}
|
||||
|
||||
/// Require permission from this client before resuming an isolate.
|
||||
Map<String, dynamic> requirePermissionToResume(
|
||||
_DartDevelopmentServiceClient client,
|
||||
json_rpc.Parameters parameters,
|
||||
) {
|
||||
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;
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handles namespace generation for service extensions.
|
||||
static const _kServicePrologue = 's';
|
||||
final NamedLookup<_DartDevelopmentServiceClient> clients = NamedLookup(
|
||||
prologue: _kServicePrologue,
|
||||
);
|
||||
|
||||
/// Mapping of client names to all clients of that name and their resume
|
||||
/// permissions.
|
||||
final Map<String, _ClientResumePermissions> clientResumePermissions = {};
|
||||
|
||||
final _DartDevelopmentService dds;
|
||||
}
|
33
pkg/dds/lib/src/constants.dart
Normal file
33
pkg/dds/lib/src/constants.dart
Normal file
|
@ -0,0 +1,33 @@
|
|||
// 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.
|
||||
|
||||
part of dds;
|
||||
|
||||
abstract class _RPCResponses {
|
||||
static const success = <String, dynamic>{
|
||||
'type': 'Success',
|
||||
};
|
||||
|
||||
static const collectedSentinel = <String, dynamic>{
|
||||
'type': 'Sentinel',
|
||||
'kind': 'Collected',
|
||||
'valueAsString': '<collected>',
|
||||
};
|
||||
}
|
||||
|
||||
abstract class _PauseTypeMasks {
|
||||
static const pauseOnStartMask = 1 << 0;
|
||||
static const pauseOnReloadMask = 1 << 1;
|
||||
static const pauseOnExitMask = 1 << 2;
|
||||
}
|
||||
|
||||
abstract class _ServiceEvents {
|
||||
static const isolateExit = 'IsolateExit';
|
||||
static const isolateSpawn = 'IsolateSpawn';
|
||||
static const isolateStart = 'IsolateStart';
|
||||
static const pauseExit = 'PauseExit';
|
||||
static const pausePostRequest = 'PausePostRequest';
|
||||
static const pauseStart = 'PauseStart';
|
||||
static const resume = 'Resume';
|
||||
}
|
|
@ -6,18 +6,24 @@ part of dds;
|
|||
|
||||
class _DartDevelopmentService implements DartDevelopmentService {
|
||||
_DartDevelopmentService(this._remoteVmServiceUri, this._uri) {
|
||||
_clientManager = _ClientManager(this);
|
||||
_isolateManager = _IsolateManager(this);
|
||||
_streamManager = _StreamManager(this);
|
||||
}
|
||||
|
||||
Future<void> startService() async {
|
||||
// TODO(bkonyi): throw if we've already shutdown.
|
||||
// Establish the connection to the VM service.
|
||||
_vmServiceSocket = WebSocketChannel.connect(remoteVmServiceWsUri);
|
||||
_vmServiceClient = _BinaryCompatiblePeer(_vmServiceSocket, _streamManager);
|
||||
// Setup the JSON RPC client with the VM service.
|
||||
unawaited(_vmServiceClient.listen());
|
||||
unawaited(_vmServiceClient.listen().then((_) => shutdown()));
|
||||
|
||||
// Setup stream event handling.
|
||||
streamManager.listen();
|
||||
await streamManager.listen();
|
||||
|
||||
// Populate initial isolate state.
|
||||
await _isolateManager.initialize();
|
||||
|
||||
// Once we have a connection to the VM service, we're ready to spawn the intermediary.
|
||||
await _startDDSServer();
|
||||
|
@ -52,15 +58,16 @@ class _DartDevelopmentService implements DartDevelopmentService {
|
|||
|
||||
/// Stop accepting requests after gracefully handling existing requests.
|
||||
Future<void> shutdown() async {
|
||||
if (_done.isCompleted || _shuttingDown) {
|
||||
// Already shutdown.
|
||||
return;
|
||||
}
|
||||
_shuttingDown = true;
|
||||
// Don't accept anymore HTTP requests.
|
||||
await _server.close();
|
||||
|
||||
// Close all incoming websocket connections.
|
||||
final futures = <Future>[];
|
||||
for (final client in _clients) {
|
||||
futures.add(client.close());
|
||||
}
|
||||
await Future.wait(futures);
|
||||
// Close connections to clients.
|
||||
await clientManager.shutdown();
|
||||
|
||||
// Close connection to VM service.
|
||||
await _vmServiceSocket.sink.close();
|
||||
|
@ -79,8 +86,7 @@ class _DartDevelopmentService implements DartDevelopmentService {
|
|||
ws,
|
||||
_vmServiceClient,
|
||||
);
|
||||
_clients.add(client);
|
||||
client.listen().then((_) => _clients.remove(client));
|
||||
clientManager.addClient(client);
|
||||
});
|
||||
|
||||
Handler _httpHandler() {
|
||||
|
@ -108,7 +114,7 @@ class _DartDevelopmentService implements DartDevelopmentService {
|
|||
}
|
||||
|
||||
String _getNamespace(_DartDevelopmentServiceClient client) =>
|
||||
_clients.keyOf(client);
|
||||
clientManager.clients.keyOf(client);
|
||||
|
||||
Uri get remoteVmServiceUri => _remoteVmServiceUri;
|
||||
Uri get remoteVmServiceWsUri => _toWebSocket(_remoteVmServiceUri);
|
||||
|
@ -122,16 +128,17 @@ class _DartDevelopmentService implements DartDevelopmentService {
|
|||
|
||||
Future<void> get done => _done.future;
|
||||
Completer _done = Completer<void>();
|
||||
bool _shuttingDown = false;
|
||||
|
||||
_ClientManager get clientManager => _clientManager;
|
||||
_ClientManager _clientManager;
|
||||
|
||||
_IsolateManager get isolateManager => _isolateManager;
|
||||
_IsolateManager _isolateManager;
|
||||
|
||||
_StreamManager get streamManager => _streamManager;
|
||||
_StreamManager _streamManager;
|
||||
|
||||
// Handles namespace generation for service extensions.
|
||||
static const _kServicePrologue = 's';
|
||||
final NamedLookup<_DartDevelopmentServiceClient> _clients = NamedLookup(
|
||||
prologue: _kServicePrologue,
|
||||
);
|
||||
|
||||
json_rpc.Peer _vmServiceClient;
|
||||
WebSocketChannel _vmServiceSocket;
|
||||
HttpServer _server;
|
||||
|
|
228
pkg/dds/lib/src/isolate_manager.dart
Normal file
228
pkg/dds/lib/src/isolate_manager.dart
Normal file
|
@ -0,0 +1,228 @@
|
|||
// 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.
|
||||
|
||||
part of dds;
|
||||
|
||||
/// This file contains functionality used to track the running state of
|
||||
/// all isolates in a given Dart process.
|
||||
///
|
||||
/// [_RunningIsolate] is a representation of a single live isolate and contains
|
||||
/// running state information for that isolate. In addition, approvals from
|
||||
/// clients used to synchronize isolate resuming across multiple clients are
|
||||
/// tracked in this class.
|
||||
///
|
||||
/// The [_IsolateManager] keeps track of all the isolates in the
|
||||
/// target process and handles isolate lifecycle events including:
|
||||
/// - Startup
|
||||
/// - Shutdown
|
||||
/// - Pauses
|
||||
///
|
||||
/// The [_IsolateManager] also handles the `resume` RPC, which checks the
|
||||
/// resume approvals in the target [_RunningIsolate] to determine if the
|
||||
/// isolate should be resumed or wait for additional approvals to be granted.
|
||||
|
||||
enum _IsolateState {
|
||||
start,
|
||||
running,
|
||||
pauseStart,
|
||||
pauseExit,
|
||||
pausePostRequest,
|
||||
}
|
||||
|
||||
class _RunningIsolate {
|
||||
_RunningIsolate(this.isolateManager, this.portId, this.name);
|
||||
|
||||
// State setters.
|
||||
void pausedOnExit() => _state = _IsolateState.pauseExit;
|
||||
void pausedOnStart() => _state = _IsolateState.pauseStart;
|
||||
void pausedPostRequest() => _state = _IsolateState.pausePostRequest;
|
||||
void resumed() => running();
|
||||
void running() => _state = _IsolateState.running;
|
||||
void started() => _state = _IsolateState.start;
|
||||
|
||||
/// Resumes the isolate if all clients which need to approve a resume have
|
||||
/// done so. Called when the last client of a given name disconnects or
|
||||
/// changes name to ensure we don't deadlock waiting for approval to resume
|
||||
/// from a disconnected client.
|
||||
Future<void> maybeResumeAfterClientChange(String clientName) async {
|
||||
// Remove approvals from the disconnected client.
|
||||
_resumeApprovalsByName.remove(clientName);
|
||||
|
||||
if (shouldResume()) {
|
||||
clearResumeApprovals();
|
||||
await isolateManager.dds._vmServiceClient.sendRequest('resume', {
|
||||
'isolateId': 'isolates/$portId',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this isolate should resume given its client approvals
|
||||
/// state.
|
||||
///
|
||||
/// If `resumingClient` is provided, it will be added to the set of clients
|
||||
/// which have provided approval to resume this isolate. If not provided,
|
||||
/// the existing approvals state will be examined to see if the isolate
|
||||
/// should resume due to a client disconnect or name change.
|
||||
bool shouldResume({_DartDevelopmentServiceClient resumingClient}) {
|
||||
if (resumingClient != null) {
|
||||
// Mark approval by the client.
|
||||
_resumeApprovalsByName.add(resumingClient.name);
|
||||
}
|
||||
final requiredClientApprovals = <String>{};
|
||||
final permissions =
|
||||
isolateManager.dds.clientManager.clientResumePermissions;
|
||||
|
||||
// Determine which clients require approval for this pause type.
|
||||
permissions.forEach((name, clientNamePermissions) {
|
||||
if (clientNamePermissions.permissionsMask & _isolateStateMask != 0) {
|
||||
requiredClientApprovals.add(name);
|
||||
}
|
||||
});
|
||||
|
||||
// We require at least a single client to resume, even if that client
|
||||
// doesn't require resume approval.
|
||||
if (_resumeApprovalsByName.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If all the required approvals are present, we should resume.
|
||||
return _resumeApprovalsByName.containsAll(requiredClientApprovals);
|
||||
}
|
||||
|
||||
/// Resets the internal resume approvals state.
|
||||
///
|
||||
/// Should always be called after an isolate is resumed.
|
||||
void clearResumeApprovals() => _resumeApprovalsByName.clear();
|
||||
|
||||
int get _isolateStateMask => isolateStateToMaskMapping[_state] ?? 0;
|
||||
|
||||
static const isolateStateToMaskMapping = {
|
||||
_IsolateState.pauseStart: _PauseTypeMasks.pauseOnStartMask,
|
||||
_IsolateState.pausePostRequest: _PauseTypeMasks.pauseOnReloadMask,
|
||||
_IsolateState.pauseExit: _PauseTypeMasks.pauseOnExitMask,
|
||||
};
|
||||
|
||||
final _IsolateManager isolateManager;
|
||||
final String name;
|
||||
final String portId;
|
||||
final Set<String> _resumeApprovalsByName = {};
|
||||
_IsolateState _state;
|
||||
}
|
||||
|
||||
class _IsolateManager {
|
||||
_IsolateManager(this.dds);
|
||||
|
||||
/// Handles state changes for isolates.
|
||||
void handleIsolateEvent(json_rpc.Parameters parameters) {
|
||||
final event = parameters['event'];
|
||||
final eventKind = event['kind'].asString;
|
||||
|
||||
// There's no interesting information about isolate state associated with
|
||||
// and IsolateSpawn event.
|
||||
if (eventKind == _ServiceEvents.isolateSpawn) {
|
||||
return;
|
||||
}
|
||||
|
||||
final isolateData = event['isolate'];
|
||||
final id = isolateData['number'].asString;
|
||||
final name = isolateData['name'].asString;
|
||||
|
||||
switch (eventKind) {
|
||||
case _ServiceEvents.isolateStart:
|
||||
isolateStarted(id, name);
|
||||
break;
|
||||
case _ServiceEvents.isolateExit:
|
||||
isolateExited(id);
|
||||
break;
|
||||
default:
|
||||
final isolate = isolates[id];
|
||||
switch (eventKind) {
|
||||
case _ServiceEvents.pauseExit:
|
||||
isolate.pausedOnExit();
|
||||
break;
|
||||
case _ServiceEvents.pausePostRequest:
|
||||
isolate.pausedPostRequest();
|
||||
break;
|
||||
case _ServiceEvents.pauseStart:
|
||||
isolate.pausedOnStart();
|
||||
break;
|
||||
case _ServiceEvents.resume:
|
||||
isolate.resumed();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the set of running isolates.
|
||||
Future<void> initialize() async {
|
||||
final vm = await dds._vmServiceClient.sendRequest('getVM');
|
||||
final List<Map> isolates = vm['isolates'].cast<Map<String, dynamic>>();
|
||||
isolates.forEach(
|
||||
(isolate) => isolateStarted(
|
||||
isolate['number'],
|
||||
isolate['name'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Initializes state for a newly started isolate.
|
||||
void isolateStarted(String portId, String name) {
|
||||
final isolate = _RunningIsolate(this, portId, name);
|
||||
isolate.running();
|
||||
isolates[portId] = isolate;
|
||||
}
|
||||
|
||||
/// Cleans up state for an isolate that has exited.
|
||||
void isolateExited(String portId) {
|
||||
isolates.remove(portId);
|
||||
}
|
||||
|
||||
/// Handles `resume` RPC requests. If the client requires that approval be
|
||||
/// given before resuming an isolate, this method will:
|
||||
///
|
||||
/// - Update the approval state for the isolate.
|
||||
/// - Resume the isolate if approval has been given by all clients which
|
||||
/// require approval.
|
||||
///
|
||||
/// Returns a collected sentinel if the isolate no longer exists.
|
||||
Future<Map<String, dynamic>> resumeIsolate(
|
||||
_DartDevelopmentServiceClient client,
|
||||
json_rpc.Parameters parameters,
|
||||
) async {
|
||||
final isolateId = parameters['isolateId'].asString;
|
||||
final portId = _isolateIdToPortId(isolateId);
|
||||
final isolate = isolates[portId];
|
||||
if (isolate == null) {
|
||||
return _RPCResponses.collectedSentinel;
|
||||
}
|
||||
if (isolate.shouldResume(resumingClient: client)) {
|
||||
isolate.clearResumeApprovals();
|
||||
return await _sendResumeRequest(isolateId, parameters);
|
||||
}
|
||||
return _RPCResponses.success;
|
||||
}
|
||||
|
||||
/// Forwards a `resume` request to the VM service.
|
||||
Future<Map<String, dynamic>> _sendResumeRequest(
|
||||
String isolateId,
|
||||
json_rpc.Parameters parameters,
|
||||
) async {
|
||||
final step = parameters['step'].asStringOr(null);
|
||||
final frameIndex = parameters['frameIndex'].asIntOr(null);
|
||||
final resumeResult = await dds._vmServiceClient.sendRequest('resume', {
|
||||
'isolateId': isolateId,
|
||||
if (step != null) 'step': step,
|
||||
if (frameIndex != null) 'frameIndex': frameIndex,
|
||||
});
|
||||
return resumeResult;
|
||||
}
|
||||
|
||||
static String _isolateIdToPortId(String isolateId) =>
|
||||
isolateId.substring('isolates/'.length);
|
||||
|
||||
final _DartDevelopmentService dds;
|
||||
final Map<String, _RunningIsolate> isolates = {};
|
||||
}
|
|
@ -83,13 +83,24 @@ class _StreamManager {
|
|||
|
||||
/// Start listening for `streamNotify` events from the VM service and forward
|
||||
/// them to the clients which have subscribed to the stream.
|
||||
void listen() => dds._vmServiceClient.registerMethod(
|
||||
'streamNotify',
|
||||
(parameters) {
|
||||
final streamId = parameters['streamId'].asString;
|
||||
streamNotify(streamId, parameters.value);
|
||||
},
|
||||
);
|
||||
Future<void> listen() async {
|
||||
// The _IsolateManager requires information from both the Debug and
|
||||
// Isolate streams, so they must always be subscribed to by DDS.
|
||||
for (final stream in ddsCoreStreams) {
|
||||
await streamListen(null, stream);
|
||||
}
|
||||
dds._vmServiceClient.registerMethod(
|
||||
'streamNotify',
|
||||
(parameters) {
|
||||
final streamId = parameters['streamId'].asString;
|
||||
// Forward events from the streams _IsolateManager subscribes to.
|
||||
if (isolateManagerStreams.contains(streamId)) {
|
||||
dds.isolateManager.handleIsolateEvent(parameters);
|
||||
}
|
||||
streamNotify(streamId, parameters.value);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Subscribes `client` to a stream.
|
||||
///
|
||||
|
@ -112,7 +123,9 @@ class _StreamManager {
|
|||
if (streamListeners[stream].contains(client)) {
|
||||
throw kStreamAlreadySubscribedException;
|
||||
}
|
||||
streamListeners[stream].add(client);
|
||||
if (client != null) {
|
||||
streamListeners[stream].add(client);
|
||||
}
|
||||
}
|
||||
|
||||
/// Unsubscribes `client` from a stream.
|
||||
|
@ -129,7 +142,8 @@ class _StreamManager {
|
|||
throw kStreamNotSubscribedException;
|
||||
}
|
||||
listeners.remove(client);
|
||||
if (listeners.isEmpty) {
|
||||
// Don't cancel streams DDS needs to function.
|
||||
if (listeners.isEmpty && !ddsCoreStreams.contains(stream)) {
|
||||
streamListeners.remove(stream);
|
||||
final result = await dds._vmServiceClient.sendRequest('streamCancel', {
|
||||
'streamId': stream,
|
||||
|
@ -165,6 +179,21 @@ class _StreamManager {
|
|||
_RpcErrorCodes.kStreamNotSubscribed,
|
||||
);
|
||||
|
||||
static const kDebugStream = 'Debug';
|
||||
static const kIsolateStream = 'Isolate';
|
||||
|
||||
// Never cancel the Debug or Isolate stream as `_IsolateManager` requires
|
||||
// them for isolate state notifications.
|
||||
static const isolateManagerStreams = <String>{
|
||||
kDebugStream,
|
||||
kIsolateStream,
|
||||
};
|
||||
|
||||
// The set of streams that DDS requires to function.
|
||||
static final ddsCoreStreams = <String>{
|
||||
...isolateManagerStreams,
|
||||
};
|
||||
|
||||
final _DartDevelopmentService dds;
|
||||
final streamListeners = <String, List<_DartDevelopmentServiceClient>>{};
|
||||
}
|
||||
|
|
|
@ -66,6 +66,4 @@ var tests = <VMTest>[
|
|||
main(args) async => runVMTests(
|
||||
args,
|
||||
tests,
|
||||
// TODO(bkonyi): client names are not yet supported in DDS.
|
||||
enableDds: false,
|
||||
);
|
||||
|
|
|
@ -66,6 +66,4 @@ Future<void> main(args) => runIsolateTests(
|
|||
testeeConcurrent: fooBar,
|
||||
pause_on_start: true,
|
||||
pause_on_exit: true,
|
||||
// TODO(bkonyi): client names are not yet supported in DDS.
|
||||
enableDds: false,
|
||||
);
|
||||
|
|
|
@ -64,6 +64,4 @@ Future<void> main(args) => runIsolateTests(
|
|||
testeeConcurrent: fooBar,
|
||||
pause_on_start: true,
|
||||
pause_on_exit: true,
|
||||
// TODO(bkonyi): client names are not yet supported in DDS.
|
||||
enableDds: false,
|
||||
);
|
||||
|
|
|
@ -46,6 +46,4 @@ Future<void> main(args) => runIsolateTests(
|
|||
testeeConcurrent: fooBar,
|
||||
pause_on_start: true,
|
||||
pause_on_exit: true,
|
||||
// TODO(bkonyi): client names are not yet supported in DDS.
|
||||
enableDds: false,
|
||||
);
|
||||
|
|
|
@ -64,6 +64,4 @@ Future<void> main(args) => runIsolateTests(
|
|||
testeeConcurrent: fooBar,
|
||||
pause_on_start: true,
|
||||
pause_on_exit: true,
|
||||
// TODO(bkonyi): client names are not yet supported in DDS.
|
||||
enableDds: false,
|
||||
);
|
||||
|
|
|
@ -57,6 +57,4 @@ Future<void> main(args) => runIsolateTests(
|
|||
testeeConcurrent: fooBar,
|
||||
pause_on_start: true,
|
||||
pause_on_exit: true,
|
||||
// TODO(bkonyi): client names are not yet supported in DDS.
|
||||
enableDds: false,
|
||||
);
|
||||
|
|
|
@ -65,6 +65,4 @@ Future<void> main(args) => runIsolateTests(
|
|||
hotReloadTest,
|
||||
testeeConcurrent: fooBar,
|
||||
pause_on_start: true,
|
||||
// TODO(bkonyi): client names are not yet supported in DDS.
|
||||
enableDds: false,
|
||||
);
|
||||
|
|
|
@ -41,6 +41,4 @@ Future<void> main(args) => runIsolateTests(
|
|||
args,
|
||||
test,
|
||||
testeeBefore: fooBar,
|
||||
// TODO(bkonyi): client names are not yet supported in DDS.
|
||||
enableDds: false,
|
||||
);
|
||||
|
|
|
@ -12,7 +12,7 @@ var tests = <VMTest>[
|
|||
var result = await vm.invokeRpcNoUpgrade('getVersion', {});
|
||||
expect(result['type'], equals('Version'));
|
||||
expect(result['major'], equals(3));
|
||||
expect(result['minor'], equals(32));
|
||||
expect(result['minor'], equals(33));
|
||||
expect(result['_privateMajor'], equals(0));
|
||||
expect(result['_privateMinor'], equals(0));
|
||||
},
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
namespace dart {
|
||||
|
||||
#define SERVICE_PROTOCOL_MAJOR_VERSION 3
|
||||
#define SERVICE_PROTOCOL_MINOR_VERSION 32
|
||||
#define SERVICE_PROTOCOL_MINOR_VERSION 33
|
||||
|
||||
class Array;
|
||||
class EmbedderServiceHandler;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Dart VM Service Protocol 3.32
|
||||
# Dart VM Service Protocol 3.33
|
||||
|
||||
> Please post feedback to the [observatory-discuss group][discuss-list]
|
||||
|
||||
This document describes of _version 3.32_ of the Dart VM Service Protocol. This
|
||||
This document describes of _version 3.33_ of the Dart VM Service Protocol. This
|
||||
protocol is used to communicate with a running Dart Virtual Machine.
|
||||
|
||||
To use the Service Protocol, start the VM with the *--observe* flag.
|
||||
|
@ -726,6 +726,9 @@ See [ClassList](#classlist).
|
|||
|
||||
### getClientName
|
||||
|
||||
_**Note**: This method is deprecated and will be removed in v4.0 of the protocol.
|
||||
An equivalent can be found in the Dart Development Service (DDS) protocol._
|
||||
|
||||
```
|
||||
ClientName getClientName()
|
||||
```
|
||||
|
@ -1201,6 +1204,9 @@ _Collected_ [Sentinel](#sentinel) is returned.
|
|||
|
||||
### requirePermissionToResume
|
||||
|
||||
_**Note**: This method is deprecated and will be removed in v4.0 of the protocol.
|
||||
An equivalent can be found in the Dart Development Service (DDS) protocol._
|
||||
|
||||
```
|
||||
Success requirePermissionToResume(bool onPauseStart [optional],
|
||||
bool onPauseReload[optional],
|
||||
|
@ -1234,7 +1240,6 @@ need to provide resume approval for this pause type have done so.
|
|||
approval for the current pause event, the isolate will be resumed if at
|
||||
least one other client has attempted to [resume](#resume) the isolate.
|
||||
|
||||
|
||||
### resume
|
||||
|
||||
```
|
||||
|
@ -1271,6 +1276,9 @@ See [Success](#success), [StepOption](#StepOption).
|
|||
|
||||
### setClientName
|
||||
|
||||
_**Note**: This method is deprecated and will be removed in v4.0 of the protocol.
|
||||
An equivalent can be found in the Dart Development Service (DDS) protocol._
|
||||
|
||||
```
|
||||
Success setClientName(string name)
|
||||
```
|
||||
|
@ -1416,7 +1424,7 @@ The _streamId_ parameter may have the following published values:
|
|||
streamId | event types provided
|
||||
-------- | -----------
|
||||
VM | VMUpdate, VMFlagUpdate
|
||||
Isolate | IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, IsolateReload, ServiceExtensionAdded
|
||||
Isolate | IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, IsolateReload, IsolateSpawn, ServiceExtensionAdded
|
||||
Debug | PauseStart, PauseExit, PauseBreakpoint, PauseInterrupted, PauseException, PausePostRequest, Resume, BreakpointAdded, BreakpointResolved, BreakpointRemoved, Inspect, None
|
||||
GC | GC
|
||||
Extension | Extension
|
||||
|
@ -1716,6 +1724,9 @@ class ClassList extends Response {
|
|||
|
||||
### ClientName
|
||||
|
||||
_**Note**: This class is deprecated and will be removed in v4.0 of the protocol.
|
||||
An equivalent can be found in the Dart Development Service (DDS) protocol._
|
||||
|
||||
```
|
||||
class ClientName extends Response {
|
||||
// The name of the currently connected VM service client.
|
||||
|
@ -3756,5 +3767,6 @@ version | comments
|
|||
3.31 | Added single client mode, which allows for the Dart Development Service (DDS) to become the sole client of
|
||||
the VM service.
|
||||
3.32 | Added `getClassList` RPC and `ClassList` object.
|
||||
3.33 | Added deprecation notice for `getClientName`, `setClientName`, `requireResumeApproval`, and `ClientName`. These RPCs are moving to the DDS protocol and will be removed in v4.0 of the VM service protocol.
|
||||
|
||||
[discuss-list]: https://groups.google.com/a/dartlang.org/forum/#!forum/observatory-discuss
|
||||
|
|
Loading…
Reference in a new issue