| // 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( |
| "${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;'); |
| } |
| } |
| 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; |
| } |
| } |