Skip to content

Commit 5536951

Browse files
bkonyiCommit Queue
authored andcommitted
[ DDS ] Rework client resume permissions API
This change reworks the client resume permissions API to make it easier for clients to deal with user provided `--pause-isolates-on-start` and `--pause-isolates-on-exit` flags. `requireUserPermissionToResume` should be called by the tool that launches the Dart process to indicate whether or not the user provided `--pause-isolates-on-{start,exit}`. The default behavior is to assume that a tool set these flags for its own use (e.g., resetting breakpoints after a hot restart in Flutter), where isolates will resume immediately after each client that has indicated interest in that pause event has invoked `readyToResume`. If a user provided one of the previously mentioned flags, isolates will not immediately resume after each relevant client has invoked `readyToResume`. Instead, a call to `resume()` must be made to indicate the user has triggered the resume request instead of tooling. If the user permissions to resume are changed while the isolate is paused and all relevant clients have invoked `readyToResume`, the isolate will automatically resume if the user no longer requires us to wait for a user resume. `resume()` now also acts as a "force resume", bypassing any required permissions set by tooling. This behavior change is breaking, so the DDS protocol version is being bumped to 2.0. `package:dds_service_extensions` has also been updated to include the following DDS RPCs: - `requireUserPermissionToResume` - `readyToResume` Change-Id: Id5f0806b3c56507d39eb00b6305b8896bab13ae7 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/357420 Reviewed-by: Elliott Brooks <[email protected]> Commit-Queue: Ben Konyi <[email protected]>
1 parent 6eb8594 commit 5536951

22 files changed

+527
-32
lines changed

pkg/dds/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 4.0.0
2+
- Updated DDS protocol to version 2.0.
3+
- Added `readyToResume` and `requireUserPermissionToResume` RPCs.
4+
- **Breaking change:** `resume` is now treated as a user-initiated resume request and force resumes paused isolates, regardless of required resume approvals. Tooling relying on resume permissions should use the `readyToResume` RPC to indicate to DDS that they are ready to resume.
5+
16
# 3.4.0
27
- Start the Dart Tooling Daemon from the DevTools server when a connection is not passed to the server on start.
38

pkg/dds/dds_protocol.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Dart Development Service Protocol 1.5
1+
# Dart Development Service Protocol 2.0
22

3-
This document describes _version 1.5_ of the Dart Development Service Protocol.
3+
This document describes _version 2.0_ of the Dart Development Service Protocol.
44
This protocol is an extension of the Dart VM Service Protocol and implements it
55
in it's entirety. For details on the VM Service Protocol, see the [Dart VM Service Protocol Specification][service-protocol].
66

