1
0
mirror of https://github.com/dart-lang/sdk synced 2024-07-03 08:19:13 +00:00

Split package:vm_service into package:vm_service and package:vm_service_interface

To reduce the headache associated with adding parameters to VM service
APIs, VmServiceInterface has been removed from package:vm_service and
pulled into its own dedicated package:vm_service_interface. This will
help reduce the need for major version bumps of package:vm_service,
which requires manual version bumps through >8 packages in order to make
the latest version available to flutter_tools and DevTools.

This separation of the VmService client from the interface will reduce
the frequency of major version bumps to `package:vm_service` as adding
optional parameters to existing APIs would cause implementers of the
interface to break.

package:vm_service continues to expose a copy of the contents of package:vm_service_interface to avoid breaking google3 rolls until package:dwds can migrate to package:vm_service_interface. package:vm_service will not be published until this copy is removed.

This change also includes:
 - some code cleanup and modernization to both the code generator and
   generated code
 - >=3.0.0 SDK version requirement to allow for new language features

Change-Id: Ib1859c1b4e153fef7ee1f91e67e881bbf42652c2
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/330500
Reviewed-by: Derek Xu <derekx@google.com>
Reviewed-by: Devon Carew <devoncarew@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
This commit is contained in:
Ben Konyi 2023-10-23 17:00:10 +00:00 committed by Commit Queue
parent d37620ed03
commit 08b4f49249
40 changed files with 5253 additions and 2254 deletions

View File

@ -1,3 +1,7 @@
# 3.0.0
- **Breaking change:** update `vm_service` version to ^13.0.0.
- **Breaking chnage:** change type of `DartDebugAdapter.vmService` from `VmServiceInterface` to `VmService`.
# 2.11.1
- [DAP] `restartFrameRequest` is now supported for frames up until the first async boundary (that are not also the top frame).
- Update `vm_service` version to >=11.0.0 <13.0.0.

View File

@ -349,7 +349,7 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
///
/// `null` if the session is running in noDebug mode of the connection has not
/// yet been made.
vm.VmServiceInterface? vmService;
vm.VmService? vmService;
/// The root of the Dart SDK containing the VM running the debug adapter.
late final String dartSdkRoot;

View File

