Skip to content

Commit a7d8707

Browse files
bkonyiCommit Queue
authored and
Commit Queue
committed
[ package:vm_service ] Automatically invoke VmService.dispose() when the service connection closes
Fixes #55559 Change-Id: I213ae3960c15bf2a68b4113a26f333090266b9c9 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/365060 Reviewed-by: Derek Xu <[email protected]> Commit-Queue: Ben Konyi <[email protected]>
1 parent 73f5417 commit a7d8707

File tree

7 files changed

+164
-21
lines changed

7 files changed

+164
-21
lines changed

pkg/vm_service/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 14.2.2
2+
- Fixes issue where outstanding service requests were not automatically completed
3+
with an error when the VM service connection was closed.
4+
15
## 14.2.1
26
- Fixes heap snapshot decoding error (dart-lang/sdk#55475).
37

pkg/vm_service/lib/src/vm_service.dart

+13-10
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,8 @@ class VmService {
300300
Future<void> get onDone => _onDoneCompleter.future;
301301
final _onDoneCompleter = Completer<void>();
302302

303+
bool _disposed = false;
304+
303305
final _eventControllers = <String, StreamController<Event>>{};
304306

305307
StreamController<Event> _getEventController(String eventName) {
@@ -321,16 +323,14 @@ class VmService {
321323
Future? streamClosed,
322324
this.wsUri,
323325
}) {
324-
_streamSub = inStream.listen(_processMessage,
325-
onDone: () => _onDoneCompleter.complete());
326+
_streamSub = inStream.listen(
327+
_processMessage,
328+
onDone: () async => await dispose(),
329+
);
326330
_writeMessage = writeMessage;
327331
_log = log ?? _NullLog();
328332
_disposeHandler = disposeHandler;
329-
streamClosed?.then((_) {
330-
if (!_onDoneCompleter.isCompleted) {
331-
_onDoneCompleter.complete();
332-
}
333-
});
333+
streamClosed?.then((_) async => await dispose());
334334
}
335335

336336
static VmService defaultFactory({
@@ -1735,6 +1735,10 @@ class VmService {
17351735
}
17361736

17371737
Future<void> dispose() async {
1738+
if (_disposed) {
1739+
return;
1740+
}
1741+
_disposed = true;
17381742
await _streamSub.cancel();
17391743
_outstandingRequests.forEach((id, request) {
17401744
request._completer.completeError(RPCError(
@@ -1748,9 +1752,8 @@ class VmService {
17481752
if (handler != null) {
17491753
await handler();
17501754
}
1751-
if (!_onDoneCompleter.isCompleted) {
1752-
_onDoneCompleter.complete();
1753-
}
1755+
assert(!_onDoneCompleter.isCompleted);
1756+
_onDoneCompleter.complete();
17541757
}
17551758

17561759
/// When overridden, this method wraps [future] with logic.

pkg/vm_service/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: vm_service
2-
version: 14.2.1
2+
version: 14.2.2
33
description: >-
44
A library to communicate with a service implementing the Dart VM
55
service protocol.

pkg/vm_service/test/common/utils.dart

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 'dart:convert';
6+
import 'dart:io';
7+
8+
// TODO(bkonyi): Share this logic with _ServiceTesteeRunner.launch.
9+
Future<(Process, Uri)> spawnDartProcess(
10+
String script, {
11+
bool serveObservatory = true,
12+
bool pauseOnStart = true,
13+
bool disableServiceAuthCodes = false,
14+
bool subscribeToStdio = true,
15+
}) async {
16+
final executable = Platform.executable;
17+
final tmpDir = await Directory.systemTemp.createTemp('dart_service');
18+
final serviceInfoUri = tmpDir.uri.resolve('service_info.json');
19+
final serviceInfoFile = await File.fromUri(serviceInfoUri).create();
20+
21+
final arguments = [
22+
'--disable-dart-dev',
23+
'--observe=0',
24+
if (!serveObservatory) '--no-serve-observatory',
25+
if (pauseOnStart) '--pause-isolates-on-start',
26+
if (disableServiceAuthCodes) '--disable-service-auth-codes',
27+
'--write-service-info=$serviceInfoUri',
28+
...Platform.executableArguments,
29+
Platform.script.resolve(script).toString(),
30+
];
31+
final process = await Process.start(executable, arguments);
32+
if (subscribeToStdio) {
33+
process.stdout
34+
.transform(utf8.decoder)
35+
.listen((line) => print('TESTEE OUT: $line'));
36+
process.stderr
37+
.transform(utf8.decoder)
38+
.listen((line) => print('TESTEE ERR: $line'));
39+
}
40+
while ((await serviceInfoFile.length()) <= 5) {
41+
await Future.delayed(const Duration(milliseconds: 50));
42+
}
43+
final content = await serviceInfoFile.readAsString();
44+
final infoJson = json.decode(content);
45+
return (process, Uri.parse(infoJson['uri']));
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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 'dart:io';
6+
7+
void main() {
8+
// Block the thread so the isolate can't response to service requests.
9+
sleep(const Duration(hours: 1));
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
// Regression test for https://github.com/dart-lang/sdk/issues/55559.
6+
//
7+
// Ensures that the `VmService` instance calls `dispose()` automatically if the
8+
// VM service connection goes down. Without the `dispose()` call, outstanding
9+
// requests won't complete unless the developer registered a callback for
10+
// `VmService.onDone` that calls `dispose()`.
11+
12+
import 'dart:async';
13+
import 'dart:io';
14+
15+
import 'package:test/test.dart';
16+
import 'package:vm_service/vm_service.dart';
17+
import 'package:vm_service/vm_service_io.dart';
18+
19+
import 'common/utils.dart';
20+
21+
void main() {
22+
(Process, Uri)? state;
23+
24+
void killProcess() {
25+
if (state != null) {
26+
final (process, _) = state!;
27+
process.kill();
28+
state = null;
29+
}
30+
}
31+
32+
setUp(() async {
33+
state = await spawnDartProcess(
34+
'regress_55559_script.dart',
35+
pauseOnStart: false,
36+
);
37+
});
38+
39+
tearDown(() {
40+
killProcess();
41+
});
42+
43+
test(
44+
'Regress 55559: VmService closes outstanding requests on service disconnect',
45+
() async {
46+
final (_, uri) = state!;
47+
final wsUri = uri.replace(
48+
scheme: 'ws',
49+
pathSegments: [
50+
// The path will have a trailing '/', so the last path segment is the
51+
// empty string and should be removed.
52+
...[...uri.pathSegments]..removeLast(),
53+
'ws',
54+
],
55+
);
56+
final service = await vmServiceConnectUri(wsUri.toString());
57+
final vm = await service.getVM();
58+
final isolate = vm.isolates!.first;
59+
final errorCompleter = Completer<RPCError>();
60+
unawaited(
61+
service.getIsolate(isolate.id!).then(
62+
(_) => fail('Future should throw'),
63+
onError: (e) => errorCompleter.complete(e),
64+
),
65+
);
66+
killProcess();
67+
68+
// Wait for the process to exit and the service connection to close.
69+
await service.onDone;
70+
71+
// The outstanding getIsolate request should be completed with an error.
72+
final error = await errorCompleter.future;
73+
expect(error.code, RPCErrorKind.kServerError.code);
74+
expect(error.message, 'Service connection disposed');
75+
},
76+
);
77+
}

pkg/vm_service/tool/dart/generate_dart_client.dart

+13-10
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ export 'snapshot_graph.dart' show HeapSnapshotClass,
6565
}
6666
6767
Future<void> dispose() async {
68+
if (_disposed) {
69+
return;
70+
}
71+
_disposed = true;
6872
await _streamSub.cancel();
6973
_outstandingRequests.forEach((id, request) {
7074
request._completer.completeError(RPCError(
@@ -78,9 +82,8 @@ export 'snapshot_graph.dart' show HeapSnapshotClass,
7882
if (handler != null) {
7983
await handler();
8084
}
81-
if (!_onDoneCompleter.isCompleted) {
82-
_onDoneCompleter.complete();
83-
}
85+
assert(!_onDoneCompleter.isCompleted);
86+
_onDoneCompleter.complete();
8487
}
8588
8689
/// When overridden, this method wraps [future] with logic.
@@ -581,6 +584,8 @@ typedef VmServiceFactory<T extends VmService> = T Function({
581584
Future<void> get onDone => _onDoneCompleter.future;
582585
final _onDoneCompleter = Completer<void>();
583586
587+
bool _disposed = false;
588+
584589
final _eventControllers = <String, StreamController<Event>>{};
585590
586591
StreamController<Event> _getEventController(String eventName) {
@@ -602,16 +607,14 @@ typedef VmServiceFactory<T extends VmService> = T Function({
602607
Future? streamClosed,
603608
this.wsUri,
604609
}) {
605-
_streamSub = inStream.listen(_processMessage,
606-
onDone: () => _onDoneCompleter.complete());
610+
_streamSub = inStream.listen(
611+
_processMessage,
612+
onDone: () async => await dispose(),
613+
);
607614
_writeMessage = writeMessage;
608615
_log = log ?? _NullLog();
609616
_disposeHandler = disposeHandler;
610-
streamClosed?.then((_) {
611-
if (!_onDoneCompleter.isCompleted) {
612-
_onDoneCompleter.complete();
613-
}
614-
});
617+
streamClosed?.then((_) async => await dispose());
615618
}
616619
617620
static VmService defaultFactory({

0 commit comments

Comments
 (0)