@@ -172,6 +172,26 @@ void postEvent(String stream, String eventKind, Map eventData)
172172
```
173173
The _postEvent_ RPC is used to send events to custom Event streams.
174174

175+
### readyToResume
176+
177+
```
178+
Success readyToResume(string isolateId)
179+
```
180+
181+
The _readyToResume_ RPC indicates to DDS that the current client is ready
182+
to resume the isolate.
183+
184+
If the current client requires that approval be given before resuming an
185+
isolate, this method will:
186+
187+
- Update the approval state for the isolate.
188+
- Resume the isolate if approval has been given by all clients which
189+
require approval.
190+
191+
Returns a collected sentinel if the isolate no longer exists.
192+
193+
See [Success](#success).
194+
175195
### requirePermissionToResume
176196

177197
```
@@ -184,6 +204,8 @@ The _requirePermissionToResume_ RPC is used to change the pause/resume behavior
184204
of isolates by providing a way for the VM service to wait for approval to resume
185205
from some set of clients. This is useful for clients which want to perform some
186206
operation on an isolate after a pause without it being resumed by another client.
207+
These clients should invoke [readyToResume](#readyToResume) instead of [resume](resume)
208+
to indicate to DDS that they have finished their work and the isolate can be resumed.
187209

188210
If the _onPauseStart_ parameter is `true`, isolates will not resume after pausing
189211
on start until the client sends a `resume` request and all other clients which
@@ -206,6 +228,30 @@ need to provide resume approval for this pause type have done so.
206228
already given approval. In the case that no other client requires resume
207229
approval for the current pause event, the isolate will be resumed if at
208230
least one other client has attempted to [resume](resume) the isolate.
231+
- Resume permission behavior can be bypassed using the [VmService.resume]
232+
RPC, which is treated as a user-initiated resume that force resumes
233+
the isolate. Tooling relying on resume permissions should use
234+
[readyToResume](#readyToResume) instead of [resume](resume) to avoid force
235+
resuming the isolate.
236+
237+
See [Success](#success).
238+
239+
### requireUserPermissionToResume
240+
241+
```
242+
Success requireUserPermissionToResume(bool onPauseStart [optional],
243+
bool onPauseExit [optional])
244+
```
245+
246+
The _requireUserPermissionToResume_ RPC notifies DDS if it should wait
247+
for a [resume](resume) request to resume isolates paused on start or
248+
exit.
249+
250+
This RPC should only be invoked by tooling which launched the target Dart
251+
process and knows if the user indicated they wanted isolates paused on
252+
start or exit.
253+
254+
See [Success](#success).
209255

210256
### setClientName
211257

pkg/dds/lib/dds.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ abstract class DartDevelopmentService {
166166

167167
/// The version of the DDS protocol supported by this [DartDevelopmentService]
168168
/// instance.
169-
static const String protocolVersion = '1.6';
169+
static const String protocolVersion = '2.0';
170170
}
171171

172172
class DartDevelopmentServiceException implements Exception {

pkg/dds/lib/src/client.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,19 @@ class DartDevelopmentServiceClient {
179179
(parameters) => dds.isolateManager.resumeIsolate(this, parameters),
180180
);
181181

182+
_clientPeer.registerMethod(
183+
'readyToResume',
184+
(parameters) => dds.isolateManager.readyToResume(this, parameters),
185+
);
186+
187+
_clientPeer.registerMethod(
188+
'requireUserPermissionToResume',
189+
(parameters) => dds.isolateManager.requireUserPermissionToResume(
190+
this,
191+
parameters,
192+
),
193+
);
194+
182195
_clientPeer.registerMethod('getStreamHistory', (parameters) {
183196
final stream = parameters['stream'].asString;
184197
final events = dds.streamManager.getStreamHistory(stream);

pkg/dds/lib/src/client_manager.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ class ClientManager {
8181
}
8282

8383
/// Require permission from this client before resuming an isolate.
84-
Map<String, dynamic> requirePermissionToResume(
84+
Future<Map<String, dynamic>> requirePermissionToResume(
8585
DartDevelopmentServiceClient client,
8686
json_rpc.Parameters parameters,
87-
) {
87+
) async {
8888
int pauseTypeMask = 0;
8989
if (parameters['onPauseStart'].asBoolOr(false)) {
9090
pauseTypeMask |= PauseTypeMasks.pauseOnStartMask;
@@ -95,8 +95,12 @@ class ClientManager {
9595
if (parameters['onPauseExit'].asBoolOr(false)) {
9696
pauseTypeMask |= PauseTypeMasks.pauseOnExitMask;
9797
}
98-
9998
clientResumePermissions[client.name!]!.permissionsMask = pauseTypeMask;
99+
100+
// Check to see if any isolates should resume as a result of the
101+
// resume permissions being updated.
102+
await dds.isolateManager.maybeResumeIsolates();
103+
100104
return RPCResponses.success;
101105
}
102106

pkg/dds/lib/src/isolate_manager.dart

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ class RunningIsolate {
9090
// Mark approval by the client.
9191
_resumeApprovalsByName.add(resumingClient.name);
9292
}
93+
94+
// If the user is required to resume the isolate, we won't resume until a
95+
// `resume` request is received or `setRequireUserPermissionToResume` is
96+
// invoked and removes the requirement for this pause type.
97+
final userPermissionMask =
98+
isolateManager._requireUserPermissionToResumeMask;
99+
if (userPermissionMask & _isolateStateMask != 0) {
100+
return false;
101+
}
102+
93103
final requiredClientApprovals = <String>{};
94104
final permissions =
95105
isolateManager.dds.clientManager.clientResumePermissions;
@@ -293,15 +303,38 @@ class IsolateManager {
293303
isolates.remove(id);
294304
}
295305

296-
/// Handles `resume` RPC requests. If the client requires that approval be
297-
/// given before resuming an isolate, this method will:
306+
/// Handles `resume` RPC requests.
307+
///
308+
/// Invocations of `resume` are treated as user initiated and will bypass any
309+
/// resume permissions set by tooling, force resuming the isolate and clear
310+
/// any resume approvals.
311+
///
312+
/// Returns a collected sentinel if the isolate no longer exists.
313+
Future<Map<String, dynamic>> resumeIsolate(
314+
DartDevelopmentServiceClient client,
315+
json_rpc.Parameters parameters,
316+
) async {
317+
return await _mutex.runGuarded(
318+
() async {
319+
final isolateId = parameters['isolateId'].asString;
320+
final isolate = isolates[isolateId];
321+
if (isolate == null) {
322+
return RPCResponses.collectedSentinel;
323+
}
324+
return await _resumeCommon(isolate, parameters);
325+
},
326+
);
327+
}
328+
329+
/// Handles `readyToResume` RPC requests. If the client requires
330+
/// that approval be given before resuming an isolate, this method will:
298331
///
299332
/// - Update the approval state for the isolate.
300333
/// - Resume the isolate if approval has been given by all clients which
301334
/// require approval.
302335
///
303336
/// Returns a collected sentinel if the isolate no longer exists.
304-
Future<Map<String, dynamic>> resumeIsolate(
337+
Future<Map<String, dynamic>> readyToResume(
305338
DartDevelopmentServiceClient client,
306339
json_rpc.Parameters parameters,
307340
) async {
@@ -313,14 +346,75 @@ class IsolateManager {
313346
return RPCResponses.collectedSentinel;
314347
}
315348
if (isolate.shouldResume(resumingClient: client)) {
316-
isolate.clearResumeApprovals();
317-
return await _sendResumeRequest(isolateId, parameters);
349+
return await _resumeCommon(isolate, parameters);
318350
}
319351
return RPCResponses.success;
320352
},
321353
);
322354
}
323355

356+
Future<void> maybeResumeIsolates() async {
357+
await _mutex.runGuarded(() async {
358+
for (final isolate in isolates.values) {
359+
if (isolate.shouldResume()) {
360+
await _resumeCommon(isolate, json_rpc.Parameters('', {}));
361+
}
362+
}
363+
});
364+
}
365+
366+
Future<Map<String, dynamic>> _resumeCommon(
367+
RunningIsolate isolate,
368+
json_rpc.Parameters parameters,
369+
) async {
370+
isolate.clearResumeApprovals();
371+
return await _sendResumeRequest(isolate.id, parameters);
372+
}
373+
374+
/// Handles `requireUserPermissionToResume` requests.
375+
///
376+
/// Notifies DDS if it should wait for a `resume` request to resume isolates
377+
/// paused on start or exit.
378+
///
379+
/// This RPC should only be invoked by tooling which launched the target Dart
380+
/// process and knows if the user indicated they wanted isolates paused on
381+
/// start or exit.
382+
Future<Map<String, dynamic>> requireUserPermissionToResume(
383+
DartDevelopmentServiceClient client,
384+
json_rpc.Parameters parameters,
385+
) async {
386+
int pauseTypeMask = 0;
387+
if (parameters['onPauseStart'].asBoolOr(false)) {
388+
pauseTypeMask |= PauseTypeMasks.pauseOnStartMask;
389+
}
390+
if (parameters['onPauseExit'].asBoolOr(false)) {
391+
pauseTypeMask |= PauseTypeMasks.pauseOnExitMask;
392+
}
393+
_requireUserPermissionToResumeMask = pauseTypeMask;
394+
395+
// Check if isolates have been waiting for a user resume and resume any
396+
// isolates that no longer need to wait for a user resume.
397+
await maybeResumeIsolates();
398+
399+
return RPCResponses.success;
400+
}
401+
402+
/// Handles `getRequireUserPermissionToResume` requests.
403+
///
404+
/// Returns an object indicating whether or not a `resume` request is
405+
/// required for DDS to resume an isolate paused on start or exit.
406+
Future<Map<String, dynamic>> getRequireUserPermissionToResume(
407+
DartDevelopmentServiceClient client,
408+
json_rpc.Parameters parameters) async {
409+
bool flagFromMask(int mask) =>
410+
_requireUserPermissionToResumeMask & mask != 0;
411+
return <String, dynamic>{
412+
'type': 'ResumePermissionsRequired',
413+
'onPauseStart': flagFromMask(PauseTypeMasks.pauseOnStartMask),
414+
'onPauseExit': flagFromMask(PauseTypeMasks.pauseOnExitMask),
415+
};
416+
}
417+
324418
Future<Map<String, dynamic>> getCachedCpuSamples(
325419
json_rpc.Parameters parameters) async {
326420
final isolateId = parameters['isolateId'].asString;
@@ -381,5 +475,6 @@ class IsolateManager {
381475
bool _initialized = false;
382476
final DartDevelopmentServiceImpl dds;
383477
final _mutex = Mutex();
384-
final Map<String, RunningIsolate> isolates = {};
478+
int _requireUserPermissionToResumeMask = 0;
479+
final isolates = <String, RunningIsolate>{};
385480
}

pkg/dds/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: dds
2-
version: 3.4.0
2+
version: 4.0.0
33
description: >-
44
A library used to spawn the Dart Developer Service, used to communicate with
55
a Dart VM Service instance.

pkg/dds/test/client_resume_approvals_approve_then_disconnect_test.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'package:dds_service_extensions/dds_service_extensions.dart';
56
import 'package:vm_service/vm_service.dart';
67

78
import 'client_resume_approvals_common.dart';
@@ -34,7 +35,7 @@ final test = <IsolateTest>[
3435

3536
// Give resume approval for client1 to ensure approval state is cleaned up
3637
// properly when both client1 and client2 have disconnected.
37-
await client1.resume(isolateId);
38+
await client1.readyToResume(isolateId);
3839
await hasPausedAtStart(service, isolate);
3940

4041
// Once client1 is disconnected, we should still be paused.
@@ -47,7 +48,7 @@ final test = <IsolateTest>[
4748
client2.dispose();
4849
await hasPausedAtStart(service, isolate);
4950

50-
await service.resume(isolateId);
51+
await service.readyToResume(isolateId);
5152
},
5253
hasStoppedAtExit,
5354
];

pkg/dds/test/client_resume_approvals_disconnect_test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'package:dds_service_extensions/dds_service_extensions.dart';
56
import 'package:vm_service/vm_service.dart';
67

78
import 'client_resume_approvals_common.dart';
@@ -34,7 +35,7 @@ final test = <IsolateTest>[
3435

3536
// Send a resume request on the test client so we'll resume once the other
3637
// clients which require approval disconnect.
37-
await service.resume(isolateId);
38+
await service.readyToResume(isolateId);
3839
await hasPausedAtStart(service, isolate);
3940

4041
// Once client1 is disconnected, we should still be paused.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:dds_service_extensions/dds_service_extensions.dart';
6+
import 'package:vm_service/vm_service.dart';
7+
8+
import 'client_resume_approvals_common.dart';
9+
import 'common/service_test_common.dart';
10+
import 'common/test_helper.dart';
11+
12+
const String clientName = 'TestClient';
13+
const String otherClientName = 'OtherTestClient';
14+
15+
void fooBar() {
16+
int i = 0;
17+
print(i);
18+
}
19+
20+
final test = <IsolateTest>[
21+
// Multiple clients, different client names.
22+
(VmService service, IsolateRef isolateRef) async {
23+
final isolateId = isolateRef.id!;
24+
final client1 = await createClient(
25+
service: service,
26+
clientName: clientName,
27+
onPauseStart: true,
28+
);
29+
// ignore: unused_local_variable
30+
final client2 = await createClient(
31+
service: service,
32+
clientName: otherClientName,
33+
onPauseStart: true,
34+
);
35+
36+
await hasPausedAtStart(service, isolateRef);
37+
await client1.requireUserPermissionToResume(
38+
onPauseStart: true,
39+
);
40+
41+
// Invocations of `resume` are considered to be resume requests made by the
42+
// user and is treated as a force resume, ignoring any resume permissions
43+
// set by clients.
44+
await client1.resume(isolateId);
45+
await hasStoppedAtExit(service, isolateRef);
46+
},
47+
];
48+
49+
void main([args = const <String>[]]) => runIsolateTests(
50+
args,
51+
test,
52+
'client_resume_approvals_force_resume.dart',
53+
testeeConcurrent: fooBar,
54+
pauseOnStart: true,
55+
pauseOnExit: true,
56+
);

0 commit comments

Comments
 (0)