@ -316,7 +316,7 @@ class ProtocolConverter {
/// Creates a Variable for a getter after eagerly fetching its value.
Future<Variable> createVariableForGetter(
vm.VmServiceInterface service,
vm.VmService service,
ThreadInfo thread,
vm.Instance instance, {
String? variableName,

View File

@ -1,5 +1,5 @@
name: dds
version: 2.11.1
version: 3.0.0
description: >-
A library used to spawn the Dart Developer Service, used to communicate with
a Dart VM Service instance.
@ -28,7 +28,7 @@ dependencies:
sse: ^4.0.0
stack_trace: ^1.10.0
stream_channel: ^2.0.0
vm_service: '>=11.7.2 <13.0.0'
vm_service: ^13.0.0
web_socket_channel: ^2.0.0
# We use 'any' version constraints here as we get our package versions from

View File

@ -148,7 +148,7 @@ class MockRequest extends dap.Request {
});
}
class MockVmService implements VmServiceInterface {
class MockVmService implements VmService {
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);

View File

@ -1,5 +1,8 @@
# 1.6.2
- Updated `vm_service` version to `^13.0.0`.
# 1.6.1
- Updated `vm_service` version to `^12.0.0`
- Updated `vm_service` version to `^12.0.0`.
## 1.6.0
- Made DAP extensions methods accessible in lib.

View File

@ -1,5 +1,5 @@
name: dds_service_extensions
version: 1.6.1
version: 1.6.2
description: >-
Extension methods for `package:vm_service`, used to make requests a
Dart Development Service (DDS) instance.
@ -11,7 +11,7 @@ environment:
dependencies:
async: ^2.4.1
dap: ^1.0.0
vm_service: ^12.0.0
vm_service: ^13.0.0
# We use 'any' version constraints here as we get our package versions from
# the dart-lang/sdk repo's DEPS file. Note that this is a special case; the

View File

@ -0,0 +1,34 @@
# Contributing to `package:vm_service` and `package:vm_service_interface`
## Updating the VM service version
To update `package:vm_service` and `package:vm_service_interface` to support the latest version of the [VM service protocol](https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md), run the following script to regenerate the client and interface:
`dart tool/generate.dart`
## Updating the code generator
Both `package:vm_service` and `package:vm_service_interface` make use of code generation to generate much of their implementations. As a result, manual changes made to some files (e.g., `package:vm_service/src/vm_service.dart` and `package:vm_service_interface/src/vm_service_interface.dart`) will be overwritten by the code generator.
To make changes to the generated files, make changes in one or more of the following files:
- `tool/dart/generate_dart_client.dart` for code specific to `package:vm_service`
- `tool/dart/generate_dart_interface.dart` for code specific to `package:vm_service_interface`
- `tool/dart/generate_dart_common.dart` for code common to `package:vm_service` and `package:vm_service_interface`
## Running tests locally
### 1. Build the SDK
From the root of the Dart SDK, run the following commands:
gclient sync -D && \
python3 tools/build.py -ax64 create_sdk
Note: for a release build, add the `-mrelease` flag: `./tools/build.py -mrelease -ax64 create_sdk`
### 2. Run the tests
To run all the tests: `python3 tools/test.py [ -mdebug | -mrelease ] -ax64 -j4 pkg/vm_service`
To run a single test: `dart pkg/vm_service/test/<test_name>.dart`

View File

@ -9,7 +9,7 @@ See the
[example](https://github.com/dart-lang/sdk/blob/main/pkg/vm_service/example/vm_service_tester.dart)
for a simple use of the library's API.
The VM Service Protocol spec can be found at
The VM Service Protocol specification can be found at
[github.com/dart-lang/sdk/runtime/vm/service/service.md](https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md).
## Features and bugs
@ -17,18 +17,3 @@ The VM Service Protocol spec can be found at
Please file feature requests and bugs at the [issue tracker][tracker].
[tracker]: https://github.com/dart-lang/sdk/issues
## Running tests locally
1. Build the SDK
```
gclient sync -D && \
./tools/build.py -ax64 create_sdk
```
Note: for a release build, add the `-mrelease` flag: `./tools/build.py -mrelease -ax64 create_sdk`
2. Run the tests
- To run all the tests: `python3 tools/test.py [ -mdebug | -mrelease ] -ax64 -j4 pkg/vm_service`
- To run a single test: `dart pkg/vm_service/test/<test_name>.dart`

View File

@ -7,7 +7,7 @@ analyzer:
linter:
rules:
# still 6 errors in lib/src/vm_service.dart
# still 5 errors in lib/src/vm_service.dart
#- comment_references
- directives_ordering
- prefer_single_quotes

View File

@ -4,8 +4,10 @@
import 'dart:async';
import '../vm_service.dart' show VmServerConnection, RPCError, Event, EventKind;
import 'package:vm_service/vm_service.dart' show RPCError, Event, EventKind;
import 'stream_helpers.dart';
import 'vm_service_interface.dart' show VmServerConnection;
/// A registry of custom service extensions to [VmServerConnection]s in which
/// they were registered.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,4 +5,7 @@
library vm_service;
export 'src/dart_io_extensions.dart';
export 'src/snapshot_graph.dart';
export 'src/vm_service.dart' hide addTypeFactory, extensionCallHelper;
// TODO(bkonyi): remove before publishing 13.0.0
export 'src/vm_service_interface.dart' hide vmServiceVersion;

View File

@ -1,5 +1,8 @@
name: vm_service
version: 13.0.0
# TODO(bkonyi): resolve outstanding TODOs required for 13.0.0 release before
# removing.
publish_to: none
description: >-
A library to communicate with a service implementing the Dart VM
service protocol.
@ -19,20 +22,18 @@ dev_dependencies:
expect: any
lints: any
markdown: any
mockito: any
path: any
pub_semver: any
test_package: any
test: any
vm_service_protos: any
# TODO(bkonyi): remove expect and smith overrides before
# publishing 13.0.0.
dependency_overrides:
coverage: 1.6.4
expect:
path: '../expect'
smith:
path: '../smith'
test: 1.24.7
test_core: 0.5.7
test_package:
path: 'test/test_package'

View File

@ -13,7 +13,7 @@ import 'common/test_helper.dart';
// Chop off the file name.
String baseDirectory = path.dirname(Platform.script.path) + '/';
Uri baseUri = Platform.script.replace(path: baseDirectory);
Uri breakpointFile = baseUri.resolve('test_package/the_part.dart');
Uri breakpointFile = baseUri.resolve('test_package/lib/the_part.dart');
const String shortFile = "the_part.dart";
const int LINE = 87;

View File

@ -46,7 +46,10 @@ Future<void> executeWithRandomDelay(Function f) =>
.then((_) async {
try {
await f();
} on HttpException catch (_) {} on SocketException catch (_) {} on StateError catch (_) {} on OSError catch (_) {}
} on HttpException catch (_) {
} on SocketException catch (_) {
} on StateError catch (_) {
} on OSError catch (_) {}
});
Uri randomlyAddRequestParams(Uri uri) {
@ -289,8 +292,8 @@ Future<void> hasValidHttpPUTs(HttpProfile profile) =>
hasValidHttpRequests(profile, 'PUT');
void hasDefaultRequestHeaders(HttpProfile profile) {
for(final request in profile.requests) {
if(!request.request!.hasError) {
for (final request in profile.requests) {
if (!request.request!.hasError) {
expect(request.request?.headers['host'], isNotNull);
expect(request.request?.headers['user-agent'], isNotNull);
}
@ -299,8 +302,8 @@ void hasDefaultRequestHeaders(HttpProfile profile) {
void hasCustomRequestHeaders(HttpProfile profile) {
var requests = profile.requests.where((e) => e.method == "GET").toList();
for(final request in requests) {
if(!request.request!.hasError) {
for (final request in requests) {
if (!request.request!.hasError) {
expect(request.request?.headers['cookie-eater'], isNotNull);
}
}

View File

@ -7,13 +7,11 @@
// ignore_for_file: experiment_not_enabled
import 'dart:developer';
import 'package:expect/expect.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'common/service_test_common.dart';
import 'common/test_helper.dart';
abstract class A {
int get x;
int get y;
@ -26,7 +24,7 @@ class B implements A {
}
foo(Object obj) {
switch(obj) {
switch (obj) {
case A(x: 4, y: 5):
print('A(4, 5)');
case A(x: var x1, y: var y1):
@ -44,7 +42,7 @@ var tests = <IsolateTest>[
(VmService service, IsolateRef isolateRef) async {
Stack stack = await service.getStack(isolateRef.id!);
final Set<String> vars = stack.frames![0].vars!.map((v) => v.name!).toSet();
Expect.setEquals(<String>{'obj', 'x1', 'y1'}, vars);
expect(vars, <String>{'obj', 'x1', 'y1'});
},
];

View File

@ -46,7 +46,10 @@ Future<void> executeWithRandomDelay(Function f) =>
.then((_) async {
try {
await f();
} on HttpException catch (_) {} on SocketException catch (_) {} on StateError catch (_) {} on OSError catch (_) {}
} on HttpException catch (_) {
} on SocketException catch (_) {
} on StateError catch (_) {
} on OSError catch (_) {}
});
Uri randomlyAddRequestParams(Uri uri) {

View File

@ -10,7 +10,7 @@ import 'package:pub_semver/pub_semver.dart';
import 'src_gen_common.dart';
/// [ApiParseUtil] contains top level parsing utilities.
mixin class ApiParseUtil {
mixin ApiParseUtil {
/// Extract the current VM Service version number as a String.
static String parseVersionString(List<Node> nodes) =>
parseVersionSemVer(nodes).toString();

View File

@ -0,0 +1,669 @@
// Copyright (c) 2023, 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 'generate_dart_common.dart';
class VmServiceApi extends Api {
static const _clientHeaderCode = r'''
// Copyright (c) 2015, 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.
// This is a generated file. To regenerate, run `dart tool/generate.dart`.
/// A library to access the VM Service API.
///
/// The main entry-point for this library is the [VmService] class.
library;
// ignore_for_file: overridden_fields
import 'dart:async';
import 'dart:convert' show base64, jsonDecode, jsonEncode, utf8;
import 'dart:typed_data';
export 'snapshot_graph.dart' show HeapSnapshotClass,
HeapSnapshotExternalProperty,
HeapSnapshotField,
HeapSnapshotGraph,
HeapSnapshotObject,
HeapSnapshotObjectLengthData,
HeapSnapshotObjectNoData,
HeapSnapshotObjectNullData;
''';
static const _implCode = r'''
/// Call an arbitrary service protocol method. This allows clients to call
/// methods not explicitly exposed by this library.
Future<Response> callMethod(String method, {
String? isolateId,
Map<String, dynamic>? args
}) {
return callServiceExtension(method, isolateId: isolateId, args: args);
}
/// Invoke a specific service protocol extension method.
///
/// See https://api.dart.dev/stable/dart-developer/dart-developer-library.html.
Future<Response> callServiceExtension(String method, {
String? isolateId,
Map<String, dynamic>? args
}) {
if (args == null && isolateId == null) {
return _call(method);
} else if (args == null) {
return _call(method, {'isolateId': isolateId!});
} else {
args = Map.from(args);
if (isolateId != null) {
args['isolateId'] = isolateId;
}
return _call(method, args);
}
}
Future<void> dispose() async {
await _streamSub.cancel();
_outstandingRequests.forEach((id, request) {
request._completer.completeError(RPCError(
request.method,
RPCErrorKind.kServerError.code,
'Service connection disposed',
));
});
_outstandingRequests.clear();
if (_disposeHandler != null) {
await _disposeHandler!();
}
if (!_onDoneCompleter.isCompleted) {
_onDoneCompleter.complete();
}
}
/// When overridden, this method wraps [future] with logic.
///
/// [wrapFuture] is called by [_call], which is the method that each VM
/// service endpoint eventually goes through.
///
/// This method should be overridden if subclasses of [VmService] need to do
/// anything special upon calling the VM service, like tracking futures or
/// logging requests.
Future<T> wrapFuture<T>(String name, Future<T> future) {
return future;
}
Future<T> _call<T>(String method, [Map args = const {}]) {
return wrapFuture<T>(
method,
() {
final request = _OutstandingRequest<T>(method);
_outstandingRequests[request.id] = request;
Map m = {
'jsonrpc': '2.0',
'id': request.id,
'method': method,
'params': args,
};
String message = jsonEncode(m);
_onSend.add(message);
_writeMessage(message);
return request.future;
}(),
);
}
/// Register a service for invocation.
void registerServiceCallback(String service, ServiceCallback cb) {
if (_services.containsKey(service)) {
throw Exception('Service \'$service\' already registered');
}
_services[service] = cb;
}
void _processMessage(dynamic message) {
// Expect a String, an int[], or a ByteData.
if (message is String) {
_processMessageStr(message);
} else if (message is List<int>) {
final list = Uint8List.fromList(message);
_processMessageByteData(ByteData.view(list.buffer));
} else if (message is ByteData) {
_processMessageByteData(message);
} else {
_log.warning('unknown message type: ${message.runtimeType}');
}
}
void _processMessageByteData(ByteData bytes) {
final int metaOffset = 4;
final int dataOffset = bytes.getUint32(0, Endian.little);
final metaLength = dataOffset - metaOffset;
final dataLength = bytes.lengthInBytes - dataOffset;
final meta = utf8.decode(Uint8List.view(
bytes.buffer, bytes.offsetInBytes + metaOffset, metaLength));
final data = ByteData.view(
bytes.buffer, bytes.offsetInBytes + dataOffset, dataLength);
final map = jsonDecode(meta)!;
if (map['method'] == 'streamNotify') {
final streamId = map['params']['streamId'];
final event = map['params']['event'];
event['data'] = data;
_getEventController(streamId)
.add(createServiceObject(event, const ['Event'])! as Event);
}
}
void _processMessageStr(String message) {
try {
_onReceive.add(message);
final json = jsonDecode(message)!;
if (json.containsKey('method')) {
if (json.containsKey('id')) {
_processRequest(json);
} else {
_processNotification(json);
}
} else if (json.containsKey('id') &&
(json.containsKey('result') || json.containsKey('error'))) {
_processResponse(json);
}
else {
_log.severe('unknown message type: $message');
}
} catch (e, s) {
_log.severe('unable to decode message: $message, $e\n$s');
return;
}
}
void _processResponse(Map<String, dynamic> json) {
final request = _outstandingRequests.remove(json['id']);
if (request == null) {
_log.severe('unmatched request response: ${jsonEncode(json)}');
} else if (json['error'] != null) {
request.completeError(RPCError.parse(request.method, json['error']));
} else {
final result = json['result'] as Map<String, dynamic>;
final type = result['type'];
if (type == 'Sentinel') {
request.completeError(SentinelException.parse(request.method, result));
} else if (_typeFactories[type] == null) {
request.complete(Response.parse(result));
} else {
final returnTypes = _methodReturnTypes[request.method] ?? <String>[];
request.complete(createServiceObject(result, returnTypes));
}
}
}
Future _processRequest(Map<String, dynamic> json) async {
final result = await _routeRequest(json['method'], json['params'] ?? <String, dynamic>{});
result['id'] = json['id'];
result['jsonrpc'] = '2.0';
String message = jsonEncode(result);
_onSend.add(message);
_writeMessage(message);
}
Future _processNotification(Map<String, dynamic> json) async {
final method = json['method'];
final params = json['params'] ?? <String, dynamic>{};
if (method == 'streamNotify') {
final streamId = params['streamId'];
_getEventController(streamId).add(createServiceObject(params['event'], const ['Event'])! as Event);
} else {
await _routeRequest(method, params);
}
}
Future<Map> _routeRequest(String method, Map<String, dynamic> params) async {
final service = _services[method];
if (service == null) {
final error = RPCError(method, RPCErrorKind.kMethodNotFound.code,
'method not found \'$method\'');
return {'error': error.toMap()};
}
try {
return await service(params);
} catch (e, st) {
RPCError error = RPCError.withDetails(
method,
RPCErrorKind.kServerError.code,
'$e',
details: '$st',
);
return {'error': error.toMap()};
}
}
''';
static const _rpcError = r'''
typedef DisposeHandler = Future Function();
// These error codes must be kept in sync with those in vm/json_stream.h and
// vmservice.dart.
enum RPCErrorKind {
/// Application specific error code.
kServerError(code: -32000, message: 'Application error'),
/// The JSON sent is not a valid Request object.
kInvalidRequest(code: -32600, message: 'Invalid request object'),
/// The method does not exist or is not available.
kMethodNotFound(code: -32601, message: 'Method not found'),
/// Invalid method parameter(s), such as a mismatched type.
kInvalidParams(code: -32602, message: 'Invalid method parameters'),
/// Internal JSON-RPC error.
kInternalError(code: -32603, message: 'Internal JSON-RPC error'),
/// The requested feature is disabled.
kFeatureDisabled(code: 100, message: 'Feature is disabled'),
/// The stream has already been subscribed to.
kStreamAlreadySubscribed(code: 103, message: 'Stream already subscribed'),
/// The stream has not been subscribed to.
kStreamNotSubscribed(code: 104, message: 'Stream not subscribed'),
/// Isolate must first be paused.
kIsolateMustBePaused(code: 106, message: 'Isolate must be paused'),
/// The service has already been registered.
kServiceAlreadyRegistered(code: 111, message: 'Service already registered'),
/// The service no longer exists.
kServiceDisappeared(code: 112, message: 'Service has disappeared'),
/// There was an error in the expression compiler.
kExpressionCompilationError(
code: 113, message: 'Expression compilation error'),
/// The custom stream does not exist.
kCustomStreamDoesNotExist(code: 130, message: 'Custom stream does not exist'),
/// The core stream is not allowed.
kCoreStreamNotAllowed(code: 131, message: 'Core streams are not allowed');
const RPCErrorKind({required this.code, required this.message});
final int code;
final String message;
static final _codeToErrorMap =
RPCErrorKind.values.fold(<int, RPCErrorKind>{}, (map, error) {
map[error.code] = error;
return map;
});
static RPCErrorKind? fromCode(int code) {
return _codeToErrorMap[code];
}
}
class RPCError implements Exception {
@Deprecated('Use RPCErrorKind.kServerError.code instead.')
static int get kServerError => RPCErrorKind.kServerError.code;
@Deprecated('Use RPCErrorKind.kInvalidRequest.code instead.')
static int get kInvalidRequest => RPCErrorKind.kInvalidRequest.code;
@Deprecated('Use RPCErrorKind.kMethodNotFound.code instead.')
static int get kMethodNotFound => RPCErrorKind.kMethodNotFound.code;
@Deprecated('Use RPCErrorKind.kInvalidParams.code instead.')
static int get kInvalidParams => RPCErrorKind.kInvalidParams.code;
@Deprecated('Use RPCErrorKind.kInternalError.code instead.')
static int get kInternalError => RPCErrorKind.kInternalError.code;
static RPCError parse(String callingMethod, dynamic json) {
return RPCError(callingMethod, json['code'], json['message'], json['data']);
}
final String? callingMethod;
final int code;
final String message;
final Map? data;
RPCError(this.callingMethod, this.code, [message, this.data])
: message =
message ?? RPCErrorKind.fromCode(code)?.message ?? 'Unknown error';
RPCError.withDetails(this.callingMethod, this.code, this.message,
{Object? details})
: data = details == null ? null : <String, dynamic>{} {
if (details != null) {
data!['details'] = details;
}
}
String? get details => data == null ? null : data!['details'];
/// Return a map representation of this error suitable for conversion to
/// json.
Map<String, dynamic> toMap() {
final map = <String, dynamic>{
'code': code,
'message': message,
};
if (data != null) {
map['data'] = data;
}
return map;
}
@override
String toString() {
if (details == null) {
return '$callingMethod: ($code) $message';
} else {
return '$callingMethod: ($code) $message\n$details';
}
}
}
/// Thrown when an RPC response is a [Sentinel].
class SentinelException implements Exception {
final String callingMethod;
final Sentinel sentinel;
SentinelException.parse(this.callingMethod, Map<String, dynamic> data) :
sentinel = Sentinel.parse(data)!;
@override
String toString() => '$sentinel from $callingMethod()';
}
/// An `ExtensionData` is an arbitrary map that can have any contents.
class ExtensionData {
static ExtensionData? parse(Map<String, dynamic>? json) =>
json == null ? null : ExtensionData._fromJson(json);
final Map<String, dynamic> data;
ExtensionData() : data = <String, dynamic>{};
ExtensionData._fromJson(this.data);
@override
String toString() => '[ExtensionData $data]';
}
/// A logging handler you can pass to a [VmService] instance in order to get
/// notifications of non-fatal service protocol warnings and errors.
abstract class Log {
/// Log a warning level message.
void warning(String message);
/// Log an error level message.
void severe(String message);
}
class _NullLog implements Log {
@override
void warning(String message) {}
@override
void severe(String message) {}
}
''';
@override
void generate(DartGenerator gen) {
gen.out(_clientHeaderCode);
gen.writeln("const String vmServiceVersion = '$serviceVersion';");
gen.writeln();
gen.writeln('''
/// @optional
const String optional = 'optional';
/// Decode a string in Base64 encoding into the equivalent non-encoded string.
/// This is useful for handling the results of the Stdout or Stderr events.
String decodeBase64(String str) => utf8.decode(base64.decode(str));
// Returns true if a response is the Dart `null` instance.
bool _isNullInstance(Map json) => ((json['type'] == '@Instance') &&
(json['kind'] == 'Null'));
Object? createServiceObject(dynamic json, List<String> expectedTypes) {
if (json == null) return null;
if (json is List) {
return json.map((e) => createServiceObject(e, expectedTypes)).toList();
} else if (json is Map<String, dynamic>) {
String? type = json['type'];
// Not a Response type.
if (type == null) {
// If there's only one expected type, we'll just use that type.
if (expectedTypes.length == 1) {
type = expectedTypes.first;
} else {
return Response.parse(json);
}
} else if (_isNullInstance(json) && (!expectedTypes.contains('InstanceRef'))) {
// Replace null instances with null when we don't expect an instance to
// be returned.
return null;
}
final typeFactory = _typeFactories[type];
if (typeFactory == null) {
return null;
} else {
return typeFactory(json);
}
} else {
// Handle simple types.
return json;
}
}
dynamic _createSpecificObject(
dynamic json, dynamic Function(Map<String, dynamic> map) creator) {
if (json == null) return null;
if (json is List) {
return json.map((e) => creator(e)).toList();
} else if (json is Map) {
return creator({
for (String key in json.keys)
key: json[key],
});
} else {
// Handle simple types.
return json;
}
}
void _setIfNotNull(Map<String, dynamic> json, String key, Object? value) {
if (value == null) return;
json[key] = value;
}
Future<T> extensionCallHelper<T>(VmService service, String method, Map<String, dynamic> args) {
return service._call(method, args);
}
typedef ServiceCallback = Future<Map<String, dynamic>> Function(
Map<String, dynamic> params);
void addTypeFactory(String name, Function factory) {
if (_typeFactories.containsKey(name)) {
throw StateError('Factory already registered for \$name');
}
_typeFactories[name] = factory;
}
''');
gen.writeln();
gen.writeln('final _typeFactories = <String, Function>{');
for (var type in types) {
gen.writeln("'${type!.rawName}': ${type.name}.parse,");
}
gen.writeln('};');
gen.writeln();
gen.writeln('final _methodReturnTypes = <String, List<String>>{');
for (var method in methods) {
String returnTypes = typeRefListToString(method.returnType.types);
gen.writeln("'${method.name}' : $returnTypes,");
}
gen.writeln('};');
gen.writeln();
gen.write('''
class _OutstandingRequest<T> {
_OutstandingRequest(this.method);
static int _idCounter = 0;
final id = '\${_idCounter++}';
final String method;
final _stackTrace = StackTrace.current;
final _completer = Completer<T>();
Future<T> get future => _completer.future;
void complete(T value) => _completer.complete(value);
void completeError(Object error) =>
_completer.completeError(error, _stackTrace);
}
''');
gen.writeln();
gen.writeln('''
typedef VmServiceFactory<T extends VmService> = T Function({
required Stream<dynamic> /*String|List<int>*/ inStream,
required void Function(String message) writeMessage,
Log? log,
DisposeHandler? disposeHandler,
Future? streamClosed,
String? wsUri,
});
''');
// The client side service implementation.
gen.writeStatement('class VmService {');
gen.writeStatement('late final StreamSubscription _streamSub;');
gen.writeStatement('late final Function _writeMessage;');
gen.writeStatement(
'final _outstandingRequests = <String, _OutstandingRequest>{};');
gen.writeStatement('final _services = <String, ServiceCallback>{};');
gen.writeStatement('late final Log _log;');
gen.write('''
/// The web socket URI pointing to the target VM service instance.
final String? wsUri;
Stream<String> get onSend => _onSend.stream;
final _onSend = StreamController<String>.broadcast(sync: true);
Stream<String> get onReceive => _onReceive.stream;
final _onReceive = StreamController<String>.broadcast(sync: true);
Future<void> get onDone => _onDoneCompleter.future;
final _onDoneCompleter = Completer<void>();
final _eventControllers = <String, StreamController<Event>>{};
StreamController<Event> _getEventController(String eventName) {
StreamController<Event>? controller = _eventControllers[eventName];
if (controller == null) {
controller = StreamController.broadcast();
_eventControllers[eventName] = controller;
}
return controller;
}
late final DisposeHandler? _disposeHandler;
VmService(
Stream<dynamic> /*String|List<int>*/ inStream,
void Function(String message) writeMessage, {
Log? log,
DisposeHandler? disposeHandler,
Future? streamClosed,
this.wsUri,
}) {
_streamSub = inStream.listen(_processMessage,
onDone: () => _onDoneCompleter.complete());
_writeMessage = writeMessage;
_log = log ?? _NullLog();
_disposeHandler = disposeHandler;
streamClosed?.then((_) {
if (!_onDoneCompleter.isCompleted) {
_onDoneCompleter.complete();
}
});
}
static VmService defaultFactory({
required Stream<dynamic> /*String|List<int>*/ inStream,
required void Function(String message) writeMessage,
Log? log,
DisposeHandler? disposeHandler,
Future? streamClosed,
String? wsUri,
}) {
return VmService(
inStream,
writeMessage,
log: log,
disposeHandler: disposeHandler,
streamClosed: streamClosed,
wsUri: wsUri,
);
}
Stream<Event> onEvent(String streamId) => _getEventController(streamId).stream;
''');
// streamCategories
for (var s in streamCategories) {
s.generate(gen);
}
gen.writeln();
for (var m in methods) {
m.generate(gen);
}
gen.out(_implCode);
gen.writeStatement('}');
gen.writeln();
gen.out(_rpcError);
gen.writeln();
gen.writeln('// enums');
for (var e in enums) {
if (e.name == 'EventKind') {
_generateEventStream(gen);
}
e.generate(gen);
}
gen.writeln();
gen.writeln('// types');
types.where((t) => !t!.skip).forEach((t) => t!.generate(gen));
}
void setDefaultValue(String typeName, String fieldName, String defaultValue) {
types
.firstWhere((t) => t!.name == typeName)!
.fields
.firstWhere((f) => f.name == fieldName)
.defaultValue = defaultValue;
}
void _generateEventStream(DartGenerator gen) {
gen.writeln();
gen.writeDocs('An enum of available event streams.');
gen.writeln('abstract class EventStreams {');
gen.writeln();
for (var c in streamCategories) {
gen.writeln("static const String k${c.name} = '${c.name}';");
}
gen.writeln('}');
}
}

View File

@ -0,0 +1,267 @@
// Copyright (c) 2023, 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 'generate_dart_common.dart';
class VmServiceInterfaceApi extends Api {
static const _interfaceHeaderCode = r'''
// Copyright (c) 2023, 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.
// This is a generated file. To regenerate, run `dart tool/generate.dart`.
/// A library providing an interface to implement the VM Service Protocol.
library;
// ignore_for_file: overridden_fields
import 'dart:async';
import 'package:vm_service/vm_service.dart' hide ServiceExtensionRegistry, VmServerConnection, VmServiceInterface;
import 'service_extension_registry.dart';
export 'service_extension_registry.dart' show ServiceExtensionRegistry;
''';
@override
void generate(DartGenerator gen) {
gen.out(_interfaceHeaderCode);
gen.writeln("const String vmServiceVersion = '$serviceVersion';");
gen.writeln();
gen.writeStatement('''
/// A class representation of the Dart VM Service Protocol.
abstract interface class VmServiceInterface {
/// Returns the stream for a given stream id.
///
/// This is not a part of the spec, but is needed for both the client and
/// server to get access to the real event streams.
Stream<Event> onEvent(String streamId);
/// Handler for calling extra service extensions.
Future<Response> callServiceExtension(String method, {String? isolateId, Map<String, dynamic>? args});
''');
for (var m in methods) {
m.generateDefinition(gen);
gen.write(';');
}
gen.write('}');
gen.writeln();
// The server class, takes a VmServiceInterface and delegates to it
// automatically.
gen.write('''
class _PendingServiceRequest {
Future<Map<String, Object?>> get future => _completer.future;
final _completer = Completer<Map<String, Object?>>();
final dynamic originalId;
_PendingServiceRequest(this.originalId);
void complete(Map<String, Object?> response) {
response['id'] = originalId;
_completer.complete(response);
}
}
/// A Dart VM Service Protocol connection that delegates requests to a
/// [VmServiceInterface] implementation.
///
/// One of these should be created for each client, but they should generally
/// share the same [VmServiceInterface] and [ServiceExtensionRegistry]
/// instances.
class VmServerConnection {
final Stream<Map<String, Object>> _requestStream;
final StreamSink<Map<String, Object?>> _responseSink;
final ServiceExtensionRegistry _serviceExtensionRegistry;
final VmServiceInterface _serviceImplementation;
/// Used to create unique ids when acting as a proxy between clients.
int _nextServiceRequestId = 0;
/// Manages streams for `streamListen` and `streamCancel` requests.
final _streamSubscriptions = <String, StreamSubscription>{};
/// Completes when [_requestStream] is done.
Future<void> get done => _doneCompleter.future;
final _doneCompleter = Completer<void>();
/// Pending service extension requests to this client by id.
final _pendingServiceExtensionRequests = <dynamic, _PendingServiceRequest>{};
VmServerConnection(this._requestStream, this._responseSink,
this._serviceExtensionRegistry, this._serviceImplementation) {
_requestStream.listen(_delegateRequest, onDone: _doneCompleter.complete);
done.then((_) {
for (var sub in _streamSubscriptions.values) {
sub.cancel();
}
});
}
/// Invoked when the current client has registered some extension, and
/// another client sends an RPC request for that extension.
///
/// We don't attempt to do any serialization or deserialization of the
/// request or response in this case
Future<Map<String, Object?>> _forwardServiceExtensionRequest(
Map<String, Object?> request) {
final originalId = request['id'];
request = Map<String, Object?>.of(request);
// Modify the request ID to ensure we don't have conflicts between
// multiple clients ids.
final newId = '\${_nextServiceRequestId++}:\$originalId';
request['id'] = newId;
var pendingRequest = _PendingServiceRequest(originalId);
_pendingServiceExtensionRequests[newId] = pendingRequest;
_responseSink.add(request);
return pendingRequest.future;
}
void _delegateRequest(Map<String, Object?> request) async {
try {
var id = request['id'];
// Check if this is actually a response to a pending request.
if (_pendingServiceExtensionRequests.containsKey(id)) {
final pending = _pendingServiceExtensionRequests[id]!;
pending.complete(Map<String, Object?>.of(request));
return;
}
final method = request['method'] as String?;
if (method == null) {
throw RPCError(null, RPCErrorKind.kInvalidRequest.code,
'Invalid Request', request);
}
final params = request['params'] as Map<String, dynamic>?;
late Response response;
switch(method) {
case 'registerService':
$registerServiceImpl
break;
''');
for (var m in methods) {
if (m.name != 'registerService') {
gen.writeln("case '${m.name}':");
if (m.name == 'streamListen') {
gen.writeln(streamListenCaseImpl);
} else if (m.name == 'streamCancel') {
gen.writeln(streamCancelCaseImpl);
} else {
bool firstParam = true;
String nullCheck() {
final result = firstParam ? '!' : '';
if (firstParam) {
firstParam = false;
}
return result;
}
if (m.deprecated) {
gen.writeln('// ignore: deprecated_member_use_from_same_package');
}
gen.write('response = await _serviceImplementation.${m.name}(');
// Positional args
m.args.where((arg) => !arg.optional).forEach((MethodArg arg) {
if (arg.type.isArray) {
gen.write(
"${arg.type.listCreationRef}.from(params${nullCheck()}['${arg.name}'] ?? []), ");
} else {
gen.write("params${nullCheck()}['${arg.name}'], ");
}
});
// Optional named args
var namedArgs = m.args.where((arg) => arg.optional);
if (namedArgs.isNotEmpty) {
for (var arg in namedArgs) {
if (arg.name == 'scope') {
gen.writeln(
"${arg.name}: params${nullCheck()}['${arg.name}']?.cast<String, String>(), ");
} else {
gen.writeln(
"${arg.name}: params${nullCheck()}['${arg.name}'], ");
}
}
}
gen.writeln(');');
}
gen.writeln('break;');
}
}
// Handle service extensions
gen.writeln('default:');
gen.writeln('''
final registeredClient = _serviceExtensionRegistry.clientFor(method);
if (registeredClient != null) {
// Check for any client which has registered this extension, if we
// have one then delegate the request to that client.
_responseSink.add(
await registeredClient._forwardServiceExtensionRequest(request));
// Bail out early in this case, we are just acting as a proxy and
// never get a `Response` instance.
return;
} else if (method.startsWith('ext.')) {
// Remaining methods with `ext.` are assumed to be registered via
// dart:developer, which the service implementation handles.
final args = params == null ? null : Map<String, dynamic>.of(params);
final isolateId = args?.remove('isolateId');
response = await _serviceImplementation.callServiceExtension(method,
isolateId: isolateId, args: args);
} else {
throw RPCError(method, RPCErrorKind.kMethodNotFound.code,
'Method not found', request);
}
''');
// Terminate the switch
gen.writeln('}');
// Generate the json success response
gen.write("""_responseSink.add({
'jsonrpc': '2.0',
'id': id,
'result': response.toJson(),
});
""");
// Close the try block, handle errors
gen.write(r'''
} on SentinelException catch (e) {
_responseSink.add({
'jsonrpc': '2.0',
'id': request['id'],
'result': e.sentinel.toJson(),
});
} catch (e, st) {
final error = e is RPCError
? e.toMap()
: {
'code': RPCErrorKind.kInternalError.code,
'message': '${request['method']}: $e',
'data': {'details': '$st'},
};
_responseSink.add({
'jsonrpc': '2.0',
'id': request['id'],
'error': error,
});
}
''');
// terminate the _delegateRequest method
gen.write('}');
gen.writeln();
gen.write('}');
gen.writeln();
}
void setDefaultValue(String typeName, String fieldName, String defaultValue) {
types
.firstWhere((t) => t!.name == typeName)!
.fields
.firstWhere((f) => f.name == fieldName)
.defaultValue = defaultValue;
}
}

View File

@ -14,13 +14,17 @@ class DartGenerator {
static const defaultColumnBoundary = 80;
final int colBoundary;
final String interfaceName;
String _indent = '';
final StringBuffer _buf = StringBuffer();
bool _previousWasEol = false;
DartGenerator({this.colBoundary = defaultColumnBoundary});
DartGenerator({
required this.interfaceName,
this.colBoundary = defaultColumnBoundary,
});
/// Write out the given dartdoc text, wrapping lines as necessary to flow
/// along the column boundary.
@ -28,8 +32,9 @@ class DartGenerator {
docs = docs
.replaceAll('[RPC error] code', 'RPC error code')
.replaceAll('[RPC error]', '[RPCError]')
.replaceAll('[SnapshotGraph]', '[HeapSnapshotGraph]')
.replaceAllMapped(RegExp(r'\[([gs]et[a-zA-Z]+)\]'), (match) {
return '[VmServiceInterface.${match[1]}]';
return '[$interfaceName.${match[1]}]';
});
docs = wrap(docs.trim(), colBoundary - _indent.length - 4);

View File

@ -9,19 +9,21 @@ import 'package:path/path.dart';
import 'package:pub_semver/pub_semver.dart';
import 'common/generate_common.dart';
import 'dart/generate_dart.dart' as dart show Api, api, DartGenerator;
import 'dart/generate_dart_client.dart';
import 'dart/generate_dart_common.dart';
import 'dart/generate_dart_interface.dart';
import 'java/generate_java.dart' as java show Api, api, JavaGenerator;
final bool _stampPubspecVersion = false;
/// Parse the 'service.md' into a model and generate both Dart and Java
/// libraries.
void main(List<String> args) async {
String appDirPath = dirname(Platform.script.toFilePath());
Future<void> main(List<String> args) async {
final codeGeneratorDir = dirname(Platform.script.toFilePath());
// Parse service.md into a model.
final file = File(
normalize(join(appDirPath, '../../../runtime/vm/service/service.md')),
normalize(join(codeGeneratorDir, '../../../runtime/vm/service/service.md')),
);
final document = Document();
final buf = StringBuffer(file.readAsStringSync());
@ -32,26 +34,81 @@ void main(List<String> args) async {
// Generate code from the model.
print('');
await _generateDart(appDirPath, nodes);
await _generateJava(appDirPath, nodes);
await _generateDartClient(codeGeneratorDir, nodes);
await _generateDartInterface(codeGeneratorDir, nodes);
await _generateJava(codeGeneratorDir, nodes);
}
Future _generateDart(String appDirPath, List<Node> nodes) async {
var outDirPath = normalize(join(appDirPath, '..', 'lib/src'));
var outDir = Directory(outDirPath);
if (!outDir.existsSync()) outDir.createSync(recursive: true);
var outputFile = File(join(outDirPath, 'vm_service.dart'));
var generator = dart.DartGenerator();
dart.api = dart.Api();
dart.api.parse(nodes);
dart.api.generate(generator);
outputFile.writeAsStringSync(generator.toString());
ProcessResult result = Process.runSync('dart', ['format', outDirPath]);
if (result.exitCode != 0) {
print('dart format: ${result.stdout}\n${result.stderr}');
throw result.exitCode;
Future<void> _generateDartClient(
String codeGeneratorDir, List<Node> nodes) async {
var outputFilePath = await _generateDartCommon(
api: VmServiceApi(),
nodes: nodes,
codeGeneratorDir: codeGeneratorDir,
packageName: 'vm_service',
interfaceName: 'VmService',
);
print('Wrote Dart client to $outputFilePath.');
outputFilePath = await _generateDartCommon(
api: VmServiceInterfaceApi(),
nodes: nodes,
codeGeneratorDir: codeGeneratorDir,
packageName: 'vm_service',
interfaceName: 'VmServiceInterface',
fileNameOverride: 'vm_service_interface',
);
print('Wrote Dart temporary interface to $outputFilePath.');
}
Future<void> _generateDartInterface(
String codeGeneratorDir, List<Node> nodes) async {
final outputFilePath = await _generateDartCommon(
api: VmServiceInterfaceApi(),
nodes: nodes,
codeGeneratorDir: codeGeneratorDir,
packageName: 'vm_service_interface',
interfaceName: 'VmServiceInterface',
);
print('Wrote Dart interface to $outputFilePath.');
}
Future<String> _generateDartCommon({
required Api api,
required List<Node> nodes,
required String codeGeneratorDir,
required String packageName,
required String interfaceName,
String? fileNameOverride,
}) async {
final outDirPath = normalize(
join(
codeGeneratorDir,
'../..',
packageName,
'lib/src',
),
);
final outDir = Directory(outDirPath);
if (!outDir.existsSync()) {
outDir.createSync(recursive: true);
}
final outputFile = File(
join(
outDirPath,
'${fileNameOverride ?? packageName}.dart',
),
);
final generator = DartGenerator(interfaceName: interfaceName);
// Generate the code.
api.parse(nodes);
api.generate(generator);
outputFile.writeAsStringSync(generator.toString());
// Clean up the code.
await _runDartFormat(outDirPath);
if (_stampPubspecVersion) {
// Update the pubspec file.
Version version = ApiParseUtil.parseVersionSemVer(nodes);
@ -60,12 +117,19 @@ Future _generateDart(String appDirPath, List<Node> nodes) async {
// Validate that the changelog contains an entry for the current version.
_checkUpdateChangelog(version);
}
print('Wrote Dart to ${outputFile.path}.');
return outputFile.path;
}
Future _generateJava(String appDirPath, List<Node> nodes) async {
var srcDirPath = normalize(join(appDirPath, '..', 'java', 'src'));
Future<void> _runDartFormat(String outDirPath) async {
ProcessResult result = Process.runSync('dart', ['format', outDirPath]);
if (result.exitCode != 0) {
print('dart format: ${result.stdout}\n${result.stderr}');
throw result.exitCode;
}
}
Future<void> _generateJava(String codeGeneratorDir, List<Node> nodes) async {
var srcDirPath = normalize(join(codeGeneratorDir, '..', 'java', 'src'));
var generator = java.JavaGenerator(srcDirPath);
final scriptPath = Platform.script.toFilePath();
@ -83,7 +147,7 @@ Future _generateJava(String appDirPath, List<Node> nodes) async {
.map((path) => relative(path, from: 'java'))
.toList();
generatedPaths.sort();
File gitignoreFile = File(join(appDirPath, '..', 'java', '.gitignore'));
File gitignoreFile = File(join(codeGeneratorDir, '..', 'java', '.gitignore'));
gitignoreFile.writeAsStringSync('''
# This is a generated file.

View File

@ -0,0 +1,3 @@
## 1.0.0
- Initial release.
- Provides version 4.13 of the Dart VM service protocol.

View File

@ -0,0 +1,3 @@
# Contributing to `package:vm_service` and `package:vm_service_interface`
See [CONTRIBUTING.md](https://github.com/dart-lang/sdk/blob/main/pkg/vm_service/CONTRIBUTING.md) in `package:vm_service` for details.

View File

@ -0,0 +1,27 @@
Copyright 2023, the Dart project authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1 @@
file:/tools/OWNERS_VM

View File

@ -0,0 +1,15 @@
# `package:vm_service_interface`
[![pub package](https://img.shields.io/pub/v/vm_service_interface.svg)](https://pub.dev/packages/vm_service_interface)
[![package publisher](https://img.shields.io/pub/publisher/vm_service_interface.svg)](https://pub.dev/packages/vm_service_interface/publisher)
A library providing an interface to implement the Dart VM service protocol.
The VM Service Protocol specification can be found at
[github.com/dart-lang/sdk/runtime/vm/service/service.md](https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md).
## Features and bugs
Please file feature requests and bugs at the [issue tracker][tracker].
[tracker]: https://github.com/dart-lang/sdk/issues

View File

@ -0,0 +1,8 @@
include: package:lints/recommended.yaml
linter:
rules:
# still 1 error in lib/src/vm_service_interface.dart
- comment_references
- directives_ordering
- prefer_single_quotes

View File

@ -0,0 +1,71 @@
// Copyright (c) 2023, 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';
import 'package:vm_service/vm_service.dart' show RPCError, Event, EventKind;
import 'stream_helpers.dart';
import 'vm_service_interface.dart' show VmServerConnection;
/// A registry of custom service extensions to [VmServerConnection]s in which
/// they were registered.
class ServiceExtensionRegistry {
/// Maps service extensions registered through the protocol to the
/// [VmServerConnection] in which they were registered.
///
/// Note: this does not track services registered through `dart:developer`,
/// only the services registered through the `_registerService` rpc method.
final _extensionToConnection = <String, VmServerConnection>{};
/// Controller for tracking registration and unregistration events.
final _eventController = StreamController<Event>.broadcast();
ServiceExtensionRegistry();
/// Registers [extension] for [client].
///
/// All future requests for [extension] will be routed to [client].
void registerExtension(String extension, VmServerConnection client) {
if (_extensionToConnection.containsKey(extension)) {
throw RPCError('registerExtension', 111, 'Service already registered');
}
_eventController.sink.add(_toRegistrationEvent(extension));
_extensionToConnection[extension] = client;
// Remove the mapping if the client disconnects.
client.done.whenComplete(() {
_extensionToConnection.remove(extension);
_eventController.sink.add(_toRegistrationEvent(extension,
kind: EventKind.kServiceUnregistered));
});
}
/// Returns the [VmServerConnection] for a given [extension], or `null` if
/// none is registered.
///
/// The result of this function should not be stored, because clients may
/// shut down at any time.
VmServerConnection? clientFor(String extension) =>
_extensionToConnection[extension];
/// All of the currently registered extensions
Iterable<String> get registeredExtensions => _extensionToConnection.keys;
/// Emits an [Event] of type `ServiceRegistered` for all current and future
/// extensions that are registered, and `ServiceUnregistered` when those
/// clients disconnect.
Stream<Event> get onExtensionEvent => _eventController.stream
.transform(startWithMany(registeredExtensions.map(_toRegistrationEvent)));
/// Creates a `_Service` stream event, with a default kind of
/// [EventKind.kServiceRegistered].
Event _toRegistrationEvent(String method,
{String kind = EventKind.kServiceRegistered}) =>
Event(
kind: kind,
service: method,
method: method,
timestamp: DateTime.now().millisecondsSinceEpoch,
);
}

View File

@ -0,0 +1,100 @@
// Copyright (c) 2023, 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';
/// Copied from package:stream_transform.
/// Starts emitting values from [next] after the original stream is complete.
///
/// If the initial stream never finishes, the [next] stream will never be
/// listened to.
///
/// If a single-subscription follows the a broadcast stream it may be listened
/// to and never canceled.
///
/// If a broadcast stream follows any other stream it will miss any events which
/// occur before the first stream is done. If a broadcast stream follows a
/// single-subscription stream, pausing the stream while it is listening to the
/// second stream will cause events to be dropped rather than buffered.
StreamTransformer<T, T> followedBy<T>(Stream<T> next) => _FollowedBy<T>(next);
class _FollowedBy<T> extends StreamTransformerBase<T, T> {
final Stream<T> _next;
_FollowedBy(this._next);
@override
Stream<T> bind(Stream<T> first) {
var controller = first.isBroadcast
? StreamController<T>.broadcast(sync: true)
: StreamController<T>(sync: true);
var next = first.isBroadcast && !_next.isBroadcast
? _next.asBroadcastStream()
: _next;
StreamSubscription<T>? subscription;
var currentStream = first;
var firstDone = false;
var secondDone = false;
late Function currentDoneHandler;
listen() {
subscription = currentStream.listen(controller.add,
onError: controller.addError, onDone: () => currentDoneHandler());
}
onSecondDone() {
secondDone = true;
controller.close();
}
onFirstDone() {
firstDone = true;
currentStream = next;
currentDoneHandler = onSecondDone;
listen();
}
currentDoneHandler = onFirstDone;
controller.onListen = () {
assert(subscription == null);
listen();
final sub = subscription!;
if (!first.isBroadcast) {
controller
..onPause = () {
if (!firstDone || !next.isBroadcast) return sub.pause();
sub.cancel();
subscription = null;
}
..onResume = () {
if (!firstDone || !next.isBroadcast) return sub.resume();
listen();
};
}
controller.onCancel = () {
if (secondDone) return null;
var toCancel = subscription!;
subscription = null;
return toCancel.cancel();
};
};
return controller.stream;
}
}
StreamTransformer<T, T> startWithMany<T>(Iterable<T> initial) =>
startWithStream<T>(Stream.fromIterable(initial));
StreamTransformer<T, T> startWithStream<T>(Stream<T> initial) =>
StreamTransformer.fromBind((values) {
if (values.isBroadcast && !initial.isBroadcast) {
initial = initial.asBroadcastStream();
}
return initial.transform(followedBy(values));
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
// Copyright (c) 2023, 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.
library vm_service_interface;
export 'src/vm_service_interface.dart';

View File

@ -0,0 +1,23 @@
name: vm_service_interface
version: 1.0.0
description: >-
A library providing an interface to implement the Dart VM service protocol.
repository: https://github.com/dart-lang/sdk/tree/main/pkg/vm_service/packages/vm_service_interface/
environment:
sdk: ^3.0.0
dependencies:
vm_service: '>=12.0.0 <14.0.0'
# We use 'any' version constraints here as we get our package versions from
# the dart-lang/sdk repo's DEPS file. Note that this is a special case; the
# best practice for packages is to specify their compatible version ranges.
# See also https://dart.dev/tools/pub/dependencies.
dev_dependencies:
async: any
lints: any
mockito: any
test: any

View File

@ -9,16 +9,18 @@ import 'dart:convert';
import 'package:async/async.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service.dart'
hide ServiceExtensionRegistry, VmServiceInterface, VmServerConnection;
import 'package:vm_service_interface/vm_service_interface.dart';
void main() {
late MockVmService serviceMock;
late MockVmServiceImplementation serviceMock;
late StreamController<Map<String, Object>> requestsController;
late StreamController<Map<String, Object?>> responsesController;
late ServiceExtensionRegistry serviceRegistry;
setUp(() {
serviceMock = MockVmService();
serviceMock = MockVmServiceImplementation();
requestsController = StreamController<Map<String, Object>>();
responsesController = StreamController<Map<String, Object?>>();
serviceRegistry = ServiceExtensionRegistry();
@ -33,7 +35,7 @@ void main() {
group('method delegation', () {
test('works for simple methods', () {
var request = rpcRequest("getVersion");
var request = rpcRequest('getVersion');
var version = Version(major: 1, minor: 0);
when(serviceMock.getVersion()).thenAnswer((_) => Future.value(version));
expect(responsesController.stream, emits(rpcResponse(version)));
@ -59,7 +61,7 @@ void main() {
isSystemIsolate: false,
);
var request =
rpcRequest("getIsolate", params: {'isolateId': isolate.id!});
rpcRequest('getIsolate', params: {'isolateId': isolate.id!});
when(serviceMock.getIsolate(isolate.id!))
.thenAnswer((Invocation invocation) {
expect(invocation.positionalArguments, equals([isolate.id]));
@ -88,7 +90,7 @@ void main() {
breakpoints: [],
isSystemIsolate: false,
);
var request = rpcRequest("setVMTimelineFlags", params: {
var request = rpcRequest('setVMTimelineFlags', params: {
'isolateId': isolate.id!,
// Note: the dynamic list below is intentional in order to exercise the
// code under test.
@ -109,7 +111,7 @@ void main() {
test('with no params or isolateId', () {
var extension = 'ext.cool';
var request = rpcRequest(extension, params: null);
var response = Response()..json = {"hello": "world"};
var response = Response()..json = {'hello': 'world'};
when(serviceMock.callServiceExtension(
extension,
isolateId: argThat(isNull, named: 'isolateId'),
@ -126,7 +128,7 @@ void main() {
test('with isolateId and no other params', () {
var extension = 'ext.cool';
var request = rpcRequest(extension, params: {'isolateId': '1'});
var response = Response()..json = {"hello": "world"};
var response = Response()..json = {'hello': 'world'};
when(serviceMock.callServiceExtension(
extension,
isolateId: argThat(equals('1'), named: 'isolateId'),
@ -144,7 +146,7 @@ void main() {
var extension = 'ext.cool';
var params = {'cool': 'option'};
var request = rpcRequest(extension, params: params);
var response = Response()..json = {"hello": "world"};
var response = Response()..json = {'hello': 'world'};
when(serviceMock.callServiceExtension(
extension,
isolateId: argThat(isNull, named: 'isolateId'),
@ -163,10 +165,10 @@ void main() {
var params = {'cool': 'option'};
var request =
rpcRequest(extension, params: Map.of(params)..['isolateId'] = '1');
var response = Response()..json = {"hello": "world"};
var response = Response()..json = {'hello': 'world'};
when(serviceMock.callServiceExtension(
extension,
isolateId: argThat(equals("1"), named: 'isolateId'),
isolateId: argThat(equals('1'), named: 'isolateId'),
args: argThat(equals(params), named: 'args'),
)).thenAnswer((Invocation invocation) {
expect(invocation.namedArguments,
@ -181,7 +183,7 @@ void main() {
group('error handling', () {
test('special cases RPCError instances', () {
var request = rpcRequest("getVersion");
var request = rpcRequest('getVersion');
var error =
RPCError('getVersion', 1234, 'custom message', {'custom': 'data'});
when(serviceMock.getVersion()).thenAnswer((_) => Future.error(error));
@ -190,7 +192,7 @@ void main() {
});
test('has a fallback for generic exceptions', () {
var request = rpcRequest("getVersion");
var request = rpcRequest('getVersion');
var error = UnimplementedError();
when(serviceMock.getVersion()).thenAnswer((_) => Future.error(error));
expect(
@ -346,7 +348,7 @@ void main() {
requestsController2.stream,
responsesController2.sink,
serviceRegistry,
VmService(Stream.empty(), (String _) => null),
MockVmServiceImplementation(),
);
expect(
@ -374,7 +376,8 @@ void main() {
requestsController3.stream,
responsesController3.sink,
serviceRegistry,
VmService(Stream.empty(), (String _) => null),
MockVmServiceImplementation(),
//VmService(Stream.empty(), (String _) => null),
);
expect(
responsesController3.stream,
@ -446,21 +449,21 @@ void main() {
}
Map<String, Object> rpcRequest(String method,
{Map<String, Object>? params = const {}, String id = "1"}) =>
{Map<String, Object>? params = const {}, String id = '1'}) =>
{
"jsonrpc": "2.0",
"method": method,
if (params != null) "params": params,
"id": id,
'jsonrpc': '2.0',
'method': method,
if (params != null) 'params': params,
'id': id,
};
Map<String, Object> rpcResponse(Response response, {String id = "1"}) => {
Map<String, Object> rpcResponse(Response response, {String id = '1'}) => {
'jsonrpc': '2.0',
'id': id,
'result': response.toJson(),
};
Map<String, Object> rpcErrorResponse(Object error, {String id = "1"}) {
Map<String, Object> rpcErrorResponse(Object error, {String id = '1'}) {
Map<String, Object> errorJson;
if (error is RPCError) {
errorJson = {
@ -488,7 +491,7 @@ Map<String, Object> streamNotifyResponse(String streamId, Event event) {
'jsonrpc': '2.0',
'method': 'streamNotify',
'params': {
'streamId': '$streamId',
'streamId': streamId,
'event': event.toJson(),
},
};
@ -502,7 +505,7 @@ Map<String, Object?> stripEventTimestamp(Map response) {
return response as Map<String, Object?>;
}
class MockVmService extends Mock implements VmServiceInterface {
class MockVmServiceImplementation extends Mock implements VmServiceInterface {
final streamControllers = <String, StreamController<Event>>{};
@override