82a952f14a
Also updates special cased handling of the `scope` parameter to instead work for any Map typed parameter. Simplified things in general here by casting values to the correct collection type and then relying on `.cast()` to fill in the proper generic types instead of explicitly filling them in. Change-Id: I7fef91105ca73ee9726780abd76e92817a9e46c0 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/504780 Commit-Queue: Jake Macdonald <jakemac@google.com> Auto-Submit: Jake Macdonald <jakemac@google.com> Reviewed-by: Ben Konyi <bkonyi@google.com>
290 lines
10 KiB
Dart
290 lines
10 KiB
Dart
// 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';
|
|
|
|
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});
|
|
|
|
/// Invoked by the Dart Development Service (DDS) immediately after it
|
|
/// connects.
|
|
///
|
|
/// [uri] is a HTTP URI pointing to the connected DDS instance.
|
|
///
|
|
/// When invoked, the VM service implementation should enter single-client
|
|
/// mode, disconnecting all non-DDS clients and rejecting any other future
|
|
/// direct connections to the service. This is to ensure that assumptions
|
|
/// about state made by DDS are valid, as DDS assumes responsibility for
|
|
/// client management, stream management, and service extension routing upon
|
|
/// connection.
|
|
Future<void> yieldControlToDDS(String uri);
|
|
''');
|
|
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(
|
|
"(params${nullCheck()}['${arg.name}'] as List? ?? []).cast(), ");
|
|
} 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.type.isMap) {
|
|
gen.writeln(
|
|
"${arg.name}: (params${nullCheck()}['${arg.name}'] as Map?)?.cast(), ");
|
|
} else if (arg.type.isArray) {
|
|
gen.writeln(
|
|
"${arg.name}: (params${nullCheck()}['${arg.name}'] as List?)?.cast(), ");
|
|
} else {
|
|
gen.writeln(
|
|
"${arg.name}: params${nullCheck()}['${arg.name}'], ");
|
|
}
|
|
}
|
|
}
|
|
gen.writeln(');');
|
|
}
|
|
gen.writeln('break;');
|
|
}
|
|
}
|
|
gen.writeln("case '_yieldControlToDDS':");
|
|
gen.writeln(
|
|
"await _serviceImplementation.yieldControlToDDS(params!['uri']!);");
|
|
gen.writeln('response = Success();');
|
|
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;
|
|
}
|
|
}
|