From 08b4f492490f1aadef4098bbe57fdc55cc108183 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Mon, 23 Oct 2023 17:00:10 +0000 Subject: [PATCH] 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 Reviewed-by: Devon Carew Commit-Queue: Ben Konyi --- pkg/dds/CHANGELOG.md | 4 + pkg/dds/lib/src/dap/adapters/dart.dart | 2 +- pkg/dds/lib/src/dap/protocol_converter.dart | 2 +- pkg/dds/pubspec.yaml | 4 +- pkg/dds/test/dap/mocks.dart | 2 +- pkg/dds_service_extensions/CHANGELOG.md | 5 +- pkg/dds_service_extensions/pubspec.yaml | 4 +- pkg/vm_service/CONTRIBUTING.md | 34 + pkg/vm_service/README.md | 17 +- pkg/vm_service/analysis_options.yaml | 2 +- .../lib/src/service_extension_registry.dart | 4 +- pkg/vm_service/lib/src/vm_service.dart | 1631 ++++------------ .../lib/src/vm_service_interface.dart | 1694 +++++++++++++++++ pkg/vm_service/lib/vm_service.dart | 3 + pkg/vm_service/pubspec.yaml | 9 +- ..._in_package_parts_class_file_uri_test.dart | 2 +- .../test/get_http_profile_test.dart | 13 +- .../test/patterns_local_vars_test.dart | 6 +- .../test/test_package/{ => lib}/has_part.dart | 0 .../test/test_package/{ => lib}/the_part.dart | 0 .../test_package/{ => lib}/the_part_2.dart | 0 .../test/verify_http_timeline_test.dart | 5 +- .../tool/common/generate_common.dart | 2 +- .../tool/dart/generate_dart_client.dart | 669 +++++++ ...te_dart.dart => generate_dart_common.dart} | 1000 +--------- .../tool/dart/generate_dart_interface.dart | 267 +++ pkg/vm_service/tool/dart/src_gen_dart.dart | 9 +- pkg/vm_service/tool/generate.dart | 114 +- pkg/vm_service_interface/CHANGELOG.md | 3 + pkg/vm_service_interface/CONTRIBUTING.md | 3 + pkg/vm_service_interface/LICENSE | 27 + pkg/vm_service_interface/OWNERS | 1 + pkg/vm_service_interface/README.md | 15 + .../analysis_options.yaml | 8 + .../lib/src/service_extension_registry.dart | 71 + .../lib/src/stream_helpers.dart | 100 + .../lib/src/vm_service_interface.dart | 1694 +++++++++++++++++ .../lib/vm_service_interface.dart | 7 + pkg/vm_service_interface/pubspec.yaml | 23 + .../test/server_test.dart | 51 +- 40 files changed, 5253 insertions(+), 2254 deletions(-) create mode 100644 pkg/vm_service/CONTRIBUTING.md create mode 100644 pkg/vm_service/lib/src/vm_service_interface.dart rename pkg/vm_service/test/test_package/{ => lib}/has_part.dart (100%) rename pkg/vm_service/test/test_package/{ => lib}/the_part.dart (100%) rename pkg/vm_service/test/test_package/{ => lib}/the_part_2.dart (100%) create mode 100644 pkg/vm_service/tool/dart/generate_dart_client.dart rename pkg/vm_service/tool/dart/{generate_dart.dart => generate_dart_common.dart} (58%) create mode 100644 pkg/vm_service/tool/dart/generate_dart_interface.dart create mode 100644 pkg/vm_service_interface/CHANGELOG.md create mode 100644 pkg/vm_service_interface/CONTRIBUTING.md create mode 100644 pkg/vm_service_interface/LICENSE create mode 100644 pkg/vm_service_interface/OWNERS create mode 100644 pkg/vm_service_interface/README.md create mode 100644 pkg/vm_service_interface/analysis_options.yaml create mode 100644 pkg/vm_service_interface/lib/src/service_extension_registry.dart create mode 100644 pkg/vm_service_interface/lib/src/stream_helpers.dart create mode 100644 pkg/vm_service_interface/lib/src/vm_service_interface.dart create mode 100644 pkg/vm_service_interface/lib/vm_service_interface.dart create mode 100644 pkg/vm_service_interface/pubspec.yaml rename pkg/{vm_service => vm_service_interface}/test/server_test.dart (93%) diff --git a/pkg/dds/CHANGELOG.md b/pkg/dds/CHANGELOG.md index d3cad02f006..3716463a220 100644 --- a/pkg/dds/CHANGELOG.md +++ b/pkg/dds/CHANGELOG.md @@ -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. diff --git a/pkg/dds/lib/src/dap/adapters/dart.dart b/pkg/dds/lib/src/dap/adapters/dart.dart index 8042b3e0c5a..e1be4d623bc 100644 --- a/pkg/dds/lib/src/dap/adapters/dart.dart +++ b/pkg/dds/lib/src/dap/adapters/dart.dart @@ -349,7 +349,7 @@ abstract class DartDebugAdapter createVariableForGetter( - vm.VmServiceInterface service, + vm.VmService service, ThreadInfo thread, vm.Instance instance, { String? variableName, diff --git a/pkg/dds/pubspec.yaml b/pkg/dds/pubspec.yaml index 013c352ef85..f0c182b7910 100644 --- a/pkg/dds/pubspec.yaml +++ b/pkg/dds/pubspec.yaml @@ -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 diff --git a/pkg/dds/test/dap/mocks.dart b/pkg/dds/test/dap/mocks.dart index 225994b05c3..83571064b89 100644 --- a/pkg/dds/test/dap/mocks.dart +++ b/pkg/dds/test/dap/mocks.dart @@ -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); diff --git a/pkg/dds_service_extensions/CHANGELOG.md b/pkg/dds_service_extensions/CHANGELOG.md index cdd0984b362..313f5cc8701 100644 --- a/pkg/dds_service_extensions/CHANGELOG.md +++ b/pkg/dds_service_extensions/CHANGELOG.md @@ -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. diff --git a/pkg/dds_service_extensions/pubspec.yaml b/pkg/dds_service_extensions/pubspec.yaml index cfb87a9b005..27205322db6 100644 --- a/pkg/dds_service_extensions/pubspec.yaml +++ b/pkg/dds_service_extensions/pubspec.yaml @@ -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 diff --git a/pkg/vm_service/CONTRIBUTING.md b/pkg/vm_service/CONTRIBUTING.md new file mode 100644 index 00000000000..c21f19658ea --- /dev/null +++ b/pkg/vm_service/CONTRIBUTING.md @@ -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/.dart` \ No newline at end of file diff --git a/pkg/vm_service/README.md b/pkg/vm_service/README.md index 374178c5d21..427c9d1c02c 100644 --- a/pkg/vm_service/README.md +++ b/pkg/vm_service/README.md @@ -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/.dart` diff --git a/pkg/vm_service/analysis_options.yaml b/pkg/vm_service/analysis_options.yaml index 3a2d8b48212..b75439f6267 100644 --- a/pkg/vm_service/analysis_options.yaml +++ b/pkg/vm_service/analysis_options.yaml @@ -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 diff --git a/pkg/vm_service/lib/src/service_extension_registry.dart b/pkg/vm_service/lib/src/service_extension_registry.dart index 6de71cd0422..4ab32cb9325 100644 --- a/pkg/vm_service/lib/src/service_extension_registry.dart +++ b/pkg/vm_service/lib/src/service_extension_registry.dart @@ -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. diff --git a/pkg/vm_service/lib/src/vm_service.dart b/pkg/vm_service/lib/src/vm_service.dart index 143fca16313..63bec0454db 100644 --- a/pkg/vm_service/lib/src/vm_service.dart +++ b/pkg/vm_service/lib/src/vm_service.dart @@ -15,9 +15,6 @@ import 'dart:async'; import 'dart:convert' show base64, jsonDecode, jsonEncode, utf8; import 'dart:typed_data'; -import 'service_extension_registry.dart'; - -export 'service_extension_registry.dart' show ServiceExtensionRegistry; export 'snapshot_graph.dart' show HeapSnapshotClass, @@ -97,7 +94,8 @@ void _setIfNotNull(Map json, String key, Object? value) { json[key] = value; } -Future extensionCallHelper(VmService service, String method, Map args) { +Future extensionCallHelper( + VmService service, String method, Map args) { return service._call(method, args); } @@ -111,7 +109,7 @@ void addTypeFactory(String name, Function factory) { _typeFactories[name] = factory; } -Map _typeFactories = { +final _typeFactories = { 'AllocationProfile': AllocationProfile.parse, 'BoundField': BoundField.parse, 'BoundVariable': BoundVariable.parse, @@ -199,7 +197,7 @@ Map _typeFactories = { 'VM': VM.parse, }; -Map> _methodReturnTypes = { +final _methodReturnTypes = >{ 'addBreakpoint': const ['Breakpoint'], 'addBreakpointWithScriptUri': const ['Breakpoint'], 'addBreakpointAtEntry': const ['Breakpoint'], @@ -259,19 +257,139 @@ Map> _methodReturnTypes = { 'streamListen': const ['Success'], }; -/// A class representation of the Dart VM Service Protocol. -/// -/// Both clients and servers should implement this interface. -abstract 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 onEvent(String streamId); +class _OutstandingRequest { + _OutstandingRequest(this.method); + static int _idCounter = 0; + final id = '${_idCounter++}'; + final String method; + final _stackTrace = StackTrace.current; + final _completer = Completer(); - /// Handler for calling extra service extensions. - Future callServiceExtension(String method, - {String? isolateId, Map? args}); + Future get future => _completer.future; + + void complete(T value) => _completer.complete(value); + void completeError(Object error) => + _completer.completeError(error, _stackTrace); +} + +typedef VmServiceFactory = T Function({ + required Stream /*String|List*/ inStream, + required void Function(String message) writeMessage, + Log? log, + DisposeHandler? disposeHandler, + Future? streamClosed, + String? wsUri, +}); + +class VmService { + late final StreamSubscription _streamSub; + late final Function _writeMessage; + final _outstandingRequests = {}; + final _services = {}; + late final Log _log; + + /// The web socket URI pointing to the target VM service instance. + final String? wsUri; + + Stream get onSend => _onSend.stream; + final _onSend = StreamController.broadcast(sync: true); + + Stream get onReceive => _onReceive.stream; + final _onReceive = StreamController.broadcast(sync: true); + + Future get onDone => _onDoneCompleter.future; + final _onDoneCompleter = Completer(); + + final _eventControllers = >{}; + + StreamController _getEventController(String eventName) { + StreamController? controller = _eventControllers[eventName]; + if (controller == null) { + controller = StreamController.broadcast(); + _eventControllers[eventName] = controller; + } + return controller; + } + + late final DisposeHandler? _disposeHandler; + + VmService( + Stream /*String|List*/ 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 /*String|List*/ 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 onEvent(String streamId) => + _getEventController(streamId).stream; + + // VMUpdate, VMFlagUpdate + Stream get onVMEvent => _getEventController('VM').stream; + + // IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, IsolateReload, ServiceExtensionAdded + Stream get onIsolateEvent => _getEventController('Isolate').stream; + + // PauseStart, PauseExit, PauseBreakpoint, PauseInterrupted, PauseException, PausePostRequest, Resume, BreakpointAdded, BreakpointResolved, BreakpointRemoved, BreakpointUpdated, Inspect, None + Stream get onDebugEvent => _getEventController('Debug').stream; + + // CpuSamples, UserTagChanged + Stream get onProfilerEvent => _getEventController('Profiler').stream; + + // GC + Stream get onGCEvent => _getEventController('GC').stream; + + // Extension + Stream get onExtensionEvent => _getEventController('Extension').stream; + + // TimelineEvents, TimelineStreamsSubscriptionUpdate + Stream get onTimelineEvent => _getEventController('Timeline').stream; + + // Logging + Stream get onLoggingEvent => _getEventController('Logging').stream; + + // ServiceRegistered, ServiceUnregistered + Stream get onServiceEvent => _getEventController('Service').stream; + + // HeapSnapshot + Stream get onHeapSnapshotEvent => + _getEventController('HeapSnapshot').stream; + + // WriteEvent + Stream get onStdoutEvent => _getEventController('Stdout').stream; + + // WriteEvent + Stream get onStderrEvent => _getEventController('Stderr').stream; /// The `addBreakpoint` RPC is used to add a breakpoint at a specific line of /// some script. @@ -306,7 +424,13 @@ abstract class VmServiceInterface { String scriptId, int line, { int? column, - }); + }) => + _call('addBreakpoint', { + 'isolateId': isolateId, + 'scriptId': scriptId, + 'line': line, + if (column != null) 'column': column, + }); /// The `addBreakpoint` RPC is used to add a breakpoint at a specific line of /// some script. This RPC is useful when a script has not yet been assigned an @@ -343,7 +467,13 @@ abstract class VmServiceInterface { String scriptUri, int line, { int? column, - }); + }) => + _call('addBreakpointWithScriptUri', { + 'isolateId': isolateId, + 'scriptUri': scriptUri, + 'line': line, + if (column != null) 'column': column, + }); /// The `addBreakpointAtEntry` RPC is used to add a breakpoint at the /// entrypoint of some function. @@ -360,7 +490,10 @@ abstract class VmServiceInterface { /// /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. - Future addBreakpointAtEntry(String isolateId, String functionId); + Future addBreakpointAtEntry( + String isolateId, String functionId) => + _call('addBreakpointAtEntry', + {'isolateId': isolateId, 'functionId': functionId}); /// Clears all CPU profiling samples. /// @@ -371,12 +504,13 @@ abstract class VmServiceInterface { /// /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. - Future clearCpuSamples(String isolateId); + Future clearCpuSamples(String isolateId) => + _call('clearCpuSamples', {'isolateId': isolateId}); /// Clears all VM timeline events. /// /// See [Success]. - Future clearVMTimeline(); + Future clearVMTimeline() => _call('clearVMTimeline'); /// The `invoke` RPC is used to perform regular method invocation on some /// receiver, as if by dart:mirror's ObjectMirror.invoke. Note this does not @@ -420,7 +554,15 @@ abstract class VmServiceInterface { String selector, List argumentIds, { bool? disableBreakpoints, - }); + }) => + _call('invoke', { + 'isolateId': isolateId, + 'targetId': targetId, + 'selector': selector, + 'argumentIds': argumentIds, + if (disableBreakpoints != null) + 'disableBreakpoints': disableBreakpoints, + }); /// The `evaluate` RPC is used to evaluate an expression in the context of /// some target. @@ -466,7 +608,15 @@ abstract class VmServiceInterface { String expression, { Map? scope, bool? disableBreakpoints, - }); + }) => + _call('evaluate', { + 'isolateId': isolateId, + 'targetId': targetId, + 'expression': expression, + if (scope != null) 'scope': scope, + if (disableBreakpoints != null) + 'disableBreakpoints': disableBreakpoints, + }); /// The `evaluateInFrame` RPC is used to evaluate an expression in the context /// of a particular stack frame. `frameIndex` is the index of the desired @@ -504,7 +654,15 @@ abstract class VmServiceInterface { String expression, { Map? scope, bool? disableBreakpoints, - }); + }) => + _call('evaluateInFrame', { + 'isolateId': isolateId, + 'frameIndex': frameIndex, + 'expression': expression, + if (scope != null) 'scope': scope, + if (disableBreakpoints != null) + 'disableBreakpoints': disableBreakpoints, + }); /// The `getAllocationProfile` RPC is used to retrieve allocation information /// for a given isolate. @@ -522,13 +680,18 @@ abstract class VmServiceInterface { /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. Future getAllocationProfile(String isolateId, - {bool? reset, bool? gc}); + {bool? reset, bool? gc}) => + _call('getAllocationProfile', { + 'isolateId': isolateId, + if (reset != null && reset) 'reset': reset, + if (gc != null && gc) 'gc': gc, + }); /// The `getAllocationTraces` RPC allows for the retrieval of allocation /// traces for objects of a specific set of types (see - /// [VmServiceInterface.setTraceClassAllocation]). Only samples collected in - /// the time range `[timeOriginMicros, timeOriginMicros + timeExtentMicros]` - /// will be reported. + /// [VmService.setTraceClassAllocation]). Only samples collected in the time + /// range `[timeOriginMicros, timeOriginMicros + timeExtentMicros]` will be + /// reported. /// /// If `classId` is provided, only traces for allocations with the matching /// `classId` will be reported. @@ -544,7 +707,13 @@ abstract class VmServiceInterface { int? timeOriginMicros, int? timeExtentMicros, String? classId, - }); + }) => + _call('getAllocationTraces', { + 'isolateId': isolateId, + if (timeOriginMicros != null) 'timeOriginMicros': timeOriginMicros, + if (timeExtentMicros != null) 'timeExtentMicros': timeExtentMicros, + if (classId != null) 'classId': classId, + }); /// The `getClassList` RPC is used to retrieve a `ClassList` containing all /// classes for an isolate based on the isolate's `isolateId`. @@ -556,7 +725,8 @@ abstract class VmServiceInterface { /// /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. - Future getClassList(String isolateId); + Future getClassList(String isolateId) => + _call('getClassList', {'isolateId': isolateId}); /// The `getCpuSamples` RPC is used to retrieve samples collected by the CPU /// profiler. See [CpuSamples] for a detailed description of the response. @@ -564,8 +734,8 @@ abstract class VmServiceInterface { /// The `timeOriginMicros` parameter is the beginning of the time range used /// to filter samples. It uses the same monotonic clock as dart:developer's /// `Timeline.now` and the VM embedding API's `Dart_TimelineGetMicros`. See - /// [VmServiceInterface.getVMTimelineMicros] for access to this clock through - /// the service protocol. + /// [VmService.getVMTimelineMicros] for access to this clock through the + /// service protocol. /// /// The `timeExtentMicros` parameter specifies how large the time range used /// to filter samples should be. @@ -582,13 +752,18 @@ abstract class VmServiceInterface { /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. Future getCpuSamples( - String isolateId, int timeOriginMicros, int timeExtentMicros); + String isolateId, int timeOriginMicros, int timeExtentMicros) => + _call('getCpuSamples', { + 'isolateId': isolateId, + 'timeOriginMicros': timeOriginMicros, + 'timeExtentMicros': timeExtentMicros + }); /// The `getFlagList` RPC returns a list of all command line flags in the VM /// along with their current values. /// /// See [FlagList]. - Future getFlagList(); + Future getFlagList() => _call('getFlagList'); /// Returns a set of inbound references to the object specified by `targetId`. /// Up to `limit` references will be returned. @@ -618,7 +793,9 @@ abstract class VmServiceInterface { /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. Future getInboundReferences( - String isolateId, String targetId, int limit); + String isolateId, String targetId, int limit) => + _call('getInboundReferences', + {'isolateId': isolateId, 'targetId': targetId, 'limit': limit}); /// The `getInstances` RPC is used to retrieve a set of instances which are of /// a specific class. @@ -656,7 +833,15 @@ abstract class VmServiceInterface { int limit, { bool? includeSubclasses, bool? includeImplementers, - }); + }) => + _call('getInstances', { + 'isolateId': isolateId, + 'objectId': objectId, + 'limit': limit, + if (includeSubclasses != null) 'includeSubclasses': includeSubclasses, + if (includeImplementers != null) + 'includeImplementers': includeImplementers, + }); /// The `getInstancesAsList` RPC is used to retrieve a set of instances which /// are of a specific class. This RPC returns an `InstanceRef` corresponding @@ -693,7 +878,14 @@ abstract class VmServiceInterface { String objectId, { bool? includeSubclasses, bool? includeImplementers, - }); + }) => + _call('getInstancesAsList', { + 'isolateId': isolateId, + 'objectId': objectId, + if (includeSubclasses != null) 'includeSubclasses': includeSubclasses, + if (includeImplementers != null) + 'includeImplementers': includeImplementers, + }); /// The `getIsolate` RPC is used to lookup an `Isolate` object by its `id`. /// @@ -704,7 +896,8 @@ abstract class VmServiceInterface { /// /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. - Future getIsolate(String isolateId); + Future getIsolate(String isolateId) => + _call('getIsolate', {'isolateId': isolateId}); /// The `getIsolateGroup` RPC is used to lookup an `IsolateGroup` object by /// its `id`. @@ -720,7 +913,8 @@ abstract class VmServiceInterface { /// /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. - Future getIsolateGroup(String isolateGroupId); + Future getIsolateGroup(String isolateGroupId) => + _call('getIsolateGroup', {'isolateGroupId': isolateGroupId}); /// The `getIsolatePauseEvent` RPC is used to lookup an isolate's pause event /// by its `id`. @@ -732,7 +926,8 @@ abstract class VmServiceInterface { /// /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. - Future getIsolatePauseEvent(String isolateId); + Future getIsolatePauseEvent(String isolateId) => + _call('getIsolatePauseEvent', {'isolateId': isolateId}); /// The `getMemoryUsage` RPC is used to lookup an isolate's memory usage /// statistics by its `id`. @@ -744,7 +939,8 @@ abstract class VmServiceInterface { /// /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. - Future getMemoryUsage(String isolateId); + Future getMemoryUsage(String isolateId) => + _call('getMemoryUsage', {'isolateId': isolateId}); /// The `getIsolateGroupMemoryUsage` RPC is used to lookup an isolate group's /// memory usage statistics by its `id`. @@ -756,7 +952,8 @@ abstract class VmServiceInterface { /// /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. - Future getIsolateGroupMemoryUsage(String isolateGroupId); + Future getIsolateGroupMemoryUsage(String isolateGroupId) => + _call('getIsolateGroupMemoryUsage', {'isolateGroupId': isolateGroupId}); /// The `getScripts` RPC is used to retrieve a `ScriptList` containing all /// scripts for an isolate based on the isolate's `isolateId`. @@ -768,7 +965,8 @@ abstract class VmServiceInterface { /// /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. - Future getScripts(String isolateId); + Future getScripts(String isolateId) => + _call('getScripts', {'isolateId': isolateId}); /// The `getObject` RPC is used to lookup an `object` from some isolate by its /// `id`. @@ -801,7 +999,13 @@ abstract class VmServiceInterface { String objectId, { int? offset, int? count, - }); + }) => + _call('getObject', { + 'isolateId': isolateId, + 'objectId': objectId, + if (offset != null) 'offset': offset, + if (count != null) 'count': count, + }); /// The `getPerfettoCpuSamples` RPC is used to retrieve samples collected by /// the CPU profiler, serialized in Perfetto's proto format. See @@ -810,8 +1014,8 @@ abstract class VmServiceInterface { /// The `timeOriginMicros` parameter is the beginning of the time range used /// to filter samples. It uses the same monotonic clock as dart:developer's /// `Timeline.now` and the VM embedding API's `Dart_TimelineGetMicros`. See - /// [VmServiceInterface.getVMTimelineMicros] for access to this clock through - /// the service protocol. + /// [VmService.getVMTimelineMicros] for access to this clock through the + /// service protocol. /// /// The `timeExtentMicros` parameter specifies how large the time range used /// to filter samples should be. @@ -828,7 +1032,12 @@ abstract class VmServiceInterface { /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. Future getPerfettoCpuSamples(String isolateId, - {int? timeOriginMicros, int? timeExtentMicros}); + {int? timeOriginMicros, int? timeExtentMicros}) => + _call('getPerfettoCpuSamples', { + 'isolateId': isolateId, + if (timeOriginMicros != null) 'timeOriginMicros': timeOriginMicros, + if (timeExtentMicros != null) 'timeExtentMicros': timeExtentMicros, + }); /// The `getPerfettoVMTimeline` RPC is used to retrieve an object which /// contains a VM timeline trace represented in Perfetto's proto format. See @@ -837,8 +1046,8 @@ abstract class VmServiceInterface { /// The `timeOriginMicros` parameter is the beginning of the time range used /// to filter timeline events. It uses the same monotonic clock as /// dart:developer's `Timeline.now` and the VM embedding API's - /// `Dart_TimelineGetMicros`. See [VmServiceInterface.getVMTimelineMicros] for - /// access to this clock through the service protocol. + /// `Dart_TimelineGetMicros`. See [VmService.getVMTimelineMicros] for access + /// to this clock through the service protocol. /// /// The `timeExtentMicros` parameter specifies how large the time range used /// to filter timeline events should be. @@ -862,13 +1071,18 @@ abstract class VmServiceInterface { /// request`, will be returned as timeline events are written directly to a /// file, and thus cannot be retrieved through the VM Service, in these modes. Future getPerfettoVMTimeline( - {int? timeOriginMicros, int? timeExtentMicros}); + {int? timeOriginMicros, int? timeExtentMicros}) => + _call('getPerfettoVMTimeline', { + if (timeOriginMicros != null) 'timeOriginMicros': timeOriginMicros, + if (timeExtentMicros != null) 'timeExtentMicros': timeExtentMicros, + }); /// The `getPorts` RPC is used to retrieve the list of `ReceivePort` instances /// for a given isolate. /// /// See [PortList]. - Future getPorts(String isolateId); + Future getPorts(String isolateId) => + _call('getPorts', {'isolateId': isolateId}); /// The `getRetainingPath` RPC is used to lookup a path from an object /// specified by `targetId` to a GC root (i.e., the object which is preventing @@ -895,14 +1109,17 @@ abstract class VmServiceInterface { /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. Future getRetainingPath( - String isolateId, String targetId, int limit); + String isolateId, String targetId, int limit) => + _call('getRetainingPath', + {'isolateId': isolateId, 'targetId': targetId, 'limit': limit}); /// Returns a description of major uses of memory known to the VM. /// /// Adding or removing buckets is considered a backwards-compatible change for /// the purposes of versioning. A client must gracefully handle the removal or /// addition of any bucket. - Future getProcessMemoryUsage(); + Future getProcessMemoryUsage() => + _call('getProcessMemoryUsage'); /// The `getStack` RPC is used to retrieve the current execution stack and /// message queue for an isolate. The isolate does not need to be paused. @@ -919,7 +1136,10 @@ abstract class VmServiceInterface { /// /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. - Future getStack(String isolateId, {int? limit}); + Future getStack(String isolateId, {int? limit}) => _call('getStack', { + 'isolateId': isolateId, + if (limit != null) 'limit': limit, + }); /// The `getSupportedProtocols` RPC is used to determine which protocols are /// supported by the current server. @@ -929,7 +1149,8 @@ abstract class VmServiceInterface { /// the list of protocols before forwarding the response to the client. /// /// See [ProtocolList]. - Future getSupportedProtocols(); + Future getSupportedProtocols() => + _call('getSupportedProtocols'); /// The `getSourceReport` RPC is used to generate a set of reports tied to /// source locations in an isolate. @@ -1002,18 +1223,30 @@ abstract class VmServiceInterface { bool? reportLines, List? libraryFilters, List? librariesAlreadyCompiled, - }); + }) => + _call('getSourceReport', { + 'isolateId': isolateId, + 'reports': reports, + if (scriptId != null) 'scriptId': scriptId, + if (tokenPos != null) 'tokenPos': tokenPos, + if (endTokenPos != null) 'endTokenPos': endTokenPos, + if (forceCompile != null) 'forceCompile': forceCompile, + if (reportLines != null) 'reportLines': reportLines, + if (libraryFilters != null) 'libraryFilters': libraryFilters, + if (librariesAlreadyCompiled != null) + 'librariesAlreadyCompiled': librariesAlreadyCompiled, + }); /// The `getVersion` RPC is used to determine what version of the Service /// Protocol is served by a VM. /// /// See [Version]. - Future getVersion(); + Future getVersion() => _call('getVersion'); /// The `getVM` RPC returns global information about a Dart virtual machine. /// /// See [VM]. - Future getVM(); + Future getVM() => _call('getVM'); /// The `getVMTimeline` RPC is used to retrieve an object which contains VM /// timeline events. See [Timeline] for a detailed description of the @@ -1022,8 +1255,8 @@ abstract class VmServiceInterface { /// The `timeOriginMicros` parameter is the beginning of the time range used /// to filter timeline events. It uses the same monotonic clock as /// dart:developer's `Timeline.now` and the VM embedding API's - /// `Dart_TimelineGetMicros`. See [VmServiceInterface.getVMTimelineMicros] for - /// access to this clock through the service protocol. + /// `Dart_TimelineGetMicros`. See [VmService.getVMTimelineMicros] for access + /// to this clock through the service protocol. /// /// The `timeExtentMicros` parameter specifies how large the time range used /// to filter timeline events should be. @@ -1046,23 +1279,27 @@ abstract class VmServiceInterface { /// request`, will be returned as timeline events are written directly to a /// file, and thus cannot be retrieved through the VM Service, in these modes. Future getVMTimeline( - {int? timeOriginMicros, int? timeExtentMicros}); + {int? timeOriginMicros, int? timeExtentMicros}) => + _call('getVMTimeline', { + if (timeOriginMicros != null) 'timeOriginMicros': timeOriginMicros, + if (timeExtentMicros != null) 'timeExtentMicros': timeExtentMicros, + }); /// The `getVMTimelineFlags` RPC returns information about the current VM /// timeline configuration. /// /// To change which timeline streams are currently enabled, see - /// [VmServiceInterface.setVMTimelineFlags]. + /// [VmService.setVMTimelineFlags]. /// /// See [TimelineFlags]. - Future getVMTimelineFlags(); + Future getVMTimelineFlags() => _call('getVMTimelineFlags'); /// The `getVMTimelineMicros` RPC returns the current time stamp from the /// clock used by the timeline, similar to `Timeline.now` in `dart:developer` /// and `Dart_TimelineGetMicros` in the VM embedding API. /// - /// See [Timestamp] and [VmServiceInterface.getVMTimeline]. - Future getVMTimelineMicros(); + /// See [Timestamp] and [VmService.getVMTimeline]. + Future getVMTimelineMicros() => _call('getVMTimelineMicros'); /// The `pause` RPC is used to interrupt a running isolate. The RPC enqueues /// the interrupt request and potentially returns before the isolate is @@ -1077,7 +1314,8 @@ abstract class VmServiceInterface { /// /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. - Future pause(String isolateId); + Future pause(String isolateId) => + _call('pause', {'isolateId': isolateId}); /// The `kill` RPC is used to kill an isolate as if by dart:isolate's /// `Isolate.kill(IMMEDIATE)`. @@ -1091,7 +1329,8 @@ abstract class VmServiceInterface { /// /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. - Future kill(String isolateId); + Future kill(String isolateId) => + _call('kill', {'isolateId': isolateId}); /// The `lookupResolvedPackageUris` RPC is used to convert a list of URIs to /// their resolved (or absolute) paths. For example, URIs passed to this RPC @@ -1110,7 +1349,12 @@ abstract class VmServiceInterface { /// /// See [UriList]. Future lookupResolvedPackageUris(String isolateId, List uris, - {bool? local}); + {bool? local}) => + _call('lookupResolvedPackageUris', { + 'isolateId': isolateId, + 'uris': uris, + if (local != null) 'local': local, + }); /// The `lookupPackageUris` RPC is used to convert a list of URIs to their /// unresolved paths. For example, URIs passed to this RPC are mapped in the @@ -1125,7 +1369,8 @@ abstract class VmServiceInterface { /// will be `null`. /// /// See [UriList]. - Future lookupPackageUris(String isolateId, List uris); + Future lookupPackageUris(String isolateId, List uris) => + _call('lookupPackageUris', {'isolateId': isolateId, 'uris': uris}); /// Registers a service that can be invoked by other VM service clients, where /// `service` is the name of the service to advertise and `alias` is an @@ -1135,7 +1380,8 @@ abstract class VmServiceInterface { /// originally registered the service. /// /// See [Success]. - Future registerService(String service, String alias); + Future registerService(String service, String alias) => + _call('registerService', {'service': service, 'alias': alias}); /// The `reloadSources` RPC is used to perform a hot reload of the sources of /// all isolates in the same isolate group as the isolate specified by @@ -1164,7 +1410,14 @@ abstract class VmServiceInterface { bool? pause, String? rootLibUri, String? packagesUri, - }); + }) => + _call('reloadSources', { + 'isolateId': isolateId, + if (force != null) 'force': force, + if (pause != null) 'pause': pause, + if (rootLibUri != null) 'rootLibUri': rootLibUri, + if (packagesUri != null) 'packagesUri': packagesUri, + }); /// The `removeBreakpoint` RPC is used to remove a breakpoint by its `id`. /// @@ -1177,22 +1430,25 @@ abstract class VmServiceInterface { /// /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. - Future removeBreakpoint(String isolateId, String breakpointId); + Future removeBreakpoint(String isolateId, String breakpointId) => + _call('removeBreakpoint', + {'isolateId': isolateId, 'breakpointId': breakpointId}); /// Requests a dump of the Dart heap of the given isolate. /// /// This method immediately returns success. The VM will then begin delivering /// binary events on the `HeapSnapshot` event stream. The binary data in these - /// events, when concatenated together, conforms to the [SnapshotGraph] type. - /// The splitting of the SnapshotGraph into events can happen at any byte - /// offset. + /// events, when concatenated together, conforms to the [HeapSnapshotGraph] + /// type. The splitting of the SnapshotGraph into events can happen at any + /// byte offset. /// /// If `isolateId` refers to an isolate which has exited, then the `Collected` /// [Sentinel] is returned. /// /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. - Future requestHeapSnapshot(String isolateId); + Future requestHeapSnapshot(String isolateId) => + _call('requestHeapSnapshot', {'isolateId': isolateId}); /// The `resume` RPC is used to resume execution of a paused isolate. /// @@ -1224,7 +1480,12 @@ abstract class VmServiceInterface { /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. Future resume(String isolateId, - {/*StepOption*/ String? step, int? frameIndex}); + {/*StepOption*/ String? step, int? frameIndex}) => + _call('resume', { + 'isolateId': isolateId, + if (step != null) 'step': step, + if (frameIndex != null) 'frameIndex': frameIndex, + }); /// The `setBreakpointState` RPC allows for breakpoints to be enabled or /// disabled, without requiring for the breakpoint to be completely removed. @@ -1236,7 +1497,12 @@ abstract class VmServiceInterface { /// /// See [Breakpoint]. Future setBreakpointState( - String isolateId, String breakpointId, bool enable); + String isolateId, String breakpointId, bool enable) => + _call('setBreakpointState', { + 'isolateId': isolateId, + 'breakpointId': breakpointId, + 'enable': enable + }); /// The `setExceptionPauseMode` RPC is used to control if an isolate pauses /// when an exception is thrown. @@ -1254,7 +1520,8 @@ abstract class VmServiceInterface { /// returned. @Deprecated('Use setIsolatePauseMode instead') Future setExceptionPauseMode( - String isolateId, /*ExceptionPauseMode*/ String mode); + String isolateId, /*ExceptionPauseMode*/ String mode) => + _call('setExceptionPauseMode', {'isolateId': isolateId, 'mode': mode}); /// The `setIsolatePauseMode` RPC is used to control if or when an isolate /// will pause due to a change in execution state. @@ -1274,8 +1541,14 @@ abstract class VmServiceInterface { /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. Future setIsolatePauseMode(String isolateId, - {/*ExceptionPauseMode*/ String? exceptionPauseMode, - bool? shouldPauseOnExit}); + {/*ExceptionPauseMode*/ String? exceptionPauseMode, + bool? shouldPauseOnExit}) => + _call('setIsolatePauseMode', { + 'isolateId': isolateId, + if (exceptionPauseMode != null) + 'exceptionPauseMode': exceptionPauseMode, + if (shouldPauseOnExit != null) 'shouldPauseOnExit': shouldPauseOnExit, + }); /// The `setFlag` RPC is used to set a VM flag at runtime. Returns an error if /// the named flag does not exist, the flag may not be set at runtime, or the @@ -1302,7 +1575,8 @@ abstract class VmServiceInterface { /// See [Success]. /// /// The return value can be one of [Success] or [Error]. - Future setFlag(String name, String value); + Future setFlag(String name, String value) => + _call('setFlag', {'name': name, 'value': value}); /// The `setLibraryDebuggable` RPC is used to enable or disable whether /// breakpoints and stepping work for a given library. @@ -1315,7 +1589,12 @@ abstract class VmServiceInterface { /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. Future setLibraryDebuggable( - String isolateId, String libraryId, bool isDebuggable); + String isolateId, String libraryId, bool isDebuggable) => + _call('setLibraryDebuggable', { + 'isolateId': isolateId, + 'libraryId': libraryId, + 'isDebuggable': isDebuggable + }); /// The `setName` RPC is used to change the debugging name for an isolate. /// @@ -1326,7 +1605,8 @@ abstract class VmServiceInterface { /// /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. - Future setName(String isolateId, String name); + Future setName(String isolateId, String name) => + _call('setName', {'isolateId': isolateId, 'name': name}); /// The `setTraceClassAllocation` RPC allows for enabling or disabling /// allocation tracing for a specific type of object. Allocation traces can be @@ -1343,12 +1623,14 @@ abstract class VmServiceInterface { /// This method will throw a [SentinelException] in the case a [Sentinel] is /// returned. Future setTraceClassAllocation( - String isolateId, String classId, bool enable); + String isolateId, String classId, bool enable) => + _call('setTraceClassAllocation', + {'isolateId': isolateId, 'classId': classId, 'enable': enable}); /// The `setVMName` RPC is used to change the debugging name for the vm. /// /// See [Success]. - Future setVMName(String name); + Future setVMName(String name) => _call('setVMName', {'name': name}); /// The `setVMTimelineFlags` RPC is used to set which timeline streams are /// enabled. @@ -1361,10 +1643,11 @@ abstract class VmServiceInterface { /// stream as a result of invoking this RPC. /// /// To get the list of currently enabled timeline streams, see - /// [VmServiceInterface.getVMTimelineFlags]. + /// [VmService.getVMTimelineFlags]. /// /// See [Success]. - Future setVMTimelineFlags(List recordedStreams); + Future setVMTimelineFlags(List recordedStreams) => + _call('setVMTimelineFlags', {'recordedStreams': recordedStreams}); /// The `streamCancel` RPC cancels a stream subscription in the VM. /// @@ -1372,7 +1655,8 @@ abstract class VmServiceInterface { /// subscribed) RPC error code is returned. /// /// See [Success]. - Future streamCancel(String streamId); + Future streamCancel(String streamId) => + _call('streamCancel', {'streamId': streamId}); /// The `streamCpuSamplesWithUserTag` RPC allows for clients to specify which /// CPU samples collected by the profiler should be sent over the `Profiler` @@ -1381,7 +1665,8 @@ abstract class VmServiceInterface { /// active. /// /// See [Success]. - Future streamCpuSamplesWithUserTag(List userTags); + Future streamCpuSamplesWithUserTag(List userTags) => + _call('streamCpuSamplesWithUserTag', {'userTags': userTags}); /// The `streamListen` RPC subscribes to a stream in the VM. Once subscribed, /// the client will begin receiving events from the stream. @@ -1421,1073 +1706,6 @@ abstract class VmServiceInterface { /// gracefully, perhaps by warning and ignoring. /// /// See [Success]. - Future streamListen(String streamId); -} - -class _PendingServiceRequest { - Future> get future => _completer.future; - final _completer = Completer>(); - - final dynamic originalId; - - _PendingServiceRequest(this.originalId); - - void complete(Map 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> _requestStream; - final StreamSink> _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 = {}; - - /// Completes when [_requestStream] is done. - Future get done => _doneCompleter.future; - final _doneCompleter = Completer(); - - /// Pending service extension requests to this client by id. - final _pendingServiceExtensionRequests = {}; - - 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> _forwardServiceExtensionRequest( - Map request) { - final originalId = request['id']; - request = Map.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 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.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?; - late Response response; - - switch (method) { - case 'registerService': - _serviceExtensionRegistry.registerExtension(params!['service'], this); - response = Success(); - break; - case 'addBreakpoint': - response = await _serviceImplementation.addBreakpoint( - params!['isolateId'], - params['scriptId'], - params['line'], - column: params['column'], - ); - break; - case 'addBreakpointWithScriptUri': - response = await _serviceImplementation.addBreakpointWithScriptUri( - params!['isolateId'], - params['scriptUri'], - params['line'], - column: params['column'], - ); - break; - case 'addBreakpointAtEntry': - response = await _serviceImplementation.addBreakpointAtEntry( - params!['isolateId'], - params['functionId'], - ); - break; - case 'clearCpuSamples': - response = await _serviceImplementation.clearCpuSamples( - params!['isolateId'], - ); - break; - case 'clearVMTimeline': - response = await _serviceImplementation.clearVMTimeline(); - break; - case 'invoke': - response = await _serviceImplementation.invoke( - params!['isolateId'], - params['targetId'], - params['selector'], - List.from(params['argumentIds'] ?? []), - disableBreakpoints: params['disableBreakpoints'], - ); - break; - case 'evaluate': - response = await _serviceImplementation.evaluate( - params!['isolateId'], - params['targetId'], - params['expression'], - scope: params['scope']?.cast(), - disableBreakpoints: params['disableBreakpoints'], - ); - break; - case 'evaluateInFrame': - response = await _serviceImplementation.evaluateInFrame( - params!['isolateId'], - params['frameIndex'], - params['expression'], - scope: params['scope']?.cast(), - disableBreakpoints: params['disableBreakpoints'], - ); - break; - case 'getAllocationProfile': - response = await _serviceImplementation.getAllocationProfile( - params!['isolateId'], - reset: params['reset'], - gc: params['gc'], - ); - break; - case 'getAllocationTraces': - response = await _serviceImplementation.getAllocationTraces( - params!['isolateId'], - timeOriginMicros: params['timeOriginMicros'], - timeExtentMicros: params['timeExtentMicros'], - classId: params['classId'], - ); - break; - case 'getClassList': - response = await _serviceImplementation.getClassList( - params!['isolateId'], - ); - break; - case 'getCpuSamples': - response = await _serviceImplementation.getCpuSamples( - params!['isolateId'], - params['timeOriginMicros'], - params['timeExtentMicros'], - ); - break; - case 'getFlagList': - response = await _serviceImplementation.getFlagList(); - break; - case 'getInboundReferences': - response = await _serviceImplementation.getInboundReferences( - params!['isolateId'], - params['targetId'], - params['limit'], - ); - break; - case 'getInstances': - response = await _serviceImplementation.getInstances( - params!['isolateId'], - params['objectId'], - params['limit'], - includeSubclasses: params['includeSubclasses'], - includeImplementers: params['includeImplementers'], - ); - break; - case 'getInstancesAsList': - response = await _serviceImplementation.getInstancesAsList( - params!['isolateId'], - params['objectId'], - includeSubclasses: params['includeSubclasses'], - includeImplementers: params['includeImplementers'], - ); - break; - case 'getIsolate': - response = await _serviceImplementation.getIsolate( - params!['isolateId'], - ); - break; - case 'getIsolateGroup': - response = await _serviceImplementation.getIsolateGroup( - params!['isolateGroupId'], - ); - break; - case 'getIsolatePauseEvent': - response = await _serviceImplementation.getIsolatePauseEvent( - params!['isolateId'], - ); - break; - case 'getMemoryUsage': - response = await _serviceImplementation.getMemoryUsage( - params!['isolateId'], - ); - break; - case 'getIsolateGroupMemoryUsage': - response = await _serviceImplementation.getIsolateGroupMemoryUsage( - params!['isolateGroupId'], - ); - break; - case 'getScripts': - response = await _serviceImplementation.getScripts( - params!['isolateId'], - ); - break; - case 'getObject': - response = await _serviceImplementation.getObject( - params!['isolateId'], - params['objectId'], - offset: params['offset'], - count: params['count'], - ); - break; - case 'getPerfettoCpuSamples': - response = await _serviceImplementation.getPerfettoCpuSamples( - params!['isolateId'], - timeOriginMicros: params['timeOriginMicros'], - timeExtentMicros: params['timeExtentMicros'], - ); - break; - case 'getPerfettoVMTimeline': - response = await _serviceImplementation.getPerfettoVMTimeline( - timeOriginMicros: params!['timeOriginMicros'], - timeExtentMicros: params['timeExtentMicros'], - ); - break; - case 'getPorts': - response = await _serviceImplementation.getPorts( - params!['isolateId'], - ); - break; - case 'getRetainingPath': - response = await _serviceImplementation.getRetainingPath( - params!['isolateId'], - params['targetId'], - params['limit'], - ); - break; - case 'getProcessMemoryUsage': - response = await _serviceImplementation.getProcessMemoryUsage(); - break; - case 'getStack': - response = await _serviceImplementation.getStack( - params!['isolateId'], - limit: params['limit'], - ); - break; - case 'getSupportedProtocols': - response = await _serviceImplementation.getSupportedProtocols(); - break; - case 'getSourceReport': - response = await _serviceImplementation.getSourceReport( - params!['isolateId'], - List.from(params['reports'] ?? []), - scriptId: params['scriptId'], - tokenPos: params['tokenPos'], - endTokenPos: params['endTokenPos'], - forceCompile: params['forceCompile'], - reportLines: params['reportLines'], - libraryFilters: params['libraryFilters'], - librariesAlreadyCompiled: params['librariesAlreadyCompiled'], - ); - break; - case 'getVersion': - response = await _serviceImplementation.getVersion(); - break; - case 'getVM': - response = await _serviceImplementation.getVM(); - break; - case 'getVMTimeline': - response = await _serviceImplementation.getVMTimeline( - timeOriginMicros: params!['timeOriginMicros'], - timeExtentMicros: params['timeExtentMicros'], - ); - break; - case 'getVMTimelineFlags': - response = await _serviceImplementation.getVMTimelineFlags(); - break; - case 'getVMTimelineMicros': - response = await _serviceImplementation.getVMTimelineMicros(); - break; - case 'pause': - response = await _serviceImplementation.pause( - params!['isolateId'], - ); - break; - case 'kill': - response = await _serviceImplementation.kill( - params!['isolateId'], - ); - break; - case 'lookupResolvedPackageUris': - response = await _serviceImplementation.lookupResolvedPackageUris( - params!['isolateId'], - List.from(params['uris'] ?? []), - local: params['local'], - ); - break; - case 'lookupPackageUris': - response = await _serviceImplementation.lookupPackageUris( - params!['isolateId'], - List.from(params['uris'] ?? []), - ); - break; - case 'reloadSources': - response = await _serviceImplementation.reloadSources( - params!['isolateId'], - force: params['force'], - pause: params['pause'], - rootLibUri: params['rootLibUri'], - packagesUri: params['packagesUri'], - ); - break; - case 'removeBreakpoint': - response = await _serviceImplementation.removeBreakpoint( - params!['isolateId'], - params['breakpointId'], - ); - break; - case 'requestHeapSnapshot': - response = await _serviceImplementation.requestHeapSnapshot( - params!['isolateId'], - ); - break; - case 'resume': - response = await _serviceImplementation.resume( - params!['isolateId'], - step: params['step'], - frameIndex: params['frameIndex'], - ); - break; - case 'setBreakpointState': - response = await _serviceImplementation.setBreakpointState( - params!['isolateId'], - params['breakpointId'], - params['enable'], - ); - break; - case 'setExceptionPauseMode': - // ignore: deprecated_member_use_from_same_package - response = await _serviceImplementation.setExceptionPauseMode( - params!['isolateId'], - params['mode'], - ); - break; - case 'setIsolatePauseMode': - response = await _serviceImplementation.setIsolatePauseMode( - params!['isolateId'], - exceptionPauseMode: params['exceptionPauseMode'], - shouldPauseOnExit: params['shouldPauseOnExit'], - ); - break; - case 'setFlag': - response = await _serviceImplementation.setFlag( - params!['name'], - params['value'], - ); - break; - case 'setLibraryDebuggable': - response = await _serviceImplementation.setLibraryDebuggable( - params!['isolateId'], - params['libraryId'], - params['isDebuggable'], - ); - break; - case 'setName': - response = await _serviceImplementation.setName( - params!['isolateId'], - params['name'], - ); - break; - case 'setTraceClassAllocation': - response = await _serviceImplementation.setTraceClassAllocation( - params!['isolateId'], - params['classId'], - params['enable'], - ); - break; - case 'setVMName': - response = await _serviceImplementation.setVMName( - params!['name'], - ); - break; - case 'setVMTimelineFlags': - response = await _serviceImplementation.setVMTimelineFlags( - List.from(params!['recordedStreams'] ?? []), - ); - break; - case 'streamCancel': - var id = params!['streamId']; - var existing = _streamSubscriptions.remove(id); - if (existing == null) { - throw RPCError.withDetails( - 'streamCancel', - 104, - 'Stream not subscribed', - details: "The stream '$id' is not subscribed", - ); - } - await existing.cancel(); - response = Success(); - break; - case 'streamCpuSamplesWithUserTag': - response = await _serviceImplementation.streamCpuSamplesWithUserTag( - List.from(params!['userTags'] ?? []), - ); - break; - case 'streamListen': - var id = params!['streamId']; - if (_streamSubscriptions.containsKey(id)) { - throw RPCError.withDetails( - 'streamListen', - 103, - 'Stream already subscribed', - details: "The stream '$id' is already subscribed", - ); - } - - var stream = id == 'Service' - ? _serviceExtensionRegistry.onExtensionEvent - : _serviceImplementation.onEvent(id); - _streamSubscriptions[id] = stream.listen((e) { - _responseSink.add({ - 'jsonrpc': '2.0', - 'method': 'streamNotify', - 'params': { - 'streamId': id, - 'event': e.toJson(), - }, - }); - }); - response = Success(); - break; - default: - 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.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); - } - } - _responseSink.add({ - 'jsonrpc': '2.0', - 'id': id, - 'result': response.toJson(), - }); - } 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, - }); - } - } -} - -class _OutstandingRequest { - _OutstandingRequest(this.method); - static int _idCounter = 0; - final String id = '${_idCounter++}'; - final String method; - final StackTrace _stackTrace = StackTrace.current; - final Completer _completer = Completer(); - - Future get future => _completer.future; - - void complete(T value) => _completer.complete(value); - void completeError(Object error) => - _completer.completeError(error, _stackTrace); -} - -typedef VmServiceFactory = T Function({ - required Stream /*String|List*/ inStream, - required void Function(String message) writeMessage, - Log? log, - DisposeHandler? disposeHandler, - Future? streamClosed, - String? wsUri, -}); - -class VmService implements VmServiceInterface { - late final StreamSubscription _streamSub; - late final Function _writeMessage; - final Map _outstandingRequests = {}; - final Map _services = {}; - late final Log _log; - - /// The web socket URI pointing to the target VM service instance. - final String? wsUri; - - Stream get onSend => _onSend.stream; - final StreamController _onSend = - StreamController.broadcast(sync: true); - - Stream get onReceive => _onReceive.stream; - final StreamController _onReceive = - StreamController.broadcast(sync: true); - - Future get onDone => _onDoneCompleter.future; - final Completer _onDoneCompleter = Completer(); - - final Map> _eventControllers = {}; - - StreamController _getEventController(String eventName) { - StreamController? controller = _eventControllers[eventName]; - if (controller == null) { - controller = StreamController.broadcast(); - _eventControllers[eventName] = controller; - } - return controller; - } - - late final DisposeHandler? _disposeHandler; - - VmService( - Stream /*String|List*/ 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 /*String|List*/ 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, - ); - } - - @override - Stream onEvent(String streamId) => - _getEventController(streamId).stream; - - // VMUpdate, VMFlagUpdate - Stream get onVMEvent => _getEventController('VM').stream; - - // IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, IsolateReload, ServiceExtensionAdded - Stream get onIsolateEvent => _getEventController('Isolate').stream; - - // PauseStart, PauseExit, PauseBreakpoint, PauseInterrupted, PauseException, PausePostRequest, Resume, BreakpointAdded, BreakpointResolved, BreakpointRemoved, BreakpointUpdated, Inspect, None - Stream get onDebugEvent => _getEventController('Debug').stream; - - // CpuSamples, UserTagChanged - Stream get onProfilerEvent => _getEventController('Profiler').stream; - - // GC - Stream get onGCEvent => _getEventController('GC').stream; - - // Extension - Stream get onExtensionEvent => _getEventController('Extension').stream; - - // TimelineEvents, TimelineStreamsSubscriptionUpdate - Stream get onTimelineEvent => _getEventController('Timeline').stream; - - // Logging - Stream get onLoggingEvent => _getEventController('Logging').stream; - - // ServiceRegistered, ServiceUnregistered - Stream get onServiceEvent => _getEventController('Service').stream; - - // HeapSnapshot - Stream get onHeapSnapshotEvent => - _getEventController('HeapSnapshot').stream; - - // WriteEvent - Stream get onStdoutEvent => _getEventController('Stdout').stream; - - // WriteEvent - Stream get onStderrEvent => _getEventController('Stderr').stream; - - @override - Future addBreakpoint( - String isolateId, - String scriptId, - int line, { - int? column, - }) => - _call('addBreakpoint', { - 'isolateId': isolateId, - 'scriptId': scriptId, - 'line': line, - if (column != null) 'column': column, - }); - - @override - Future addBreakpointWithScriptUri( - String isolateId, - String scriptUri, - int line, { - int? column, - }) => - _call('addBreakpointWithScriptUri', { - 'isolateId': isolateId, - 'scriptUri': scriptUri, - 'line': line, - if (column != null) 'column': column, - }); - - @override - Future addBreakpointAtEntry( - String isolateId, String functionId) => - _call('addBreakpointAtEntry', - {'isolateId': isolateId, 'functionId': functionId}); - - @override - Future clearCpuSamples(String isolateId) => - _call('clearCpuSamples', {'isolateId': isolateId}); - - @override - Future clearVMTimeline() => _call('clearVMTimeline'); - - @override - Future invoke( - String isolateId, - String targetId, - String selector, - List argumentIds, { - bool? disableBreakpoints, - }) => - _call('invoke', { - 'isolateId': isolateId, - 'targetId': targetId, - 'selector': selector, - 'argumentIds': argumentIds, - if (disableBreakpoints != null) - 'disableBreakpoints': disableBreakpoints, - }); - - @override - Future evaluate( - String isolateId, - String targetId, - String expression, { - Map? scope, - bool? disableBreakpoints, - }) => - _call('evaluate', { - 'isolateId': isolateId, - 'targetId': targetId, - 'expression': expression, - if (scope != null) 'scope': scope, - if (disableBreakpoints != null) - 'disableBreakpoints': disableBreakpoints, - }); - - @override - Future evaluateInFrame( - String isolateId, - int frameIndex, - String expression, { - Map? scope, - bool? disableBreakpoints, - }) => - _call('evaluateInFrame', { - 'isolateId': isolateId, - 'frameIndex': frameIndex, - 'expression': expression, - if (scope != null) 'scope': scope, - if (disableBreakpoints != null) - 'disableBreakpoints': disableBreakpoints, - }); - - @override - Future getAllocationProfile(String isolateId, - {bool? reset, bool? gc}) => - _call('getAllocationProfile', { - 'isolateId': isolateId, - if (reset != null && reset) 'reset': reset, - if (gc != null && gc) 'gc': gc, - }); - - @override - Future getAllocationTraces( - String isolateId, { - int? timeOriginMicros, - int? timeExtentMicros, - String? classId, - }) => - _call('getAllocationTraces', { - 'isolateId': isolateId, - if (timeOriginMicros != null) 'timeOriginMicros': timeOriginMicros, - if (timeExtentMicros != null) 'timeExtentMicros': timeExtentMicros, - if (classId != null) 'classId': classId, - }); - - @override - Future getClassList(String isolateId) => - _call('getClassList', {'isolateId': isolateId}); - - @override - Future getCpuSamples( - String isolateId, int timeOriginMicros, int timeExtentMicros) => - _call('getCpuSamples', { - 'isolateId': isolateId, - 'timeOriginMicros': timeOriginMicros, - 'timeExtentMicros': timeExtentMicros - }); - - @override - Future getFlagList() => _call('getFlagList'); - - @override - Future getInboundReferences( - String isolateId, String targetId, int limit) => - _call('getInboundReferences', - {'isolateId': isolateId, 'targetId': targetId, 'limit': limit}); - - @override - Future getInstances( - String isolateId, - String objectId, - int limit, { - bool? includeSubclasses, - bool? includeImplementers, - }) => - _call('getInstances', { - 'isolateId': isolateId, - 'objectId': objectId, - 'limit': limit, - if (includeSubclasses != null) 'includeSubclasses': includeSubclasses, - if (includeImplementers != null) - 'includeImplementers': includeImplementers, - }); - - @override - Future getInstancesAsList( - String isolateId, - String objectId, { - bool? includeSubclasses, - bool? includeImplementers, - }) => - _call('getInstancesAsList', { - 'isolateId': isolateId, - 'objectId': objectId, - if (includeSubclasses != null) 'includeSubclasses': includeSubclasses, - if (includeImplementers != null) - 'includeImplementers': includeImplementers, - }); - - @override - Future getIsolate(String isolateId) => - _call('getIsolate', {'isolateId': isolateId}); - - @override - Future getIsolateGroup(String isolateGroupId) => - _call('getIsolateGroup', {'isolateGroupId': isolateGroupId}); - - @override - Future getIsolatePauseEvent(String isolateId) => - _call('getIsolatePauseEvent', {'isolateId': isolateId}); - - @override - Future getMemoryUsage(String isolateId) => - _call('getMemoryUsage', {'isolateId': isolateId}); - - @override - Future getIsolateGroupMemoryUsage(String isolateGroupId) => - _call('getIsolateGroupMemoryUsage', {'isolateGroupId': isolateGroupId}); - - @override - Future getScripts(String isolateId) => - _call('getScripts', {'isolateId': isolateId}); - - @override - Future getObject( - String isolateId, - String objectId, { - int? offset, - int? count, - }) => - _call('getObject', { - 'isolateId': isolateId, - 'objectId': objectId, - if (offset != null) 'offset': offset, - if (count != null) 'count': count, - }); - - @override - Future getPerfettoCpuSamples(String isolateId, - {int? timeOriginMicros, int? timeExtentMicros}) => - _call('getPerfettoCpuSamples', { - 'isolateId': isolateId, - if (timeOriginMicros != null) 'timeOriginMicros': timeOriginMicros, - if (timeExtentMicros != null) 'timeExtentMicros': timeExtentMicros, - }); - - @override - Future getPerfettoVMTimeline( - {int? timeOriginMicros, int? timeExtentMicros}) => - _call('getPerfettoVMTimeline', { - if (timeOriginMicros != null) 'timeOriginMicros': timeOriginMicros, - if (timeExtentMicros != null) 'timeExtentMicros': timeExtentMicros, - }); - - @override - Future getPorts(String isolateId) => - _call('getPorts', {'isolateId': isolateId}); - - @override - Future getRetainingPath( - String isolateId, String targetId, int limit) => - _call('getRetainingPath', - {'isolateId': isolateId, 'targetId': targetId, 'limit': limit}); - - @override - Future getProcessMemoryUsage() => - _call('getProcessMemoryUsage'); - - @override - Future getStack(String isolateId, {int? limit}) => _call('getStack', { - 'isolateId': isolateId, - if (limit != null) 'limit': limit, - }); - - @override - Future getSupportedProtocols() => - _call('getSupportedProtocols'); - - @override - Future getSourceReport( - String isolateId, - /*List*/ List reports, { - String? scriptId, - int? tokenPos, - int? endTokenPos, - bool? forceCompile, - bool? reportLines, - List? libraryFilters, - List? librariesAlreadyCompiled, - }) => - _call('getSourceReport', { - 'isolateId': isolateId, - 'reports': reports, - if (scriptId != null) 'scriptId': scriptId, - if (tokenPos != null) 'tokenPos': tokenPos, - if (endTokenPos != null) 'endTokenPos': endTokenPos, - if (forceCompile != null) 'forceCompile': forceCompile, - if (reportLines != null) 'reportLines': reportLines, - if (libraryFilters != null) 'libraryFilters': libraryFilters, - if (librariesAlreadyCompiled != null) - 'librariesAlreadyCompiled': librariesAlreadyCompiled, - }); - - @override - Future getVersion() => _call('getVersion'); - - @override - Future getVM() => _call('getVM'); - - @override - Future getVMTimeline( - {int? timeOriginMicros, int? timeExtentMicros}) => - _call('getVMTimeline', { - if (timeOriginMicros != null) 'timeOriginMicros': timeOriginMicros, - if (timeExtentMicros != null) 'timeExtentMicros': timeExtentMicros, - }); - - @override - Future getVMTimelineFlags() => _call('getVMTimelineFlags'); - - @override - Future getVMTimelineMicros() => _call('getVMTimelineMicros'); - - @override - Future pause(String isolateId) => - _call('pause', {'isolateId': isolateId}); - - @override - Future kill(String isolateId) => - _call('kill', {'isolateId': isolateId}); - - @override - Future lookupResolvedPackageUris(String isolateId, List uris, - {bool? local}) => - _call('lookupResolvedPackageUris', { - 'isolateId': isolateId, - 'uris': uris, - if (local != null) 'local': local, - }); - - @override - Future lookupPackageUris(String isolateId, List uris) => - _call('lookupPackageUris', {'isolateId': isolateId, 'uris': uris}); - - @override - Future registerService(String service, String alias) => - _call('registerService', {'service': service, 'alias': alias}); - - @override - Future reloadSources( - String isolateId, { - bool? force, - bool? pause, - String? rootLibUri, - String? packagesUri, - }) => - _call('reloadSources', { - 'isolateId': isolateId, - if (force != null) 'force': force, - if (pause != null) 'pause': pause, - if (rootLibUri != null) 'rootLibUri': rootLibUri, - if (packagesUri != null) 'packagesUri': packagesUri, - }); - - @override - Future removeBreakpoint(String isolateId, String breakpointId) => - _call('removeBreakpoint', - {'isolateId': isolateId, 'breakpointId': breakpointId}); - - @override - Future requestHeapSnapshot(String isolateId) => - _call('requestHeapSnapshot', {'isolateId': isolateId}); - - @override - Future resume(String isolateId, - {/*StepOption*/ String? step, int? frameIndex}) => - _call('resume', { - 'isolateId': isolateId, - if (step != null) 'step': step, - if (frameIndex != null) 'frameIndex': frameIndex, - }); - - @override - Future setBreakpointState( - String isolateId, String breakpointId, bool enable) => - _call('setBreakpointState', { - 'isolateId': isolateId, - 'breakpointId': breakpointId, - 'enable': enable - }); - - @Deprecated('Use setIsolatePauseMode instead') - @override - Future setExceptionPauseMode( - String isolateId, /*ExceptionPauseMode*/ String mode) => - _call('setExceptionPauseMode', {'isolateId': isolateId, 'mode': mode}); - - @override - Future setIsolatePauseMode(String isolateId, - {/*ExceptionPauseMode*/ String? exceptionPauseMode, - bool? shouldPauseOnExit}) => - _call('setIsolatePauseMode', { - 'isolateId': isolateId, - if (exceptionPauseMode != null) - 'exceptionPauseMode': exceptionPauseMode, - if (shouldPauseOnExit != null) 'shouldPauseOnExit': shouldPauseOnExit, - }); - - @override - Future setFlag(String name, String value) => - _call('setFlag', {'name': name, 'value': value}); - - @override - Future setLibraryDebuggable( - String isolateId, String libraryId, bool isDebuggable) => - _call('setLibraryDebuggable', { - 'isolateId': isolateId, - 'libraryId': libraryId, - 'isDebuggable': isDebuggable - }); - - @override - Future setName(String isolateId, String name) => - _call('setName', {'isolateId': isolateId, 'name': name}); - - @override - Future setTraceClassAllocation( - String isolateId, String classId, bool enable) => - _call('setTraceClassAllocation', - {'isolateId': isolateId, 'classId': classId, 'enable': enable}); - - @override - Future setVMName(String name) => _call('setVMName', {'name': name}); - - @override - Future setVMTimelineFlags(List recordedStreams) => - _call('setVMTimelineFlags', {'recordedStreams': recordedStreams}); - - @override - Future streamCancel(String streamId) => - _call('streamCancel', {'streamId': streamId}); - - @override - Future streamCpuSamplesWithUserTag(List userTags) => - _call('streamCpuSamplesWithUserTag', {'userTags': userTags}); - - @override Future streamListen(String streamId) => _call('streamListen', {'streamId': streamId}); @@ -2501,7 +1719,6 @@ class VmService implements VmServiceInterface { /// Invoke a specific service protocol extension method. /// /// See https://api.dart.dev/stable/dart-developer/dart-developer-library.html. - @override Future callServiceExtension(String method, {String? isolateId, Map? args}) { if (args == null && isolateId == null) { @@ -2577,11 +1794,10 @@ class VmService implements VmServiceInterface { void _processMessage(dynamic message) { // Expect a String, an int[], or a ByteData. - if (message is String) { _processMessageStr(message); } else if (message is List) { - Uint8List list = Uint8List.fromList(message); + final list = Uint8List.fromList(message); _processMessageByteData(ByteData.view(list.buffer)); } else if (message is ByteData) { _processMessageByteData(message); @@ -2599,10 +1815,10 @@ class VmService implements VmServiceInterface { bytes.buffer, bytes.offsetInBytes + metaOffset, metaLength)); final data = ByteData.view( bytes.buffer, bytes.offsetInBytes + dataOffset, dataLength); - dynamic map = jsonDecode(meta)!; + final map = jsonDecode(meta)!; if (map['method'] == 'streamNotify') { - String streamId = map['params']['streamId']; - Map event = map['params']['event']; + final streamId = map['params']['streamId']; + final event = map['params']['event']; event['data'] = data; _getEventController(streamId) .add(createServiceObject(event, const ['Event'])! as Event); @@ -2610,27 +1826,25 @@ class VmService implements VmServiceInterface { } void _processMessageStr(String message) { - late Map json; try { _onReceive.add(message); - json = jsonDecode(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; } - - 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'); - } } void _processResponse(Map json) { @@ -2640,34 +1854,34 @@ class VmService implements VmServiceInterface { } else if (json['error'] != null) { request.completeError(RPCError.parse(request.method, json['error'])); } else { - Map result = json['result'] as Map; - String? type = result['type']; + final result = json['result'] as Map; + 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 { - List returnTypes = _methodReturnTypes[request.method] ?? []; + final returnTypes = _methodReturnTypes[request.method] ?? []; request.complete(createServiceObject(result, returnTypes)); } } } Future _processRequest(Map json) async { - final Map m = await _routeRequest( + final result = await _routeRequest( json['method'], json['params'] ?? {}); - m['id'] = json['id']; - m['jsonrpc'] = '2.0'; - String message = jsonEncode(m); + result['id'] = json['id']; + result['jsonrpc'] = '2.0'; + String message = jsonEncode(result); _onSend.add(message); _writeMessage(message); } Future _processNotification(Map json) async { - final String method = json['method']; - final Map params = json['params'] ?? {}; + final method = json['method']; + final params = json['params'] ?? {}; if (method == 'streamNotify') { - String streamId = params['streamId']; + final streamId = params['streamId']; _getEventController(streamId) .add(createServiceObject(params['event'], const ['Event'])! as Event); } else { @@ -2678,7 +1892,7 @@ class VmService implements VmServiceInterface { Future _routeRequest(String method, Map params) async { final service = _services[method]; if (service == null) { - RPCError error = RPCError(method, RPCErrorKind.kMethodNotFound.code, + final error = RPCError(method, RPCErrorKind.kMethodNotFound.code, 'method not found \'$method\''); return {'error': error.toMap()}; } @@ -2804,7 +2018,7 @@ class RPCError implements Exception { /// Return a map representation of this error suitable for conversion to /// json. Map toMap() { - Map map = { + final map = { 'code': code, 'message': message, }; @@ -2843,7 +2057,7 @@ class ExtensionData { final Map data; - ExtensionData() : data = {}; + ExtensionData() : data = {}; ExtensionData._fromJson(this.data); @@ -2867,6 +2081,7 @@ class _NullLog implements Log { @override void severe(String message) {} } + // enums abstract class CodeKind { @@ -4042,7 +3257,7 @@ class ContextElement { String toString() => '[ContextElement value: $value]'; } -/// See [VmServiceInterface.getCpuSamples] and [CpuSample]. +/// See [VmService.getCpuSamples] and [CpuSample]. class CpuSamples extends Response { static CpuSamples? parse(Map? json) => json == null ? null : CpuSamples._fromJson(json); @@ -4207,7 +3422,7 @@ class CpuSamplesEvent { 'sampleCount: $sampleCount, timeOriginMicros: $timeOriginMicros, timeExtentMicros: $timeExtentMicros, pid: $pid, functions: $functions, samples: $samples]'; } -/// See [VmServiceInterface.getCpuSamples] and [CpuSamples]. +/// See [VmService.getCpuSamples] and [CpuSamples]. class CpuSample { static CpuSample? parse(Map? json) => json == null ? null : CpuSample._fromJson(json); @@ -6545,7 +5760,7 @@ class IsolateGroup extends Response implements IsolateGroupRef { 'isolates: $isolates]'; } -/// See [VmServiceInterface.getInboundReferences]. +/// See [VmService.getInboundReferences]. class InboundReferences extends Response { static InboundReferences? parse(Map? json) => json == null ? null : InboundReferences._fromJson(json); @@ -6582,7 +5797,7 @@ class InboundReferences extends Response { String toString() => '[InboundReferences references: $references]'; } -/// See [VmServiceInterface.getInboundReferences]. +/// See [VmService.getInboundReferences]. class InboundReference { static InboundReference? parse(Map? json) => json == null ? null : InboundReference._fromJson(json); @@ -6641,7 +5856,7 @@ class InboundReference { String toString() => '[InboundReference source: $source]'; } -/// See [VmServiceInterface.getInstances]. +/// See [VmService.getInstances]. class InstanceSet extends Response { static InstanceSet? parse(Map? json) => json == null ? null : InstanceSet._fromJson(json); @@ -6733,7 +5948,7 @@ class LibraryRef extends ObjRef { /// A `Library` provides information about a Dart language library. /// -/// See [VmServiceInterface.setLibraryDebuggable]. +/// See [VmService.setLibraryDebuggable]. class Library extends Obj implements LibraryRef { static Library? parse(Map? json) => json == null ? null : Library._fromJson(json); @@ -7460,7 +6675,7 @@ class Parameter { '[Parameter parameterType: $parameterType, fixed: $fixed]'; } -/// See [VmServiceInterface.getPerfettoCpuSamples]. +/// See [VmService.getPerfettoCpuSamples]. class PerfettoCpuSamples extends Response { static PerfettoCpuSamples? parse(Map? json) => json == null ? null : PerfettoCpuSamples._fromJson(json); @@ -7534,7 +6749,7 @@ class PerfettoCpuSamples extends Response { 'sampleCount: $sampleCount, timeOriginMicros: $timeOriginMicros, timeExtentMicros: $timeExtentMicros, pid: $pid, samples: $samples]'; } -/// See [VmServiceInterface.getPerfettoVMTimeline]; +/// See [VmService.getPerfettoVMTimeline]; class PerfettoTimeline extends Response { static PerfettoTimeline? parse(Map? json) => json == null ? null : PerfettoTimeline._fromJson(json); @@ -7584,7 +6799,7 @@ class PerfettoTimeline extends Response { /// A `PortList` contains a list of ports associated with some isolate. /// -/// See [VmServiceInterface.getPorts]. +/// See [VmService.getPorts]. class PortList extends Response { static PortList? parse(Map? json) => json == null ? null : PortList._fromJson(json); @@ -7680,7 +6895,7 @@ class ProfileFunction { /// A `ProtocolList` contains a list of all protocols supported by the service /// instance. /// -/// See [Protocol] and [VmServiceInterface.getSupportedProtocols]. +/// See [Protocol] and [VmService.getSupportedProtocols]. class ProtocolList extends Response { static ProtocolList? parse(Map? json) => json == null ? null : ProtocolList._fromJson(json); @@ -7715,7 +6930,7 @@ class ProtocolList extends Response { String toString() => '[ProtocolList protocols: $protocols]'; } -/// See [VmServiceInterface.getSupportedProtocols]. +/// See [VmService.getSupportedProtocols]. class Protocol { static Protocol? parse(Map? json) => json == null ? null : Protocol._fromJson(json); @@ -7756,7 +6971,7 @@ class Protocol { '[Protocol protocolName: $protocolName, major: $major, minor: $minor]'; } -/// See [VmServiceInterface.getProcessMemoryUsage]. +/// See [VmService.getProcessMemoryUsage]. class ProcessMemoryUsage extends Response { static ProcessMemoryUsage? parse(Map? json) => json == null ? null : ProcessMemoryUsage._fromJson(json); @@ -7931,7 +7146,7 @@ class RetainingObject { String toString() => '[RetainingObject value: $value]'; } -/// See [VmServiceInterface.getRetainingPath]. +/// See [VmService.getRetainingPath]. class RetainingPath extends Response { static RetainingPath? parse(Map? json) => json == null ? null : RetainingPath._fromJson(json); @@ -8517,7 +7732,7 @@ class SourceReportRange { /// The `Stack` class represents the various components of a Dart stack trace /// for a given isolate. /// -/// See [VmServiceInterface.getStack]. +/// See [VmService.getStack]. class Stack extends Response { static Stack? parse(Map? json) => json == null ? null : Stack._fromJson(json); @@ -8634,7 +7849,7 @@ class Success extends Response { String toString() => '[Success]'; } -/// See [VmServiceInterface.getVMTimeline]; +/// See [VmService.getVMTimeline]; class Timeline extends Response { static Timeline? parse(Map? json) => json == null ? null : Timeline._fromJson(json); diff --git a/pkg/vm_service/lib/src/vm_service_interface.dart b/pkg/vm_service/lib/src/vm_service_interface.dart new file mode 100644 index 00000000000..699192ed760 --- /dev/null +++ b/pkg/vm_service/lib/src/vm_service_interface.dart @@ -0,0 +1,1694 @@ +// 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; + +const String vmServiceVersion = '4.13.0'; + +/// 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 onEvent(String streamId); + + /// Handler for calling extra service extensions. + Future callServiceExtension(String method, + {String? isolateId, Map? args}); + + /// The `addBreakpoint` RPC is used to add a breakpoint at a specific line of + /// some script. + /// + /// The `scriptId` parameter is used to specify the target script. + /// + /// The `line` parameter is used to specify the target line for the + /// breakpoint. If there are multiple possible breakpoints on the target line, + /// then the VM will place the breakpoint at the location which would execute + /// soonest. If it is not possible to set a breakpoint at the target line, the + /// breakpoint will be added at the next possible breakpoint location within + /// the same function. + /// + /// The `column` parameter may be optionally specified. This is useful for + /// targeting a specific breakpoint on a line with multiple possible + /// breakpoints. + /// + /// If no breakpoint is possible at that line, the `102` (Cannot add + /// breakpoint) RPC error code is returned. + /// + /// Note that breakpoints are added and removed on a per-isolate basis. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Breakpoint]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future addBreakpoint( + String isolateId, + String scriptId, + int line, { + int? column, + }); + + /// The `addBreakpoint` RPC is used to add a breakpoint at a specific line of + /// some script. This RPC is useful when a script has not yet been assigned an + /// id, for example, if a script is in a deferred library which has not yet + /// been loaded. + /// + /// The `scriptUri` parameter is used to specify the target script. + /// + /// The `line` parameter is used to specify the target line for the + /// breakpoint. If there are multiple possible breakpoints on the target line, + /// then the VM will place the breakpoint at the location which would execute + /// soonest. If it is not possible to set a breakpoint at the target line, the + /// breakpoint will be added at the next possible breakpoint location within + /// the same function. + /// + /// The `column` parameter may be optionally specified. This is useful for + /// targeting a specific breakpoint on a line with multiple possible + /// breakpoints. + /// + /// If no breakpoint is possible at that line, the `102` (Cannot add + /// breakpoint) RPC error code is returned. + /// + /// Note that breakpoints are added and removed on a per-isolate basis. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Breakpoint]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future addBreakpointWithScriptUri( + String isolateId, + String scriptUri, + int line, { + int? column, + }); + + /// The `addBreakpointAtEntry` RPC is used to add a breakpoint at the + /// entrypoint of some function. + /// + /// If no breakpoint is possible at the function entry, the `102` (Cannot add + /// breakpoint) RPC error code is returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Breakpoint]. + /// + /// Note that breakpoints are added and removed on a per-isolate basis. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future addBreakpointAtEntry(String isolateId, String functionId); + + /// Clears all CPU profiling samples. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future clearCpuSamples(String isolateId); + + /// Clears all VM timeline events. + /// + /// See [Success]. + Future clearVMTimeline(); + + /// The `invoke` RPC is used to perform regular method invocation on some + /// receiver, as if by dart:mirror's ObjectMirror.invoke. Note this does not + /// provide a way to perform getter, setter or constructor invocation. + /// + /// `targetId` may refer to a [Library], [Class], or [Instance]. + /// + /// Each elements of `argumentId` may refer to an [Instance]. + /// + /// If `disableBreakpoints` is provided and set to true, any breakpoints hit + /// as a result of this invocation are ignored, including pauses resulting + /// from a call to `debugger()` from `dart:developer`. Defaults to false if + /// not provided. + /// + /// If `targetId` or any element of `argumentIds` is a temporary id which has + /// expired, then the `Expired` [Sentinel] is returned. + /// + /// If `targetId` or any element of `argumentIds` refers to an object which + /// has been collected by the VM's garbage collector, then the `Collected` + /// [Sentinel] is returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// If invocation triggers a failed compilation then [RPCError] 113 + /// "Expression compilation error" is returned. + /// + /// If a runtime error occurs while evaluating the invocation, an [ErrorRef] + /// reference will be returned. + /// + /// If the invocation is evaluated successfully, an [InstanceRef] reference + /// will be returned. + /// + /// The return value can be one of [InstanceRef] or [ErrorRef]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future invoke( + String isolateId, + String targetId, + String selector, + List argumentIds, { + bool? disableBreakpoints, + }); + + /// The `evaluate` RPC is used to evaluate an expression in the context of + /// some target. + /// + /// `targetId` may refer to a [Library], [Class], or [Instance]. + /// + /// If `targetId` is a temporary id which has expired, then the `Expired` + /// [Sentinel] is returned. + /// + /// If `targetId` refers to an object which has been collected by the VM's + /// garbage collector, then the `Collected` [Sentinel] is returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// If `scope` is provided, it should be a map from identifiers to object ids. + /// These bindings will be added to the scope in which the expression is + /// evaluated, which is a child scope of the class or library for + /// instance/class or library targets respectively. This means bindings + /// provided in `scope` may shadow instance members, class members and + /// top-level members. + /// + /// If `disableBreakpoints` is provided and set to true, any breakpoints hit + /// as a result of this evaluation are ignored. Defaults to false if not + /// provided. + /// + /// If the expression fails to parse and compile, then [RPCError] 113 + /// "Expression compilation error" is returned. + /// + /// If an error occurs while evaluating the expression, an [ErrorRef] + /// reference will be returned. + /// + /// If the expression is evaluated successfully, an [InstanceRef] reference + /// will be returned. + /// + /// The return value can be one of [InstanceRef] or [ErrorRef]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future evaluate( + String isolateId, + String targetId, + String expression, { + Map? scope, + bool? disableBreakpoints, + }); + + /// The `evaluateInFrame` RPC is used to evaluate an expression in the context + /// of a particular stack frame. `frameIndex` is the index of the desired + /// [Frame], with an index of `0` indicating the top (most recent) frame. + /// + /// If `scope` is provided, it should be a map from identifiers to object ids. + /// These bindings will be added to the scope in which the expression is + /// evaluated, which is a child scope of the frame's current scope. This means + /// bindings provided in `scope` may shadow instance members, class members, + /// top-level members, parameters and locals. + /// + /// If `disableBreakpoints` is provided and set to true, any breakpoints hit + /// as a result of this evaluation are ignored. Defaults to false if not + /// provided. + /// + /// If the expression fails to parse and compile, then [RPCError] 113 + /// "Expression compilation error" is returned. + /// + /// If an error occurs while evaluating the expression, an [ErrorRef] + /// reference will be returned. + /// + /// If the expression is evaluated successfully, an [InstanceRef] reference + /// will be returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// The return value can be one of [InstanceRef] or [ErrorRef]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future evaluateInFrame( + String isolateId, + int frameIndex, + String expression, { + Map? scope, + bool? disableBreakpoints, + }); + + /// The `getAllocationProfile` RPC is used to retrieve allocation information + /// for a given isolate. + /// + /// If `reset` is provided and is set to true, the allocation accumulators + /// will be reset before collecting allocation information. + /// + /// If `gc` is provided and is set to true, a garbage collection will be + /// attempted before collecting allocation information. There is no guarantee + /// that a garbage collection will be actually be performed. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getAllocationProfile(String isolateId, + {bool? reset, bool? gc}); + + /// The `getAllocationTraces` RPC allows for the retrieval of allocation + /// traces for objects of a specific set of types (see + /// [VmServiceInterface.setTraceClassAllocation]). Only samples collected in + /// the time range `[timeOriginMicros, timeOriginMicros + timeExtentMicros]` + /// will be reported. + /// + /// If `classId` is provided, only traces for allocations with the matching + /// `classId` will be reported. + /// + /// If the profiler is disabled, an RPC error response will be returned. + /// + /// If isolateId refers to an isolate which has exited, then the Collected + /// Sentinel is returned. + /// + /// See [CpuSamples]. + Future getAllocationTraces( + String isolateId, { + int? timeOriginMicros, + int? timeExtentMicros, + String? classId, + }); + + /// The `getClassList` RPC is used to retrieve a `ClassList` containing all + /// classes for an isolate based on the isolate's `isolateId`. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [ClassList]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getClassList(String isolateId); + + /// The `getCpuSamples` RPC is used to retrieve samples collected by the CPU + /// profiler. See [CpuSamples] for a detailed description of the response. + /// + /// The `timeOriginMicros` parameter is the beginning of the time range used + /// to filter samples. It uses the same monotonic clock as dart:developer's + /// `Timeline.now` and the VM embedding API's `Dart_TimelineGetMicros`. See + /// [VmServiceInterface.getVMTimelineMicros] for access to this clock through + /// the service protocol. + /// + /// The `timeExtentMicros` parameter specifies how large the time range used + /// to filter samples should be. + /// + /// For example, given `timeOriginMicros` and `timeExtentMicros`, only samples + /// from the following time range will be returned: `(timeOriginMicros, + /// timeOriginMicros + timeExtentMicros)`. + /// + /// If the profiler is disabled, an [RPCError] response will be returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getCpuSamples( + String isolateId, int timeOriginMicros, int timeExtentMicros); + + /// The `getFlagList` RPC returns a list of all command line flags in the VM + /// along with their current values. + /// + /// See [FlagList]. + Future getFlagList(); + + /// Returns a set of inbound references to the object specified by `targetId`. + /// Up to `limit` references will be returned. + /// + /// The order of the references is undefined (i.e., not related to allocation + /// order) and unstable (i.e., multiple invocations of this method against the + /// same object can give different answers even if no Dart code has executed + /// between the invocations). + /// + /// The references may include multiple `objectId`s that designate the same + /// object. + /// + /// The references may include objects that are unreachable but have not yet + /// been garbage collected. + /// + /// If `targetId` is a temporary id which has expired, then the `Expired` + /// [Sentinel] is returned. + /// + /// If `targetId` refers to an object which has been collected by the VM's + /// garbage collector, then the `Collected` [Sentinel] is returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [InboundReferences]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getInboundReferences( + String isolateId, String targetId, int limit); + + /// The `getInstances` RPC is used to retrieve a set of instances which are of + /// a specific class. + /// + /// The order of the instances is undefined (i.e., not related to allocation + /// order) and unstable (i.e., multiple invocations of this method against the + /// same class can give different answers even if no Dart code has executed + /// between the invocations). + /// + /// The set of instances may include objects that are unreachable but have not + /// yet been garbage collected. + /// + /// `objectId` is the ID of the `Class` to retrieve instances for. `objectId` + /// must be the ID of a `Class`, otherwise an [RPCError] is returned. + /// + /// `limit` is the maximum number of instances to be returned. + /// + /// If `includeSubclasses` is true, instances of subclasses of the specified + /// class will be included in the set. + /// + /// If `includeImplementers` is true, instances of implementers of the + /// specified class will be included in the set. Note that subclasses of a + /// class are also considered implementers of that class. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [InstanceSet]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getInstances( + String isolateId, + String objectId, + int limit, { + bool? includeSubclasses, + bool? includeImplementers, + }); + + /// The `getInstancesAsList` RPC is used to retrieve a set of instances which + /// are of a specific class. This RPC returns an `InstanceRef` corresponding + /// to a Dart `List` that contains the requested instances. This + /// `List` is not growable, but it is otherwise mutable. The response type is + /// what distinguishes this RPC from `getInstances`, which returns an + /// `InstanceSet`. + /// + /// The order of the instances is undefined (i.e., not related to allocation + /// order) and unstable (i.e., multiple invocations of this method against the + /// same class can give different answers even if no Dart code has executed + /// between the invocations). + /// + /// The set of instances may include objects that are unreachable but have not + /// yet been garbage collected. + /// + /// `objectId` is the ID of the `Class` to retrieve instances for. `objectId` + /// must be the ID of a `Class`, otherwise an [RPCError] is returned. + /// + /// If `includeSubclasses` is true, instances of subclasses of the specified + /// class will be included in the set. + /// + /// If `includeImplementers` is true, instances of implementers of the + /// specified class will be included in the set. Note that subclasses of a + /// class are also considered implementers of that class. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getInstancesAsList( + String isolateId, + String objectId, { + bool? includeSubclasses, + bool? includeImplementers, + }); + + /// The `getIsolate` RPC is used to lookup an `Isolate` object by its `id`. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Isolate]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getIsolate(String isolateId); + + /// The `getIsolateGroup` RPC is used to lookup an `IsolateGroup` object by + /// its `id`. + /// + /// If `isolateGroupId` refers to an isolate group which has exited, then the + /// `Expired` [Sentinel] is returned. + /// + /// `IsolateGroup` `id` is an opaque identifier that can be fetched from an + /// `IsolateGroup`. List of active `IsolateGroup`'s, for example, is available + /// on `VM` object. + /// + /// See [IsolateGroup], [VM]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getIsolateGroup(String isolateGroupId); + + /// The `getIsolatePauseEvent` RPC is used to lookup an isolate's pause event + /// by its `id`. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Isolate]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getIsolatePauseEvent(String isolateId); + + /// The `getMemoryUsage` RPC is used to lookup an isolate's memory usage + /// statistics by its `id`. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Isolate]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getMemoryUsage(String isolateId); + + /// The `getIsolateGroupMemoryUsage` RPC is used to lookup an isolate group's + /// memory usage statistics by its `id`. + /// + /// If `isolateGroupId` refers to an isolate group which has exited, then the + /// `Expired` [Sentinel] is returned. + /// + /// See [IsolateGroup]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getIsolateGroupMemoryUsage(String isolateGroupId); + + /// The `getScripts` RPC is used to retrieve a `ScriptList` containing all + /// scripts for an isolate based on the isolate's `isolateId`. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [ScriptList]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getScripts(String isolateId); + + /// The `getObject` RPC is used to lookup an `object` from some isolate by its + /// `id`. + /// + /// If `objectId` is a temporary id which has expired, then the `Expired` + /// [Sentinel] is returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// If `objectId` refers to a heap object which has been collected by the VM's + /// garbage collector, then the `Collected` [Sentinel] is returned. + /// + /// If `objectId` refers to a non-heap object which has been deleted, then the + /// `Collected` [Sentinel] is returned. + /// + /// If the object handle has not expired and the object has not been + /// collected, then an [Obj] will be returned. + /// + /// The `offset` and `count` parameters are used to request subranges of + /// Instance objects with the kinds: String, List, Map, Set, Uint8ClampedList, + /// Uint8List, Uint16List, Uint32List, Uint64List, Int8List, Int16List, + /// Int32List, Int64List, Float32List, Float64List, Inst32x3List, + /// Float32x4List, and Float64x2List. These parameters are otherwise ignored. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getObject( + String isolateId, + String objectId, { + int? offset, + int? count, + }); + + /// The `getPerfettoCpuSamples` RPC is used to retrieve samples collected by + /// the CPU profiler, serialized in Perfetto's proto format. See + /// [PerfettoCpuSamples] for a detailed description of the response. + /// + /// The `timeOriginMicros` parameter is the beginning of the time range used + /// to filter samples. It uses the same monotonic clock as dart:developer's + /// `Timeline.now` and the VM embedding API's `Dart_TimelineGetMicros`. See + /// [VmServiceInterface.getVMTimelineMicros] for access to this clock through + /// the service protocol. + /// + /// The `timeExtentMicros` parameter specifies how large the time range used + /// to filter samples should be. + /// + /// For example, given `timeOriginMicros` and `timeExtentMicros`, only samples + /// from the following time range will be returned: `(timeOriginMicros, + /// timeOriginMicros + timeExtentMicros)`. + /// + /// If the profiler is disabled, an [RPCError] response will be returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getPerfettoCpuSamples(String isolateId, + {int? timeOriginMicros, int? timeExtentMicros}); + + /// The `getPerfettoVMTimeline` RPC is used to retrieve an object which + /// contains a VM timeline trace represented in Perfetto's proto format. See + /// [PerfettoTimeline] for a detailed description of the response. + /// + /// The `timeOriginMicros` parameter is the beginning of the time range used + /// to filter timeline events. It uses the same monotonic clock as + /// dart:developer's `Timeline.now` and the VM embedding API's + /// `Dart_TimelineGetMicros`. See [VmServiceInterface.getVMTimelineMicros] for + /// access to this clock through the service protocol. + /// + /// The `timeExtentMicros` parameter specifies how large the time range used + /// to filter timeline events should be. + /// + /// For example, given `timeOriginMicros` and `timeExtentMicros`, only + /// timeline events from the following time range will be returned: + /// `(timeOriginMicros, timeOriginMicros + timeExtentMicros)`. + /// + /// If `getPerfettoVMTimeline` is invoked while the current recorder is + /// Callback, an [RPCError] with error code `114`, `invalid timeline request`, + /// will be returned as timeline events are handled by the embedder in this + /// mode. + /// + /// If `getPerfettoVMTimeline` is invoked while the current recorder is one of + /// Fuchsia or Macos or Systrace, an [RPCError] with error code `114`, + /// `invalid timeline request`, will be returned as timeline events are + /// handled by the OS in these modes. + /// + /// If `getPerfettoVMTimeline` is invoked while the current recorder is File + /// or Perfettofile, an [RPCError] with error code `114`, `invalid timeline + /// request`, will be returned as timeline events are written directly to a + /// file, and thus cannot be retrieved through the VM Service, in these modes. + Future getPerfettoVMTimeline( + {int? timeOriginMicros, int? timeExtentMicros}); + + /// The `getPorts` RPC is used to retrieve the list of `ReceivePort` instances + /// for a given isolate. + /// + /// See [PortList]. + Future getPorts(String isolateId); + + /// The `getRetainingPath` RPC is used to lookup a path from an object + /// specified by `targetId` to a GC root (i.e., the object which is preventing + /// this object from being garbage collected). + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// If `targetId` refers to a heap object which has been collected by the VM's + /// garbage collector, then the `Collected` [Sentinel] is returned. + /// + /// If `targetId` refers to a non-heap object which has been deleted, then the + /// `Collected` [Sentinel] is returned. + /// + /// If the object handle has not expired and the object has not been + /// collected, then an [RetainingPath] will be returned. + /// + /// The `limit` parameter specifies the maximum path length to be reported as + /// part of the retaining path. If a path is longer than `limit`, it will be + /// truncated at the root end of the path. + /// + /// See [RetainingPath]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getRetainingPath( + String isolateId, String targetId, int limit); + + /// Returns a description of major uses of memory known to the VM. + /// + /// Adding or removing buckets is considered a backwards-compatible change for + /// the purposes of versioning. A client must gracefully handle the removal or + /// addition of any bucket. + Future getProcessMemoryUsage(); + + /// The `getStack` RPC is used to retrieve the current execution stack and + /// message queue for an isolate. The isolate does not need to be paused. + /// + /// If `limit` is provided, up to `limit` frames from the top of the stack + /// will be returned. If the stack depth is smaller than `limit` the entire + /// stack is returned. Note: this limit also applies to the + /// `asyncCausalFrames` stack representation in the `Stack` response. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Stack]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getStack(String isolateId, {int? limit}); + + /// The `getSupportedProtocols` RPC is used to determine which protocols are + /// supported by the current server. + /// + /// The result of this call should be intercepted by any middleware that + /// extends the core VM service protocol and should add its own protocol to + /// the list of protocols before forwarding the response to the client. + /// + /// See [ProtocolList]. + Future getSupportedProtocols(); + + /// The `getSourceReport` RPC is used to generate a set of reports tied to + /// source locations in an isolate. + /// + /// The `reports` parameter is used to specify which reports should be + /// generated. The `reports` parameter is a list, which allows multiple + /// reports to be generated simultaneously from a consistent isolate state. + /// The `reports` parameter is allowed to be empty (this might be used to + /// force compilation of a particular subrange of some script). + /// + /// The available report kinds are: + /// + /// report kind | meaning + /// ----------- | ------- + /// Coverage | Provide code coverage information + /// PossibleBreakpoints | Provide a list of token positions which correspond + /// to possible breakpoints. + /// + /// The `scriptId` parameter is used to restrict the report to a particular + /// script. When analyzing a particular script, either or both of the + /// `tokenPos` and `endTokenPos` parameters may be provided to restrict the + /// analysis to a subrange of a script (for example, these can be used to + /// restrict the report to the range of a particular class or function). + /// + /// If the `scriptId` parameter is not provided then the reports are generated + /// for all loaded scripts and the `tokenPos` and `endTokenPos` parameters are + /// disallowed. + /// + /// The `forceCompilation` parameter can be used to force compilation of all + /// functions in the range of the report. Forcing compilation can cause a + /// compilation error, which could terminate the running Dart program. If this + /// parameter is not provided, it is considered to have the value `false`. + /// + /// The `reportLines` parameter changes the token positions in + /// `SourceReportRange.possibleBreakpoints` and `SourceReportCoverage` to be + /// line numbers. This is designed to reduce the number of RPCs that need to + /// be performed in the case that the client is only interested in line + /// numbers. If this parameter is not provided, it is considered to have the + /// value `false`. + /// + /// The `libraryFilters` parameter is intended to be used when gathering + /// coverage for the whole isolate. If it is provided, the `SourceReport` will + /// only contain results from scripts with URIs that start with one of the + /// filter strings. For example, pass `["package:foo/"]` to only include + /// scripts from the foo package. + /// + /// The `librariesAlreadyCompiled` parameter overrides the `forceCompilation` + /// parameter on a per-library basis, setting it to `false` for any libary in + /// this list. This is useful for cases where multiple `getSourceReport` RPCs + /// are sent with `forceCompilation` enabled, to avoid recompiling the same + /// libraries repeatedly. To use this parameter, enable `forceCompilation`, + /// cache the results of each `getSourceReport` RPC, and pass all the + /// libraries mentioned in the `SourceReport` to subsequent RPCs in the + /// `librariesAlreadyCompiled`. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [SourceReport]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getSourceReport( + String isolateId, + /*List*/ List reports, { + String? scriptId, + int? tokenPos, + int? endTokenPos, + bool? forceCompile, + bool? reportLines, + List? libraryFilters, + List? librariesAlreadyCompiled, + }); + + /// The `getVersion` RPC is used to determine what version of the Service + /// Protocol is served by a VM. + /// + /// See [Version]. + Future getVersion(); + + /// The `getVM` RPC returns global information about a Dart virtual machine. + /// + /// See [VM]. + Future getVM(); + + /// The `getVMTimeline` RPC is used to retrieve an object which contains VM + /// timeline events. See [Timeline] for a detailed description of the + /// response. + /// + /// The `timeOriginMicros` parameter is the beginning of the time range used + /// to filter timeline events. It uses the same monotonic clock as + /// dart:developer's `Timeline.now` and the VM embedding API's + /// `Dart_TimelineGetMicros`. See [VmServiceInterface.getVMTimelineMicros] for + /// access to this clock through the service protocol. + /// + /// The `timeExtentMicros` parameter specifies how large the time range used + /// to filter timeline events should be. + /// + /// For example, given `timeOriginMicros` and `timeExtentMicros`, only + /// timeline events from the following time range will be returned: + /// `(timeOriginMicros, timeOriginMicros + timeExtentMicros)`. + /// + /// If `getVMTimeline` is invoked while the current recorder is Callback, an + /// [RPCError] with error code `114`, `invalid timeline request`, will be + /// returned as timeline events are handled by the embedder in this mode. + /// + /// If `getVMTimeline` is invoked while the current recorder is one of Fuchsia + /// or Macos or Systrace, an [RPCError] with error code `114`, `invalid + /// timeline request`, will be returned as timeline events are handled by the + /// OS in these modes. + /// + /// If `getVMTimeline` is invoked while the current recorder is File or + /// Perfettofile, an [RPCError] with error code `114`, `invalid timeline + /// request`, will be returned as timeline events are written directly to a + /// file, and thus cannot be retrieved through the VM Service, in these modes. + Future getVMTimeline( + {int? timeOriginMicros, int? timeExtentMicros}); + + /// The `getVMTimelineFlags` RPC returns information about the current VM + /// timeline configuration. + /// + /// To change which timeline streams are currently enabled, see + /// [VmServiceInterface.setVMTimelineFlags]. + /// + /// See [TimelineFlags]. + Future getVMTimelineFlags(); + + /// The `getVMTimelineMicros` RPC returns the current time stamp from the + /// clock used by the timeline, similar to `Timeline.now` in `dart:developer` + /// and `Dart_TimelineGetMicros` in the VM embedding API. + /// + /// See [Timestamp] and [VmServiceInterface.getVMTimeline]. + Future getVMTimelineMicros(); + + /// The `pause` RPC is used to interrupt a running isolate. The RPC enqueues + /// the interrupt request and potentially returns before the isolate is + /// paused. + /// + /// When the isolate is paused an event will be sent on the `Debug` stream. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future pause(String isolateId); + + /// The `kill` RPC is used to kill an isolate as if by dart:isolate's + /// `Isolate.kill(IMMEDIATE)`. + /// + /// The isolate is killed regardless of whether it is paused or running. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future kill(String isolateId); + + /// The `lookupResolvedPackageUris` RPC is used to convert a list of URIs to + /// their resolved (or absolute) paths. For example, URIs passed to this RPC + /// are mapped in the following ways: + /// + /// - `dart:io` -> `org-dartlang-sdk:///sdk/lib/io/io.dart` + /// - `package:test/test.dart` -> + /// `file:///$PACKAGE_INSTALLATION_DIR/lib/test.dart` + /// - `file:///foo/bar/bazz.dart` -> `file:///foo/bar/bazz.dart` + /// + /// If a URI is not known, the corresponding entry in the [UriList] response + /// will be `null`. + /// + /// If `local` is true, the VM will attempt to return local file paths instead + /// of relative paths, but this is not guaranteed. + /// + /// See [UriList]. + Future lookupResolvedPackageUris(String isolateId, List uris, + {bool? local}); + + /// The `lookupPackageUris` RPC is used to convert a list of URIs to their + /// unresolved paths. For example, URIs passed to this RPC are mapped in the + /// following ways: + /// + /// - `org-dartlang-sdk:///sdk/lib/io/io.dart` -> `dart:io` + /// - `file:///$PACKAGE_INSTALLATION_DIR/lib/test.dart` -> + /// `package:test/test.dart` + /// - `file:///foo/bar/bazz.dart` -> `file:///foo/bar/bazz.dart` + /// + /// If a URI is not known, the corresponding entry in the [UriList] response + /// will be `null`. + /// + /// See [UriList]. + Future lookupPackageUris(String isolateId, List uris); + + /// Registers a service that can be invoked by other VM service clients, where + /// `service` is the name of the service to advertise and `alias` is an + /// alternative name for the registered service. + /// + /// Requests made to the new service will be forwarded to the client which + /// originally registered the service. + /// + /// See [Success]. + Future registerService(String service, String alias); + + /// The `reloadSources` RPC is used to perform a hot reload of the sources of + /// all isolates in the same isolate group as the isolate specified by + /// `isolateId`. + /// + /// If the `force` parameter is provided, it indicates that all sources should + /// be reloaded regardless of modification time. + /// + /// The `pause` parameter has been deprecated, so providing it no longer has + /// any effect. + /// + /// If the `rootLibUri` parameter is provided, it indicates the new uri to the + /// isolate group's root library. + /// + /// If the `packagesUri` parameter is provided, it indicates the new uri to + /// the isolate group's package map (.packages) file. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future reloadSources( + String isolateId, { + bool? force, + bool? pause, + String? rootLibUri, + String? packagesUri, + }); + + /// The `removeBreakpoint` RPC is used to remove a breakpoint by its `id`. + /// + /// Note that breakpoints are added and removed on a per-isolate basis. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future removeBreakpoint(String isolateId, String breakpointId); + + /// Requests a dump of the Dart heap of the given isolate. + /// + /// This method immediately returns success. The VM will then begin delivering + /// binary events on the `HeapSnapshot` event stream. The binary data in these + /// events, when concatenated together, conforms to the [HeapSnapshotGraph] + /// type. The splitting of the SnapshotGraph into events can happen at any + /// byte offset. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future requestHeapSnapshot(String isolateId); + + /// The `resume` RPC is used to resume execution of a paused isolate. + /// + /// If the `step` parameter is not provided, the program will resume regular + /// execution. + /// + /// If the `step` parameter is provided, it indicates what form of + /// single-stepping to use. + /// + /// step | meaning + /// ---- | ------- + /// Into | Single step, entering function calls + /// Over | Single step, skipping over function calls + /// Out | Single step until the current function exits + /// Rewind | Immediately exit the top frame(s) without executing any code. + /// Isolate will be paused at the call of the last exited function. + /// + /// The `frameIndex` parameter is only used when the `step` parameter is + /// Rewind. It specifies the stack frame to rewind to. Stack frame 0 is the + /// currently executing function, so `frameIndex` must be at least 1. + /// + /// If the `frameIndex` parameter is not provided, it defaults to 1. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success], [StepOption]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future resume(String isolateId, + {/*StepOption*/ String? step, int? frameIndex}); + + /// The `setBreakpointState` RPC allows for breakpoints to be enabled or + /// disabled, without requiring for the breakpoint to be completely removed. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// The returned [Breakpoint] is the updated breakpoint with its new values. + /// + /// See [Breakpoint]. + Future setBreakpointState( + String isolateId, String breakpointId, bool enable); + + /// The `setExceptionPauseMode` RPC is used to control if an isolate pauses + /// when an exception is thrown. + /// + /// mode | meaning + /// ---- | ------- + /// None | Do not pause isolate on thrown exceptions + /// Unhandled | Pause isolate on unhandled exceptions + /// All | Pause isolate on all thrown exceptions + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + @Deprecated('Use setIsolatePauseMode instead') + Future setExceptionPauseMode( + String isolateId, /*ExceptionPauseMode*/ String mode); + + /// The `setIsolatePauseMode` RPC is used to control if or when an isolate + /// will pause due to a change in execution state. + /// + /// The `shouldPauseOnExit` parameter specify whether the target isolate + /// should pause on exit. + /// + /// mode | meaning + /// ---- | ------- + /// None | Do not pause isolate on thrown exceptions + /// Unhandled | Pause isolate on unhandled exceptions + /// All | Pause isolate on all thrown exceptions + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future setIsolatePauseMode(String isolateId, + {/*ExceptionPauseMode*/ String? exceptionPauseMode, + bool? shouldPauseOnExit}); + + /// The `setFlag` RPC is used to set a VM flag at runtime. Returns an error if + /// the named flag does not exist, the flag may not be set at runtime, or the + /// value is of the wrong type for the flag. + /// + /// The following flags may be set at runtime: + /// + /// - pause_isolates_on_start + /// - pause_isolates_on_exit + /// - pause_isolates_on_unhandled_exceptions + /// - profile_period + /// - profiler + /// + /// Notes: + /// + /// - `profile_period` can be set to a minimum value of 50. Attempting to set + /// `profile_period` to a lower value will result in a value of 50 being set. + /// - Setting `profiler` will enable or disable the profiler depending on the + /// provided value. If set to false when the profiler is already running, the + /// profiler will be stopped but may not free its sample buffer depending on + /// platform limitations. + /// - Isolate pause settings will only be applied to newly spawned isolates. + /// + /// See [Success]. + /// + /// The return value can be one of [Success] or [Error]. + Future setFlag(String name, String value); + + /// The `setLibraryDebuggable` RPC is used to enable or disable whether + /// breakpoints and stepping work for a given library. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future setLibraryDebuggable( + String isolateId, String libraryId, bool isDebuggable); + + /// The `setName` RPC is used to change the debugging name for an isolate. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future setName(String isolateId, String name); + + /// The `setTraceClassAllocation` RPC allows for enabling or disabling + /// allocation tracing for a specific type of object. Allocation traces can be + /// retrieved with the `getAllocationTraces` RPC. + /// + /// If `enable` is true, allocations of objects of the class represented by + /// `classId` will be traced. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future setTraceClassAllocation( + String isolateId, String classId, bool enable); + + /// The `setVMName` RPC is used to change the debugging name for the vm. + /// + /// See [Success]. + Future setVMName(String name); + + /// The `setVMTimelineFlags` RPC is used to set which timeline streams are + /// enabled. + /// + /// The `recordedStreams` parameter is the list of all timeline streams which + /// are to be enabled. Streams not explicitly specified will be disabled. + /// Invalid stream names are ignored. + /// + /// A `TimelineStreamSubscriptionsUpdate` event is sent on the `Timeline` + /// stream as a result of invoking this RPC. + /// + /// To get the list of currently enabled timeline streams, see + /// [VmServiceInterface.getVMTimelineFlags]. + /// + /// See [Success]. + Future setVMTimelineFlags(List recordedStreams); + + /// The `streamCancel` RPC cancels a stream subscription in the VM. + /// + /// If the client is not subscribed to the stream, the `104` (Stream not + /// subscribed) RPC error code is returned. + /// + /// See [Success]. + Future streamCancel(String streamId); + + /// The `streamCpuSamplesWithUserTag` RPC allows for clients to specify which + /// CPU samples collected by the profiler should be sent over the `Profiler` + /// stream. When called, the VM will stream `CpuSamples` events containing + /// `CpuSample`'s collected while a user tag contained in `userTags` was + /// active. + /// + /// See [Success]. + Future streamCpuSamplesWithUserTag(List userTags); + + /// The `streamListen` RPC subscribes to a stream in the VM. Once subscribed, + /// the client will begin receiving events from the stream. + /// + /// If the client is already subscribed to the stream, the `103` (Stream + /// already subscribed) RPC error code is returned. + /// + /// The `streamId` parameter may have the following published values: + /// + /// streamId | event types provided + /// -------- | ----------- + /// VM | VMUpdate, VMFlagUpdate + /// Isolate | IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, + /// IsolateReload, ServiceExtensionAdded + /// Debug | PauseStart, PauseExit, PauseBreakpoint, PauseInterrupted, + /// PauseException, PausePostRequest, Resume, BreakpointAdded, + /// BreakpointResolved, BreakpointRemoved, BreakpointUpdated, Inspect, None + /// Profiler | CpuSamples, UserTagChanged + /// GC | GC + /// Extension | Extension + /// Timeline | TimelineEvents, TimelineStreamsSubscriptionUpdate + /// Logging | Logging + /// Service | ServiceRegistered, ServiceUnregistered + /// HeapSnapshot | HeapSnapshot + /// + /// Additionally, some embedders provide the `Stdout` and `Stderr` streams. + /// These streams allow the client to subscribe to writes to stdout and + /// stderr. + /// + /// streamId | event types provided + /// -------- | ----------- + /// Stdout | WriteEvent + /// Stderr | WriteEvent + /// + /// It is considered a `backwards compatible` change to add a new type of + /// event to an existing stream. Clients should be written to handle this + /// gracefully, perhaps by warning and ignoring. + /// + /// See [Success]. + Future streamListen(String streamId); +} + +class _PendingServiceRequest { + Future> get future => _completer.future; + final _completer = Completer>(); + + final dynamic originalId; + + _PendingServiceRequest(this.originalId); + + void complete(Map 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> _requestStream; + final StreamSink> _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 = {}; + + /// Completes when [_requestStream] is done. + Future get done => _doneCompleter.future; + final _doneCompleter = Completer(); + + /// Pending service extension requests to this client by id. + final _pendingServiceExtensionRequests = {}; + + 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> _forwardServiceExtensionRequest( + Map request) { + final originalId = request['id']; + request = Map.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 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.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?; + late Response response; + + switch (method) { + case 'registerService': + _serviceExtensionRegistry.registerExtension(params!['service'], this); + response = Success(); + break; + case 'addBreakpoint': + response = await _serviceImplementation.addBreakpoint( + params!['isolateId'], + params['scriptId'], + params['line'], + column: params['column'], + ); + break; + case 'addBreakpointWithScriptUri': + response = await _serviceImplementation.addBreakpointWithScriptUri( + params!['isolateId'], + params['scriptUri'], + params['line'], + column: params['column'], + ); + break; + case 'addBreakpointAtEntry': + response = await _serviceImplementation.addBreakpointAtEntry( + params!['isolateId'], + params['functionId'], + ); + break; + case 'clearCpuSamples': + response = await _serviceImplementation.clearCpuSamples( + params!['isolateId'], + ); + break; + case 'clearVMTimeline': + response = await _serviceImplementation.clearVMTimeline(); + break; + case 'invoke': + response = await _serviceImplementation.invoke( + params!['isolateId'], + params['targetId'], + params['selector'], + List.from(params['argumentIds'] ?? []), + disableBreakpoints: params['disableBreakpoints'], + ); + break; + case 'evaluate': + response = await _serviceImplementation.evaluate( + params!['isolateId'], + params['targetId'], + params['expression'], + scope: params['scope']?.cast(), + disableBreakpoints: params['disableBreakpoints'], + ); + break; + case 'evaluateInFrame': + response = await _serviceImplementation.evaluateInFrame( + params!['isolateId'], + params['frameIndex'], + params['expression'], + scope: params['scope']?.cast(), + disableBreakpoints: params['disableBreakpoints'], + ); + break; + case 'getAllocationProfile': + response = await _serviceImplementation.getAllocationProfile( + params!['isolateId'], + reset: params['reset'], + gc: params['gc'], + ); + break; + case 'getAllocationTraces': + response = await _serviceImplementation.getAllocationTraces( + params!['isolateId'], + timeOriginMicros: params['timeOriginMicros'], + timeExtentMicros: params['timeExtentMicros'], + classId: params['classId'], + ); + break; + case 'getClassList': + response = await _serviceImplementation.getClassList( + params!['isolateId'], + ); + break; + case 'getCpuSamples': + response = await _serviceImplementation.getCpuSamples( + params!['isolateId'], + params['timeOriginMicros'], + params['timeExtentMicros'], + ); + break; + case 'getFlagList': + response = await _serviceImplementation.getFlagList(); + break; + case 'getInboundReferences': + response = await _serviceImplementation.getInboundReferences( + params!['isolateId'], + params['targetId'], + params['limit'], + ); + break; + case 'getInstances': + response = await _serviceImplementation.getInstances( + params!['isolateId'], + params['objectId'], + params['limit'], + includeSubclasses: params['includeSubclasses'], + includeImplementers: params['includeImplementers'], + ); + break; + case 'getInstancesAsList': + response = await _serviceImplementation.getInstancesAsList( + params!['isolateId'], + params['objectId'], + includeSubclasses: params['includeSubclasses'], + includeImplementers: params['includeImplementers'], + ); + break; + case 'getIsolate': + response = await _serviceImplementation.getIsolate( + params!['isolateId'], + ); + break; + case 'getIsolateGroup': + response = await _serviceImplementation.getIsolateGroup( + params!['isolateGroupId'], + ); + break; + case 'getIsolatePauseEvent': + response = await _serviceImplementation.getIsolatePauseEvent( + params!['isolateId'], + ); + break; + case 'getMemoryUsage': + response = await _serviceImplementation.getMemoryUsage( + params!['isolateId'], + ); + break; + case 'getIsolateGroupMemoryUsage': + response = await _serviceImplementation.getIsolateGroupMemoryUsage( + params!['isolateGroupId'], + ); + break; + case 'getScripts': + response = await _serviceImplementation.getScripts( + params!['isolateId'], + ); + break; + case 'getObject': + response = await _serviceImplementation.getObject( + params!['isolateId'], + params['objectId'], + offset: params['offset'], + count: params['count'], + ); + break; + case 'getPerfettoCpuSamples': + response = await _serviceImplementation.getPerfettoCpuSamples( + params!['isolateId'], + timeOriginMicros: params['timeOriginMicros'], + timeExtentMicros: params['timeExtentMicros'], + ); + break; + case 'getPerfettoVMTimeline': + response = await _serviceImplementation.getPerfettoVMTimeline( + timeOriginMicros: params!['timeOriginMicros'], + timeExtentMicros: params['timeExtentMicros'], + ); + break; + case 'getPorts': + response = await _serviceImplementation.getPorts( + params!['isolateId'], + ); + break; + case 'getRetainingPath': + response = await _serviceImplementation.getRetainingPath( + params!['isolateId'], + params['targetId'], + params['limit'], + ); + break; + case 'getProcessMemoryUsage': + response = await _serviceImplementation.getProcessMemoryUsage(); + break; + case 'getStack': + response = await _serviceImplementation.getStack( + params!['isolateId'], + limit: params['limit'], + ); + break; + case 'getSupportedProtocols': + response = await _serviceImplementation.getSupportedProtocols(); + break; + case 'getSourceReport': + response = await _serviceImplementation.getSourceReport( + params!['isolateId'], + List.from(params['reports'] ?? []), + scriptId: params['scriptId'], + tokenPos: params['tokenPos'], + endTokenPos: params['endTokenPos'], + forceCompile: params['forceCompile'], + reportLines: params['reportLines'], + libraryFilters: params['libraryFilters'], + librariesAlreadyCompiled: params['librariesAlreadyCompiled'], + ); + break; + case 'getVersion': + response = await _serviceImplementation.getVersion(); + break; + case 'getVM': + response = await _serviceImplementation.getVM(); + break; + case 'getVMTimeline': + response = await _serviceImplementation.getVMTimeline( + timeOriginMicros: params!['timeOriginMicros'], + timeExtentMicros: params['timeExtentMicros'], + ); + break; + case 'getVMTimelineFlags': + response = await _serviceImplementation.getVMTimelineFlags(); + break; + case 'getVMTimelineMicros': + response = await _serviceImplementation.getVMTimelineMicros(); + break; + case 'pause': + response = await _serviceImplementation.pause( + params!['isolateId'], + ); + break; + case 'kill': + response = await _serviceImplementation.kill( + params!['isolateId'], + ); + break; + case 'lookupResolvedPackageUris': + response = await _serviceImplementation.lookupResolvedPackageUris( + params!['isolateId'], + List.from(params['uris'] ?? []), + local: params['local'], + ); + break; + case 'lookupPackageUris': + response = await _serviceImplementation.lookupPackageUris( + params!['isolateId'], + List.from(params['uris'] ?? []), + ); + break; + case 'reloadSources': + response = await _serviceImplementation.reloadSources( + params!['isolateId'], + force: params['force'], + pause: params['pause'], + rootLibUri: params['rootLibUri'], + packagesUri: params['packagesUri'], + ); + break; + case 'removeBreakpoint': + response = await _serviceImplementation.removeBreakpoint( + params!['isolateId'], + params['breakpointId'], + ); + break; + case 'requestHeapSnapshot': + response = await _serviceImplementation.requestHeapSnapshot( + params!['isolateId'], + ); + break; + case 'resume': + response = await _serviceImplementation.resume( + params!['isolateId'], + step: params['step'], + frameIndex: params['frameIndex'], + ); + break; + case 'setBreakpointState': + response = await _serviceImplementation.setBreakpointState( + params!['isolateId'], + params['breakpointId'], + params['enable'], + ); + break; + case 'setExceptionPauseMode': + // ignore: deprecated_member_use_from_same_package + response = await _serviceImplementation.setExceptionPauseMode( + params!['isolateId'], + params['mode'], + ); + break; + case 'setIsolatePauseMode': + response = await _serviceImplementation.setIsolatePauseMode( + params!['isolateId'], + exceptionPauseMode: params['exceptionPauseMode'], + shouldPauseOnExit: params['shouldPauseOnExit'], + ); + break; + case 'setFlag': + response = await _serviceImplementation.setFlag( + params!['name'], + params['value'], + ); + break; + case 'setLibraryDebuggable': + response = await _serviceImplementation.setLibraryDebuggable( + params!['isolateId'], + params['libraryId'], + params['isDebuggable'], + ); + break; + case 'setName': + response = await _serviceImplementation.setName( + params!['isolateId'], + params['name'], + ); + break; + case 'setTraceClassAllocation': + response = await _serviceImplementation.setTraceClassAllocation( + params!['isolateId'], + params['classId'], + params['enable'], + ); + break; + case 'setVMName': + response = await _serviceImplementation.setVMName( + params!['name'], + ); + break; + case 'setVMTimelineFlags': + response = await _serviceImplementation.setVMTimelineFlags( + List.from(params!['recordedStreams'] ?? []), + ); + break; + case 'streamCancel': + var id = params!['streamId']; + var existing = _streamSubscriptions.remove(id); + if (existing == null) { + throw RPCError.withDetails( + 'streamCancel', + 104, + 'Stream not subscribed', + details: "The stream '$id' is not subscribed", + ); + } + await existing.cancel(); + response = Success(); + break; + case 'streamCpuSamplesWithUserTag': + response = await _serviceImplementation.streamCpuSamplesWithUserTag( + List.from(params!['userTags'] ?? []), + ); + break; + case 'streamListen': + var id = params!['streamId']; + if (_streamSubscriptions.containsKey(id)) { + throw RPCError.withDetails( + 'streamListen', + 103, + 'Stream already subscribed', + details: "The stream '$id' is already subscribed", + ); + } + + var stream = id == 'Service' + ? _serviceExtensionRegistry.onExtensionEvent + : _serviceImplementation.onEvent(id); + _streamSubscriptions[id] = stream.listen((e) { + _responseSink.add({ + 'jsonrpc': '2.0', + 'method': 'streamNotify', + 'params': { + 'streamId': id, + 'event': e.toJson(), + }, + }); + }); + response = Success(); + break; + default: + 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.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); + } + } + _responseSink.add({ + 'jsonrpc': '2.0', + 'id': id, + 'result': response.toJson(), + }); + } 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, + }); + } + } +} diff --git a/pkg/vm_service/lib/vm_service.dart b/pkg/vm_service/lib/vm_service.dart index 9327d0e5f07..73a6c1f2d65 100644 --- a/pkg/vm_service/lib/vm_service.dart +++ b/pkg/vm_service/lib/vm_service.dart @@ -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; diff --git a/pkg/vm_service/pubspec.yaml b/pkg/vm_service/pubspec.yaml index c24eb75748c..77baca846b1 100644 --- a/pkg/vm_service/pubspec.yaml +++ b/pkg/vm_service/pubspec.yaml @@ -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' diff --git a/pkg/vm_service/test/breakpoint_in_package_parts_class_file_uri_test.dart b/pkg/vm_service/test/breakpoint_in_package_parts_class_file_uri_test.dart index 70cd0fd0226..cb71ca53031 100644 --- a/pkg/vm_service/test/breakpoint_in_package_parts_class_file_uri_test.dart +++ b/pkg/vm_service/test/breakpoint_in_package_parts_class_file_uri_test.dart @@ -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; diff --git a/pkg/vm_service/test/get_http_profile_test.dart b/pkg/vm_service/test/get_http_profile_test.dart index d8124767fe7..1ff9988bce2 100644 --- a/pkg/vm_service/test/get_http_profile_test.dart +++ b/pkg/vm_service/test/get_http_profile_test.dart @@ -46,7 +46,10 @@ Future 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 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); } } diff --git a/pkg/vm_service/test/patterns_local_vars_test.dart b/pkg/vm_service/test/patterns_local_vars_test.dart index 0cb3e678008..63c0b8814cf 100644 --- a/pkg/vm_service/test/patterns_local_vars_test.dart +++ b/pkg/vm_service/test/patterns_local_vars_test.dart @@ -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 = [ (VmService service, IsolateRef isolateRef) async { Stack stack = await service.getStack(isolateRef.id!); final Set vars = stack.frames![0].vars!.map((v) => v.name!).toSet(); - Expect.setEquals({'obj', 'x1', 'y1'}, vars); + expect(vars, {'obj', 'x1', 'y1'}); }, ]; diff --git a/pkg/vm_service/test/test_package/has_part.dart b/pkg/vm_service/test/test_package/lib/has_part.dart similarity index 100% rename from pkg/vm_service/test/test_package/has_part.dart rename to pkg/vm_service/test/test_package/lib/has_part.dart diff --git a/pkg/vm_service/test/test_package/the_part.dart b/pkg/vm_service/test/test_package/lib/the_part.dart similarity index 100% rename from pkg/vm_service/test/test_package/the_part.dart rename to pkg/vm_service/test/test_package/lib/the_part.dart diff --git a/pkg/vm_service/test/test_package/the_part_2.dart b/pkg/vm_service/test/test_package/lib/the_part_2.dart similarity index 100% rename from pkg/vm_service/test/test_package/the_part_2.dart rename to pkg/vm_service/test/test_package/lib/the_part_2.dart diff --git a/pkg/vm_service/test/verify_http_timeline_test.dart b/pkg/vm_service/test/verify_http_timeline_test.dart index ce9dbde2d6f..c61a9731ad7 100644 --- a/pkg/vm_service/test/verify_http_timeline_test.dart +++ b/pkg/vm_service/test/verify_http_timeline_test.dart @@ -46,7 +46,10 @@ Future 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) { diff --git a/pkg/vm_service/tool/common/generate_common.dart b/pkg/vm_service/tool/common/generate_common.dart index de054ee50cb..ca95e30a934 100644 --- a/pkg/vm_service/tool/common/generate_common.dart +++ b/pkg/vm_service/tool/common/generate_common.dart @@ -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 nodes) => parseVersionSemVer(nodes).toString(); diff --git a/pkg/vm_service/tool/dart/generate_dart_client.dart b/pkg/vm_service/tool/dart/generate_dart_client.dart new file mode 100644 index 00000000000..c9788417ae6 --- /dev/null +++ b/pkg/vm_service/tool/dart/generate_dart_client.dart @@ -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 callMethod(String method, { + String? isolateId, + Map? 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 callServiceExtension(String method, { + String? isolateId, + Map? 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 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 wrapFuture(String name, Future future) { + return future; + } + + Future _call(String method, [Map args = const {}]) { + return wrapFuture( + method, + () { + final request = _OutstandingRequest(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) { + 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 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; + 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] ?? []; + request.complete(createServiceObject(result, returnTypes)); + } + } + } + + Future _processRequest(Map json) async { + final result = await _routeRequest(json['method'], json['params'] ?? {}); + result['id'] = json['id']; + result['jsonrpc'] = '2.0'; + String message = jsonEncode(result); + _onSend.add(message); + _writeMessage(message); + } + + Future _processNotification(Map json) async { + final method = json['method']; + final params = json['params'] ?? {}; + if (method == 'streamNotify') { + final streamId = params['streamId']; + _getEventController(streamId).add(createServiceObject(params['event'], const ['Event'])! as Event); + } else { + await _routeRequest(method, params); + } + } + + Future _routeRequest(String method, Map 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({}, (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 : {} { + 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 toMap() { + final map = { + '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 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? json) => + json == null ? null : ExtensionData._fromJson(json); + + final Map data; + + ExtensionData() : data = {}; + + 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 expectedTypes) { + if (json == null) return null; + + if (json is List) { + return json.map((e) => createServiceObject(e, expectedTypes)).toList(); + } else if (json is Map) { + 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 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 json, String key, Object? value) { + if (value == null) return; + json[key] = value; +} + +Future extensionCallHelper(VmService service, String method, Map args) { + return service._call(method, args); +} + +typedef ServiceCallback = Future> Function( + Map 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 = {'); + for (var type in types) { + gen.writeln("'${type!.rawName}': ${type.name}.parse,"); + } + gen.writeln('};'); + gen.writeln(); + + gen.writeln('final _methodReturnTypes = >{'); + for (var method in methods) { + String returnTypes = typeRefListToString(method.returnType.types); + gen.writeln("'${method.name}' : $returnTypes,"); + } + gen.writeln('};'); + gen.writeln(); + gen.write(''' +class _OutstandingRequest { + _OutstandingRequest(this.method); + static int _idCounter = 0; + final id = '\${_idCounter++}'; + final String method; + final _stackTrace = StackTrace.current; + final _completer = Completer(); + + Future 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 Function({ + required Stream /*String|List*/ 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 = {};'); + gen.writeStatement('final _services = {};'); + gen.writeStatement('late final Log _log;'); + gen.write(''' + + /// The web socket URI pointing to the target VM service instance. + final String? wsUri; + + Stream get onSend => _onSend.stream; + final _onSend = StreamController.broadcast(sync: true); + + Stream get onReceive => _onReceive.stream; + final _onReceive = StreamController.broadcast(sync: true); + + Future get onDone => _onDoneCompleter.future; + final _onDoneCompleter = Completer(); + + final _eventControllers = >{}; + + StreamController _getEventController(String eventName) { + StreamController? controller = _eventControllers[eventName]; + if (controller == null) { + controller = StreamController.broadcast(); + _eventControllers[eventName] = controller; + } + return controller; + } + + late final DisposeHandler? _disposeHandler; + + VmService( + Stream /*String|List*/ 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 /*String|List*/ 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 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('}'); + } +} diff --git a/pkg/vm_service/tool/dart/generate_dart.dart b/pkg/vm_service/tool/dart/generate_dart_common.dart similarity index 58% rename from pkg/vm_service/tool/dart/generate_dart.dart rename to pkg/vm_service/tool/dart/generate_dart_common.dart index 575fadfb741..675e94b8f6e 100644 --- a/pkg/vm_service/tool/dart/generate_dart.dart +++ b/pkg/vm_service/tool/dart/generate_dart_common.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// 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. @@ -13,8 +13,6 @@ import 'src_gen_dart.dart'; export 'src_gen_dart.dart' show DartGenerator; -late Api api; - String? _coerceRefType(String? typeName) { if (typeName == 'Object') typeName = 'Obj'; if (typeName == '@Object') typeName = 'ObjRef'; @@ -31,431 +29,14 @@ String? _coerceRefType(String? typeName) { return typeName; } -String _typeRefListToString(List types) => +String typeRefListToString(List types) => 'const [${types.map((e) => "'${e.name!}'").join(',')}]'; -final String _headerCode = 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'; - -import 'service_extension_registry.dart'; - -export 'service_extension_registry.dart' show ServiceExtensionRegistry; -export 'snapshot_graph.dart' show HeapSnapshotClass, - HeapSnapshotExternalProperty, - HeapSnapshotField, - HeapSnapshotGraph, - HeapSnapshotObject, - HeapSnapshotObjectLengthData, - HeapSnapshotObjectNoData, - HeapSnapshotObjectNullData; -'''; - -final String _implCode = r''' - - /// Call an arbitrary service protocol method. This allows clients to call - /// methods not explicitly exposed by this library. - Future callMethod(String method, { - String? isolateId, - Map? 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. - @override - Future callServiceExtension(String method, { - String? isolateId, - Map? 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 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 wrapFuture(String name, Future future) { - return future; - } - - Future _call(String method, [Map args = const {}]) { - return wrapFuture( - method, - () { - final request = _OutstandingRequest(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) { - Uint8List 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); - dynamic map = jsonDecode(meta)!; - if (map['method'] == 'streamNotify') { - String streamId = map['params']['streamId']; - Map event = map['params']['event']; - event['data'] = data; - _getEventController(streamId) - .add(createServiceObject(event, const ['Event'])! as Event); - } - } - - void _processMessageStr(String message) { - late Map json; - try { - _onReceive.add(message); - json = jsonDecode(message)!; - } catch (e, s) { - _log.severe('unable to decode message: $message, $e\n$s'); - return; - } - - 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'); - } - } - - void _processResponse(Map 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 { - Map result = json['result'] as Map; - String? type = result['type']; - if (type == 'Sentinel') { - request.completeError(SentinelException.parse(request.method, result)); - } else if (_typeFactories[type] == null) { - request.complete(Response.parse(result)); - } else { - List returnTypes = _methodReturnTypes[request.method] ?? []; - request.complete(createServiceObject(result, returnTypes)); - } - } - } - - Future _processRequest(Map json) async { - final Map m = await _routeRequest(json['method'], json['params'] ?? {}); - m['id'] = json['id']; - m['jsonrpc'] = '2.0'; - String message = jsonEncode(m); - _onSend.add(message); - _writeMessage(message); - } - - Future _processNotification(Map json) async { - final String method = json['method']; - final Map params = json['params'] ?? {}; - if (method == 'streamNotify') { - String streamId = params['streamId']; - _getEventController(streamId).add(createServiceObject(params['event'], const ['Event'])! as Event); - } else { - await _routeRequest(method, params); - } - } - - Future _routeRequest(String method, Map params) async{ - final service = _services[method]; - if (service == null) { - RPCError 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()}; - } - } -'''; - -final String _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({}, (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 : {} { - 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 toMap() { - Map map = { - '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 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? json) => - json == null ? null : ExtensionData._fromJson(json); - - final Map data; - - ExtensionData() : data = {}; - - 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) {} -} -'''; - -final _registerServiceImpl = ''' +final registerServiceImpl = ''' _serviceExtensionRegistry.registerExtension(params!['service'], this); response = Success();'''; -final _streamListenCaseImpl = ''' +final streamListenCaseImpl = ''' var id = params!['streamId']; if (_streamSubscriptions.containsKey(id)) { throw RPCError.withDetails( @@ -479,7 +60,7 @@ _streamSubscriptions[id] = stream.listen((e) { }); response = Success();'''; -final _streamCancelCaseImpl = ''' +final streamCancelCaseImpl = ''' var id = params!['streamId']; var existing = _streamSubscriptions.remove(id); if (existing == null) { @@ -504,7 +85,7 @@ abstract class Member { String toString() => name!; } -class Api extends Member with ApiParseUtil { +abstract class Api with ApiParseUtil { String? serviceVersion; List methods = []; List enums = []; @@ -560,21 +141,15 @@ class Api extends Member with ApiParseUtil { _parseStreamListenDocs(streamListenMethod.docs!); } - @override - String get name => 'api'; - - @override - String? get docs => null; - void _parse(String name, String definition, [String? docs]) { name = replaceHTMLEntities(name.trim()); definition = replaceHTMLEntities(definition.trim()); if (docs != null) docs = replaceHTMLEntities(docs.trim()); if (definition.startsWith('class ')) { - types.add(Type(this, name, definition, docs)); + types.add(Type(this, this, name, definition, docs)); } else if (name.substring(0, 1).toLowerCase() == name.substring(0, 1)) { - methods.add(Method(name, definition, docs)); + methods.add(Method(this, name, definition, docs)); } else if (definition.startsWith('enum ')) { enums.add(Enum(name, definition, docs)); } else { @@ -582,496 +157,6 @@ class Api extends Member with ApiParseUtil { } } - static String printNode(Node n) { - if (n is Text) { - return n.text; - } else if (n is Element) { - if (n.tag != 'h3') return n.tag; - return '${n.tag}:[${n.children!.map((c) => printNode(c)).join(', ')}]'; - } else { - return '$n'; - } - } - - @override - void generate(DartGenerator gen) { - gen.out(_headerCode); - 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 expectedTypes) { - if (json == null) return null; - - if (json is List) { - return json.map((e) => createServiceObject(e, expectedTypes)).toList(); - } else if (json is Map) { - 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 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 json, String key, Object? value) { - if (value == null) return; - json[key] = value; -} - -Future extensionCallHelper(VmService service, String method, Map args) { - return service._call(method, args); -} - -typedef ServiceCallback = Future> Function( - Map 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('Map _typeFactories = {'); - for (var type in types) { - gen.writeln("'${type!.rawName}': ${type.name}.parse,"); - } - gen.writeln('};'); - gen.writeln(); - - gen.writeln('Map> _methodReturnTypes = {'); - for (var method in methods) { - String returnTypes = _typeRefListToString(method.returnType.types); - gen.writeln("'${method.name}' : $returnTypes,"); - } - gen.writeln('};'); - gen.writeln(); - - // The service interface, both servers and clients implement this. - gen.writeStatement(''' -/// A class representation of the Dart VM Service Protocol. -/// -/// Both clients and servers should implement this interface. -abstract 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 onEvent(String streamId); - - /// Handler for calling extra service extensions. - Future callServiceExtension(String method, {String? isolateId, Map? 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> get future => _completer.future; - final _completer = Completer>(); - - final dynamic originalId; - - _PendingServiceRequest(this.originalId); - - void complete(Map 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> _requestStream; - final StreamSink> _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 = {}; - - /// Completes when [_requestStream] is done. - Future get done => _doneCompleter.future; - final _doneCompleter = Completer(); - - /// Pending service extension requests to this client by id. - final _pendingServiceExtensionRequests = {}; - - 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> _forwardServiceExtensionRequest( - Map request) { - final originalId = request['id']; - request = Map.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 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.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?; - 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(), "); - } 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.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(); - - gen - ..writeln(''' -class _OutstandingRequest { - _OutstandingRequest(this.method); - static int _idCounter = 0; - final String id = '\${_idCounter++}'; - final String method; - final StackTrace _stackTrace = StackTrace.current; - final Completer _completer = Completer(); - - Future get future => _completer.future; - - void complete(T value) => _completer.complete(value); - void completeError(Object error) => - _completer.completeError(error, _stackTrace); -} -''') - ..writeln(); - - gen - ..writeln(''' -typedef VmServiceFactory = T Function({ - required Stream /*String|List*/ inStream, - required void Function(String message) writeMessage, - Log? log, - DisposeHandler? disposeHandler, - Future? streamClosed, - String? wsUri, -}); -''') - ..writeln(); - - // The client side service implementation. - gen.writeStatement('class VmService implements VmServiceInterface {'); - gen.writeStatement('late final StreamSubscription _streamSub;'); - gen.writeStatement('late final Function _writeMessage;'); - gen.writeStatement( - 'final Map _outstandingRequests = {};'); - gen.writeStatement('final Map _services = {};'); - gen.writeStatement('late final Log _log;'); - gen.write(''' - - /// The web socket URI pointing to the target VM service instance. - final String? wsUri; - - Stream get onSend => _onSend.stream; - final StreamController _onSend = StreamController.broadcast(sync: true); - - Stream get onReceive => _onReceive.stream; - final StreamController _onReceive = StreamController.broadcast(sync: true); - - Future get onDone => _onDoneCompleter.future; - final Completer _onDoneCompleter = Completer(); - - final Map> _eventControllers = {}; - - StreamController _getEventController(String eventName) { - StreamController? controller = _eventControllers[eventName]; - if (controller == null) { - controller = StreamController.broadcast(); - _eventControllers[eventName] = controller; - } - return controller; - } - - late final DisposeHandler? _disposeHandler; - - VmService( - Stream /*String|List*/ 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 /*String|List*/ 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, - ); - } - - @override - Stream 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('// 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; - } - - bool isEnumName(String? typeName) => - enums.any((Enum e) => e.name == typeName); - - Type? getType(String? name) => - types.firstWhere((t) => t!.name == name, orElse: () => null); - void _parseStreamListenDocs(String docs) { Iterator lines = docs.split('\n').map((l) => l.trim()).iterator; bool inStreamDef = false; @@ -1092,18 +177,24 @@ typedef VmServiceFactory = T Function({ } } - 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}';"); + static String printNode(Node n) { + if (n is Text) { + return n.text; + } else if (n is Element) { + if (n.tag != 'h3') return n.tag; + return '${n.tag}:[${n.children!.map((c) => printNode(c)).join(', ')}]'; + } else { + return '$n'; } - - gen.writeln('}'); } + + void generate(DartGenerator gen); + + bool isEnumName(String? typeName) => + enums.any((Enum e) => e.name == typeName); + + Type? getType(String? name) => + types.firstWhere((t) => t!.name == name, orElse: () => null); } class StreamCategory { @@ -1134,17 +225,19 @@ class StreamCategory { } class Method extends Member { + final Api api; + @override final String name; @override final String? docs; - MemberType returnType = MemberType(); + late MemberType returnType = MemberType(api); bool get deprecated => deprecationMessage != null; String? deprecationMessage; List args = []; - Method(this.name, String definition, [this.docs]) { + Method(this.api, this.name, String definition, [this.docs]) { _parse(Tokenizer(definition).tokenize()); } @@ -1154,7 +247,7 @@ class Method extends Member { @override void generate(DartGenerator gen) { - generateDefinition(gen, withDocs: false, withOverrides: true); + generateDefinition(gen); if (!hasArgs) { gen.writeStatement("=> _call('$name');"); } else if (hasOptionalArgs) { @@ -1189,13 +282,9 @@ class Method extends Member { /// Writes the method definition without the body. /// /// Does not write an opening or closing bracket, or a trailing semicolon. - /// - /// If [withOverrides] is `true` then it will add an `@override` annotation - /// before each method. - void generateDefinition(DartGenerator gen, - {bool withDocs = true, bool withOverrides = false}) { + void generateDefinition(DartGenerator gen) { gen.writeln(); - if (withDocs && docs != null) { + if (docs != null) { String methodDocs = docs!; if (returnType.isMultipleReturns) { methodDocs += '\n\nThe return value can be one of ' @@ -1212,7 +301,6 @@ class Method extends Member { if (deprecated) { gen.writeln("@Deprecated('$deprecationMessage')"); } - if (withOverrides) gen.writeln('@override'); gen.write('Future<${returnType.name}> $name('); bool startedOptional = false; gen.write(args.map((MethodArg arg) { @@ -1243,9 +331,10 @@ class Method extends Member { } class MemberType extends Member { + final Api api; List types = []; - MemberType(); + MemberType(this.api); void parse(Parser parser, {bool isReturnType = false}) { // foo|bar[]|baz @@ -1408,6 +497,7 @@ class MethodArg extends Member { } class Type extends Member { + final Api api; final Api parent; String? rawName; @override @@ -1417,11 +507,13 @@ class Type extends Member { final String? docs; List fields = []; - Type(this.parent, String categoryName, String definition, [this.docs]) { + Type(this.api, this.parent, String categoryName, String definition, + [this.docs]) { _parse(Tokenizer(definition).tokenize()); } - Type._(this.parent, this.rawName, this.name, this.superName, this.docs); + Type._(this.api, this.parent, this.rawName, this.name, this.superName, + this.docs); factory Type.merge(Type t1, Type t2) { final Api parent = t1.parent; @@ -1440,7 +532,8 @@ class Type extends Member { final fields = map.values.toList().reversed.toList(); - return Type._(parent, rawName, name, superName, docs)..fields = fields; + return Type._(t1.api, parent, rawName, name, superName, docs) + ..fields = fields; } bool get isResponse { @@ -1631,7 +724,7 @@ class Type extends Member { gen.writeln('_parseTokenPosTable();'); } else if (field.type.isArray) { TypeRef fieldType = field.type.types.first; - String typesList = _typeRefListToString(field.type.types); + String typesList = typeRefListToString(field.type.types); String ref = "json['${field.name}']"; if (field.optional) { if (fieldType.isListTypeSimple) { @@ -1665,7 +758,7 @@ class Type extends Member { } } } else { - String typesList = _typeRefListToString(field.type.types); + String typesList = typeRefListToString(field.type.types); String nullable = field.type.name != 'dynamic' ? '?' : ''; gen.writeln( '${field.generatableName} = ' @@ -1945,16 +1038,17 @@ class TypeField extends Member { 'new': 'new_', }; + final Api api; final Type parent; final String? _docs; - MemberType type = MemberType(); + late MemberType type = MemberType(api); @override String? name; bool optional = false; String? _defaultValue; bool overrides = false; - TypeField(this.parent, this._docs); + TypeField(this.api, this.parent, this._docs); void setOverrides() => overrides = true; @@ -2291,7 +1385,7 @@ class TypeParser extends Parser { expect('{'); while (peek()!.text != '}') { - TypeField field = TypeField(type, collectComments()); + TypeField field = TypeField(type.api, type, collectComments()); field.type.parse(this); field.name = expectName().text; if (consume('[')) { @@ -2308,7 +1402,7 @@ class TypeParser extends Parser { if (type.rawName == 'Event') { final comment = 'Binary data associated with the event.\n\n' 'This is provided for the event kinds:\n - HeapSnapshot'; - TypeField dataField = TypeField(type, comment); + TypeField dataField = TypeField(type.api, type, comment); dataField.type.types.add(TypeRef('ByteData')); dataField.name = 'data'; dataField.optional = true; diff --git a/pkg/vm_service/tool/dart/generate_dart_interface.dart b/pkg/vm_service/tool/dart/generate_dart_interface.dart new file mode 100644 index 00000000000..2053f9ff27a --- /dev/null +++ b/pkg/vm_service/tool/dart/generate_dart_interface.dart @@ -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 onEvent(String streamId); + + /// Handler for calling extra service extensions. + Future callServiceExtension(String method, {String? isolateId, Map? 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> get future => _completer.future; + final _completer = Completer>(); + + final dynamic originalId; + + _PendingServiceRequest(this.originalId); + + void complete(Map 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> _requestStream; + final StreamSink> _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 = {}; + + /// Completes when [_requestStream] is done. + Future get done => _doneCompleter.future; + final _doneCompleter = Completer(); + + /// Pending service extension requests to this client by id. + final _pendingServiceExtensionRequests = {}; + + 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> _forwardServiceExtensionRequest( + Map request) { + final originalId = request['id']; + request = Map.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 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.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?; + 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(), "); + } 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.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; + } +} diff --git a/pkg/vm_service/tool/dart/src_gen_dart.dart b/pkg/vm_service/tool/dart/src_gen_dart.dart index 9f8241541c5..2b8974db0fc 100644 --- a/pkg/vm_service/tool/dart/src_gen_dart.dart +++ b/pkg/vm_service/tool/dart/src_gen_dart.dart @@ -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); diff --git a/pkg/vm_service/tool/generate.dart b/pkg/vm_service/tool/generate.dart index a5f7dd45177..69ca7d0b69c 100644 --- a/pkg/vm_service/tool/generate.dart +++ b/pkg/vm_service/tool/generate.dart @@ -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 args) async { - String appDirPath = dirname(Platform.script.toFilePath()); +Future main(List 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 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 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 _generateDartClient( + String codeGeneratorDir, List 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 _generateDartInterface( + String codeGeneratorDir, List 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 _generateDartCommon({ + required Api api, + required List 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 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 nodes) async { - var srcDirPath = normalize(join(appDirPath, '..', 'java', 'src')); +Future _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 _generateJava(String codeGeneratorDir, List 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 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. diff --git a/pkg/vm_service_interface/CHANGELOG.md b/pkg/vm_service_interface/CHANGELOG.md new file mode 100644 index 00000000000..02317477c74 --- /dev/null +++ b/pkg/vm_service_interface/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 +- Initial release. +- Provides version 4.13 of the Dart VM service protocol. \ No newline at end of file diff --git a/pkg/vm_service_interface/CONTRIBUTING.md b/pkg/vm_service_interface/CONTRIBUTING.md new file mode 100644 index 00000000000..bd8b9832171 --- /dev/null +++ b/pkg/vm_service_interface/CONTRIBUTING.md @@ -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. \ No newline at end of file diff --git a/pkg/vm_service_interface/LICENSE b/pkg/vm_service_interface/LICENSE new file mode 100644 index 00000000000..4fd5739c6fe --- /dev/null +++ b/pkg/vm_service_interface/LICENSE @@ -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. diff --git a/pkg/vm_service_interface/OWNERS b/pkg/vm_service_interface/OWNERS new file mode 100644 index 00000000000..dc3a1d02753 --- /dev/null +++ b/pkg/vm_service_interface/OWNERS @@ -0,0 +1 @@ +file:/tools/OWNERS_VM diff --git a/pkg/vm_service_interface/README.md b/pkg/vm_service_interface/README.md new file mode 100644 index 00000000000..7fc94fba2fb --- /dev/null +++ b/pkg/vm_service_interface/README.md @@ -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 diff --git a/pkg/vm_service_interface/analysis_options.yaml b/pkg/vm_service_interface/analysis_options.yaml new file mode 100644 index 00000000000..adae03a5ef6 --- /dev/null +++ b/pkg/vm_service_interface/analysis_options.yaml @@ -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 diff --git a/pkg/vm_service_interface/lib/src/service_extension_registry.dart b/pkg/vm_service_interface/lib/src/service_extension_registry.dart new file mode 100644 index 00000000000..f651b68b3d6 --- /dev/null +++ b/pkg/vm_service_interface/lib/src/service_extension_registry.dart @@ -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 = {}; + + /// Controller for tracking registration and unregistration events. + final _eventController = StreamController.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 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 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, + ); +} diff --git a/pkg/vm_service_interface/lib/src/stream_helpers.dart b/pkg/vm_service_interface/lib/src/stream_helpers.dart new file mode 100644 index 00000000000..83f68479791 --- /dev/null +++ b/pkg/vm_service_interface/lib/src/stream_helpers.dart @@ -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 followedBy(Stream next) => _FollowedBy(next); + +class _FollowedBy extends StreamTransformerBase { + final Stream _next; + + _FollowedBy(this._next); + + @override + Stream bind(Stream first) { + var controller = first.isBroadcast + ? StreamController.broadcast(sync: true) + : StreamController(sync: true); + + var next = first.isBroadcast && !_next.isBroadcast + ? _next.asBroadcastStream() + : _next; + + StreamSubscription? 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 startWithMany(Iterable initial) => + startWithStream(Stream.fromIterable(initial)); + +StreamTransformer startWithStream(Stream initial) => + StreamTransformer.fromBind((values) { + if (values.isBroadcast && !initial.isBroadcast) { + initial = initial.asBroadcastStream(); + } + return initial.transform(followedBy(values)); + }); diff --git a/pkg/vm_service_interface/lib/src/vm_service_interface.dart b/pkg/vm_service_interface/lib/src/vm_service_interface.dart new file mode 100644 index 00000000000..699192ed760 --- /dev/null +++ b/pkg/vm_service_interface/lib/src/vm_service_interface.dart @@ -0,0 +1,1694 @@ +// 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; + +const String vmServiceVersion = '4.13.0'; + +/// 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 onEvent(String streamId); + + /// Handler for calling extra service extensions. + Future callServiceExtension(String method, + {String? isolateId, Map? args}); + + /// The `addBreakpoint` RPC is used to add a breakpoint at a specific line of + /// some script. + /// + /// The `scriptId` parameter is used to specify the target script. + /// + /// The `line` parameter is used to specify the target line for the + /// breakpoint. If there are multiple possible breakpoints on the target line, + /// then the VM will place the breakpoint at the location which would execute + /// soonest. If it is not possible to set a breakpoint at the target line, the + /// breakpoint will be added at the next possible breakpoint location within + /// the same function. + /// + /// The `column` parameter may be optionally specified. This is useful for + /// targeting a specific breakpoint on a line with multiple possible + /// breakpoints. + /// + /// If no breakpoint is possible at that line, the `102` (Cannot add + /// breakpoint) RPC error code is returned. + /// + /// Note that breakpoints are added and removed on a per-isolate basis. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Breakpoint]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future addBreakpoint( + String isolateId, + String scriptId, + int line, { + int? column, + }); + + /// The `addBreakpoint` RPC is used to add a breakpoint at a specific line of + /// some script. This RPC is useful when a script has not yet been assigned an + /// id, for example, if a script is in a deferred library which has not yet + /// been loaded. + /// + /// The `scriptUri` parameter is used to specify the target script. + /// + /// The `line` parameter is used to specify the target line for the + /// breakpoint. If there are multiple possible breakpoints on the target line, + /// then the VM will place the breakpoint at the location which would execute + /// soonest. If it is not possible to set a breakpoint at the target line, the + /// breakpoint will be added at the next possible breakpoint location within + /// the same function. + /// + /// The `column` parameter may be optionally specified. This is useful for + /// targeting a specific breakpoint on a line with multiple possible + /// breakpoints. + /// + /// If no breakpoint is possible at that line, the `102` (Cannot add + /// breakpoint) RPC error code is returned. + /// + /// Note that breakpoints are added and removed on a per-isolate basis. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Breakpoint]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future addBreakpointWithScriptUri( + String isolateId, + String scriptUri, + int line, { + int? column, + }); + + /// The `addBreakpointAtEntry` RPC is used to add a breakpoint at the + /// entrypoint of some function. + /// + /// If no breakpoint is possible at the function entry, the `102` (Cannot add + /// breakpoint) RPC error code is returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Breakpoint]. + /// + /// Note that breakpoints are added and removed on a per-isolate basis. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future addBreakpointAtEntry(String isolateId, String functionId); + + /// Clears all CPU profiling samples. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future clearCpuSamples(String isolateId); + + /// Clears all VM timeline events. + /// + /// See [Success]. + Future clearVMTimeline(); + + /// The `invoke` RPC is used to perform regular method invocation on some + /// receiver, as if by dart:mirror's ObjectMirror.invoke. Note this does not + /// provide a way to perform getter, setter or constructor invocation. + /// + /// `targetId` may refer to a [Library], [Class], or [Instance]. + /// + /// Each elements of `argumentId` may refer to an [Instance]. + /// + /// If `disableBreakpoints` is provided and set to true, any breakpoints hit + /// as a result of this invocation are ignored, including pauses resulting + /// from a call to `debugger()` from `dart:developer`. Defaults to false if + /// not provided. + /// + /// If `targetId` or any element of `argumentIds` is a temporary id which has + /// expired, then the `Expired` [Sentinel] is returned. + /// + /// If `targetId` or any element of `argumentIds` refers to an object which + /// has been collected by the VM's garbage collector, then the `Collected` + /// [Sentinel] is returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// If invocation triggers a failed compilation then [RPCError] 113 + /// "Expression compilation error" is returned. + /// + /// If a runtime error occurs while evaluating the invocation, an [ErrorRef] + /// reference will be returned. + /// + /// If the invocation is evaluated successfully, an [InstanceRef] reference + /// will be returned. + /// + /// The return value can be one of [InstanceRef] or [ErrorRef]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future invoke( + String isolateId, + String targetId, + String selector, + List argumentIds, { + bool? disableBreakpoints, + }); + + /// The `evaluate` RPC is used to evaluate an expression in the context of + /// some target. + /// + /// `targetId` may refer to a [Library], [Class], or [Instance]. + /// + /// If `targetId` is a temporary id which has expired, then the `Expired` + /// [Sentinel] is returned. + /// + /// If `targetId` refers to an object which has been collected by the VM's + /// garbage collector, then the `Collected` [Sentinel] is returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// If `scope` is provided, it should be a map from identifiers to object ids. + /// These bindings will be added to the scope in which the expression is + /// evaluated, which is a child scope of the class or library for + /// instance/class or library targets respectively. This means bindings + /// provided in `scope` may shadow instance members, class members and + /// top-level members. + /// + /// If `disableBreakpoints` is provided and set to true, any breakpoints hit + /// as a result of this evaluation are ignored. Defaults to false if not + /// provided. + /// + /// If the expression fails to parse and compile, then [RPCError] 113 + /// "Expression compilation error" is returned. + /// + /// If an error occurs while evaluating the expression, an [ErrorRef] + /// reference will be returned. + /// + /// If the expression is evaluated successfully, an [InstanceRef] reference + /// will be returned. + /// + /// The return value can be one of [InstanceRef] or [ErrorRef]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future evaluate( + String isolateId, + String targetId, + String expression, { + Map? scope, + bool? disableBreakpoints, + }); + + /// The `evaluateInFrame` RPC is used to evaluate an expression in the context + /// of a particular stack frame. `frameIndex` is the index of the desired + /// [Frame], with an index of `0` indicating the top (most recent) frame. + /// + /// If `scope` is provided, it should be a map from identifiers to object ids. + /// These bindings will be added to the scope in which the expression is + /// evaluated, which is a child scope of the frame's current scope. This means + /// bindings provided in `scope` may shadow instance members, class members, + /// top-level members, parameters and locals. + /// + /// If `disableBreakpoints` is provided and set to true, any breakpoints hit + /// as a result of this evaluation are ignored. Defaults to false if not + /// provided. + /// + /// If the expression fails to parse and compile, then [RPCError] 113 + /// "Expression compilation error" is returned. + /// + /// If an error occurs while evaluating the expression, an [ErrorRef] + /// reference will be returned. + /// + /// If the expression is evaluated successfully, an [InstanceRef] reference + /// will be returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// The return value can be one of [InstanceRef] or [ErrorRef]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future evaluateInFrame( + String isolateId, + int frameIndex, + String expression, { + Map? scope, + bool? disableBreakpoints, + }); + + /// The `getAllocationProfile` RPC is used to retrieve allocation information + /// for a given isolate. + /// + /// If `reset` is provided and is set to true, the allocation accumulators + /// will be reset before collecting allocation information. + /// + /// If `gc` is provided and is set to true, a garbage collection will be + /// attempted before collecting allocation information. There is no guarantee + /// that a garbage collection will be actually be performed. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getAllocationProfile(String isolateId, + {bool? reset, bool? gc}); + + /// The `getAllocationTraces` RPC allows for the retrieval of allocation + /// traces for objects of a specific set of types (see + /// [VmServiceInterface.setTraceClassAllocation]). Only samples collected in + /// the time range `[timeOriginMicros, timeOriginMicros + timeExtentMicros]` + /// will be reported. + /// + /// If `classId` is provided, only traces for allocations with the matching + /// `classId` will be reported. + /// + /// If the profiler is disabled, an RPC error response will be returned. + /// + /// If isolateId refers to an isolate which has exited, then the Collected + /// Sentinel is returned. + /// + /// See [CpuSamples]. + Future getAllocationTraces( + String isolateId, { + int? timeOriginMicros, + int? timeExtentMicros, + String? classId, + }); + + /// The `getClassList` RPC is used to retrieve a `ClassList` containing all + /// classes for an isolate based on the isolate's `isolateId`. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [ClassList]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getClassList(String isolateId); + + /// The `getCpuSamples` RPC is used to retrieve samples collected by the CPU + /// profiler. See [CpuSamples] for a detailed description of the response. + /// + /// The `timeOriginMicros` parameter is the beginning of the time range used + /// to filter samples. It uses the same monotonic clock as dart:developer's + /// `Timeline.now` and the VM embedding API's `Dart_TimelineGetMicros`. See + /// [VmServiceInterface.getVMTimelineMicros] for access to this clock through + /// the service protocol. + /// + /// The `timeExtentMicros` parameter specifies how large the time range used + /// to filter samples should be. + /// + /// For example, given `timeOriginMicros` and `timeExtentMicros`, only samples + /// from the following time range will be returned: `(timeOriginMicros, + /// timeOriginMicros + timeExtentMicros)`. + /// + /// If the profiler is disabled, an [RPCError] response will be returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getCpuSamples( + String isolateId, int timeOriginMicros, int timeExtentMicros); + + /// The `getFlagList` RPC returns a list of all command line flags in the VM + /// along with their current values. + /// + /// See [FlagList]. + Future getFlagList(); + + /// Returns a set of inbound references to the object specified by `targetId`. + /// Up to `limit` references will be returned. + /// + /// The order of the references is undefined (i.e., not related to allocation + /// order) and unstable (i.e., multiple invocations of this method against the + /// same object can give different answers even if no Dart code has executed + /// between the invocations). + /// + /// The references may include multiple `objectId`s that designate the same + /// object. + /// + /// The references may include objects that are unreachable but have not yet + /// been garbage collected. + /// + /// If `targetId` is a temporary id which has expired, then the `Expired` + /// [Sentinel] is returned. + /// + /// If `targetId` refers to an object which has been collected by the VM's + /// garbage collector, then the `Collected` [Sentinel] is returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [InboundReferences]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getInboundReferences( + String isolateId, String targetId, int limit); + + /// The `getInstances` RPC is used to retrieve a set of instances which are of + /// a specific class. + /// + /// The order of the instances is undefined (i.e., not related to allocation + /// order) and unstable (i.e., multiple invocations of this method against the + /// same class can give different answers even if no Dart code has executed + /// between the invocations). + /// + /// The set of instances may include objects that are unreachable but have not + /// yet been garbage collected. + /// + /// `objectId` is the ID of the `Class` to retrieve instances for. `objectId` + /// must be the ID of a `Class`, otherwise an [RPCError] is returned. + /// + /// `limit` is the maximum number of instances to be returned. + /// + /// If `includeSubclasses` is true, instances of subclasses of the specified + /// class will be included in the set. + /// + /// If `includeImplementers` is true, instances of implementers of the + /// specified class will be included in the set. Note that subclasses of a + /// class are also considered implementers of that class. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [InstanceSet]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getInstances( + String isolateId, + String objectId, + int limit, { + bool? includeSubclasses, + bool? includeImplementers, + }); + + /// The `getInstancesAsList` RPC is used to retrieve a set of instances which + /// are of a specific class. This RPC returns an `InstanceRef` corresponding + /// to a Dart `List` that contains the requested instances. This + /// `List` is not growable, but it is otherwise mutable. The response type is + /// what distinguishes this RPC from `getInstances`, which returns an + /// `InstanceSet`. + /// + /// The order of the instances is undefined (i.e., not related to allocation + /// order) and unstable (i.e., multiple invocations of this method against the + /// same class can give different answers even if no Dart code has executed + /// between the invocations). + /// + /// The set of instances may include objects that are unreachable but have not + /// yet been garbage collected. + /// + /// `objectId` is the ID of the `Class` to retrieve instances for. `objectId` + /// must be the ID of a `Class`, otherwise an [RPCError] is returned. + /// + /// If `includeSubclasses` is true, instances of subclasses of the specified + /// class will be included in the set. + /// + /// If `includeImplementers` is true, instances of implementers of the + /// specified class will be included in the set. Note that subclasses of a + /// class are also considered implementers of that class. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getInstancesAsList( + String isolateId, + String objectId, { + bool? includeSubclasses, + bool? includeImplementers, + }); + + /// The `getIsolate` RPC is used to lookup an `Isolate` object by its `id`. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Isolate]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getIsolate(String isolateId); + + /// The `getIsolateGroup` RPC is used to lookup an `IsolateGroup` object by + /// its `id`. + /// + /// If `isolateGroupId` refers to an isolate group which has exited, then the + /// `Expired` [Sentinel] is returned. + /// + /// `IsolateGroup` `id` is an opaque identifier that can be fetched from an + /// `IsolateGroup`. List of active `IsolateGroup`'s, for example, is available + /// on `VM` object. + /// + /// See [IsolateGroup], [VM]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getIsolateGroup(String isolateGroupId); + + /// The `getIsolatePauseEvent` RPC is used to lookup an isolate's pause event + /// by its `id`. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Isolate]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getIsolatePauseEvent(String isolateId); + + /// The `getMemoryUsage` RPC is used to lookup an isolate's memory usage + /// statistics by its `id`. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Isolate]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getMemoryUsage(String isolateId); + + /// The `getIsolateGroupMemoryUsage` RPC is used to lookup an isolate group's + /// memory usage statistics by its `id`. + /// + /// If `isolateGroupId` refers to an isolate group which has exited, then the + /// `Expired` [Sentinel] is returned. + /// + /// See [IsolateGroup]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getIsolateGroupMemoryUsage(String isolateGroupId); + + /// The `getScripts` RPC is used to retrieve a `ScriptList` containing all + /// scripts for an isolate based on the isolate's `isolateId`. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [ScriptList]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getScripts(String isolateId); + + /// The `getObject` RPC is used to lookup an `object` from some isolate by its + /// `id`. + /// + /// If `objectId` is a temporary id which has expired, then the `Expired` + /// [Sentinel] is returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// If `objectId` refers to a heap object which has been collected by the VM's + /// garbage collector, then the `Collected` [Sentinel] is returned. + /// + /// If `objectId` refers to a non-heap object which has been deleted, then the + /// `Collected` [Sentinel] is returned. + /// + /// If the object handle has not expired and the object has not been + /// collected, then an [Obj] will be returned. + /// + /// The `offset` and `count` parameters are used to request subranges of + /// Instance objects with the kinds: String, List, Map, Set, Uint8ClampedList, + /// Uint8List, Uint16List, Uint32List, Uint64List, Int8List, Int16List, + /// Int32List, Int64List, Float32List, Float64List, Inst32x3List, + /// Float32x4List, and Float64x2List. These parameters are otherwise ignored. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getObject( + String isolateId, + String objectId, { + int? offset, + int? count, + }); + + /// The `getPerfettoCpuSamples` RPC is used to retrieve samples collected by + /// the CPU profiler, serialized in Perfetto's proto format. See + /// [PerfettoCpuSamples] for a detailed description of the response. + /// + /// The `timeOriginMicros` parameter is the beginning of the time range used + /// to filter samples. It uses the same monotonic clock as dart:developer's + /// `Timeline.now` and the VM embedding API's `Dart_TimelineGetMicros`. See + /// [VmServiceInterface.getVMTimelineMicros] for access to this clock through + /// the service protocol. + /// + /// The `timeExtentMicros` parameter specifies how large the time range used + /// to filter samples should be. + /// + /// For example, given `timeOriginMicros` and `timeExtentMicros`, only samples + /// from the following time range will be returned: `(timeOriginMicros, + /// timeOriginMicros + timeExtentMicros)`. + /// + /// If the profiler is disabled, an [RPCError] response will be returned. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getPerfettoCpuSamples(String isolateId, + {int? timeOriginMicros, int? timeExtentMicros}); + + /// The `getPerfettoVMTimeline` RPC is used to retrieve an object which + /// contains a VM timeline trace represented in Perfetto's proto format. See + /// [PerfettoTimeline] for a detailed description of the response. + /// + /// The `timeOriginMicros` parameter is the beginning of the time range used + /// to filter timeline events. It uses the same monotonic clock as + /// dart:developer's `Timeline.now` and the VM embedding API's + /// `Dart_TimelineGetMicros`. See [VmServiceInterface.getVMTimelineMicros] for + /// access to this clock through the service protocol. + /// + /// The `timeExtentMicros` parameter specifies how large the time range used + /// to filter timeline events should be. + /// + /// For example, given `timeOriginMicros` and `timeExtentMicros`, only + /// timeline events from the following time range will be returned: + /// `(timeOriginMicros, timeOriginMicros + timeExtentMicros)`. + /// + /// If `getPerfettoVMTimeline` is invoked while the current recorder is + /// Callback, an [RPCError] with error code `114`, `invalid timeline request`, + /// will be returned as timeline events are handled by the embedder in this + /// mode. + /// + /// If `getPerfettoVMTimeline` is invoked while the current recorder is one of + /// Fuchsia or Macos or Systrace, an [RPCError] with error code `114`, + /// `invalid timeline request`, will be returned as timeline events are + /// handled by the OS in these modes. + /// + /// If `getPerfettoVMTimeline` is invoked while the current recorder is File + /// or Perfettofile, an [RPCError] with error code `114`, `invalid timeline + /// request`, will be returned as timeline events are written directly to a + /// file, and thus cannot be retrieved through the VM Service, in these modes. + Future getPerfettoVMTimeline( + {int? timeOriginMicros, int? timeExtentMicros}); + + /// The `getPorts` RPC is used to retrieve the list of `ReceivePort` instances + /// for a given isolate. + /// + /// See [PortList]. + Future getPorts(String isolateId); + + /// The `getRetainingPath` RPC is used to lookup a path from an object + /// specified by `targetId` to a GC root (i.e., the object which is preventing + /// this object from being garbage collected). + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// If `targetId` refers to a heap object which has been collected by the VM's + /// garbage collector, then the `Collected` [Sentinel] is returned. + /// + /// If `targetId` refers to a non-heap object which has been deleted, then the + /// `Collected` [Sentinel] is returned. + /// + /// If the object handle has not expired and the object has not been + /// collected, then an [RetainingPath] will be returned. + /// + /// The `limit` parameter specifies the maximum path length to be reported as + /// part of the retaining path. If a path is longer than `limit`, it will be + /// truncated at the root end of the path. + /// + /// See [RetainingPath]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getRetainingPath( + String isolateId, String targetId, int limit); + + /// Returns a description of major uses of memory known to the VM. + /// + /// Adding or removing buckets is considered a backwards-compatible change for + /// the purposes of versioning. A client must gracefully handle the removal or + /// addition of any bucket. + Future getProcessMemoryUsage(); + + /// The `getStack` RPC is used to retrieve the current execution stack and + /// message queue for an isolate. The isolate does not need to be paused. + /// + /// If `limit` is provided, up to `limit` frames from the top of the stack + /// will be returned. If the stack depth is smaller than `limit` the entire + /// stack is returned. Note: this limit also applies to the + /// `asyncCausalFrames` stack representation in the `Stack` response. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Stack]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getStack(String isolateId, {int? limit}); + + /// The `getSupportedProtocols` RPC is used to determine which protocols are + /// supported by the current server. + /// + /// The result of this call should be intercepted by any middleware that + /// extends the core VM service protocol and should add its own protocol to + /// the list of protocols before forwarding the response to the client. + /// + /// See [ProtocolList]. + Future getSupportedProtocols(); + + /// The `getSourceReport` RPC is used to generate a set of reports tied to + /// source locations in an isolate. + /// + /// The `reports` parameter is used to specify which reports should be + /// generated. The `reports` parameter is a list, which allows multiple + /// reports to be generated simultaneously from a consistent isolate state. + /// The `reports` parameter is allowed to be empty (this might be used to + /// force compilation of a particular subrange of some script). + /// + /// The available report kinds are: + /// + /// report kind | meaning + /// ----------- | ------- + /// Coverage | Provide code coverage information + /// PossibleBreakpoints | Provide a list of token positions which correspond + /// to possible breakpoints. + /// + /// The `scriptId` parameter is used to restrict the report to a particular + /// script. When analyzing a particular script, either or both of the + /// `tokenPos` and `endTokenPos` parameters may be provided to restrict the + /// analysis to a subrange of a script (for example, these can be used to + /// restrict the report to the range of a particular class or function). + /// + /// If the `scriptId` parameter is not provided then the reports are generated + /// for all loaded scripts and the `tokenPos` and `endTokenPos` parameters are + /// disallowed. + /// + /// The `forceCompilation` parameter can be used to force compilation of all + /// functions in the range of the report. Forcing compilation can cause a + /// compilation error, which could terminate the running Dart program. If this + /// parameter is not provided, it is considered to have the value `false`. + /// + /// The `reportLines` parameter changes the token positions in + /// `SourceReportRange.possibleBreakpoints` and `SourceReportCoverage` to be + /// line numbers. This is designed to reduce the number of RPCs that need to + /// be performed in the case that the client is only interested in line + /// numbers. If this parameter is not provided, it is considered to have the + /// value `false`. + /// + /// The `libraryFilters` parameter is intended to be used when gathering + /// coverage for the whole isolate. If it is provided, the `SourceReport` will + /// only contain results from scripts with URIs that start with one of the + /// filter strings. For example, pass `["package:foo/"]` to only include + /// scripts from the foo package. + /// + /// The `librariesAlreadyCompiled` parameter overrides the `forceCompilation` + /// parameter on a per-library basis, setting it to `false` for any libary in + /// this list. This is useful for cases where multiple `getSourceReport` RPCs + /// are sent with `forceCompilation` enabled, to avoid recompiling the same + /// libraries repeatedly. To use this parameter, enable `forceCompilation`, + /// cache the results of each `getSourceReport` RPC, and pass all the + /// libraries mentioned in the `SourceReport` to subsequent RPCs in the + /// `librariesAlreadyCompiled`. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [SourceReport]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future getSourceReport( + String isolateId, + /*List*/ List reports, { + String? scriptId, + int? tokenPos, + int? endTokenPos, + bool? forceCompile, + bool? reportLines, + List? libraryFilters, + List? librariesAlreadyCompiled, + }); + + /// The `getVersion` RPC is used to determine what version of the Service + /// Protocol is served by a VM. + /// + /// See [Version]. + Future getVersion(); + + /// The `getVM` RPC returns global information about a Dart virtual machine. + /// + /// See [VM]. + Future getVM(); + + /// The `getVMTimeline` RPC is used to retrieve an object which contains VM + /// timeline events. See [Timeline] for a detailed description of the + /// response. + /// + /// The `timeOriginMicros` parameter is the beginning of the time range used + /// to filter timeline events. It uses the same monotonic clock as + /// dart:developer's `Timeline.now` and the VM embedding API's + /// `Dart_TimelineGetMicros`. See [VmServiceInterface.getVMTimelineMicros] for + /// access to this clock through the service protocol. + /// + /// The `timeExtentMicros` parameter specifies how large the time range used + /// to filter timeline events should be. + /// + /// For example, given `timeOriginMicros` and `timeExtentMicros`, only + /// timeline events from the following time range will be returned: + /// `(timeOriginMicros, timeOriginMicros + timeExtentMicros)`. + /// + /// If `getVMTimeline` is invoked while the current recorder is Callback, an + /// [RPCError] with error code `114`, `invalid timeline request`, will be + /// returned as timeline events are handled by the embedder in this mode. + /// + /// If `getVMTimeline` is invoked while the current recorder is one of Fuchsia + /// or Macos or Systrace, an [RPCError] with error code `114`, `invalid + /// timeline request`, will be returned as timeline events are handled by the + /// OS in these modes. + /// + /// If `getVMTimeline` is invoked while the current recorder is File or + /// Perfettofile, an [RPCError] with error code `114`, `invalid timeline + /// request`, will be returned as timeline events are written directly to a + /// file, and thus cannot be retrieved through the VM Service, in these modes. + Future getVMTimeline( + {int? timeOriginMicros, int? timeExtentMicros}); + + /// The `getVMTimelineFlags` RPC returns information about the current VM + /// timeline configuration. + /// + /// To change which timeline streams are currently enabled, see + /// [VmServiceInterface.setVMTimelineFlags]. + /// + /// See [TimelineFlags]. + Future getVMTimelineFlags(); + + /// The `getVMTimelineMicros` RPC returns the current time stamp from the + /// clock used by the timeline, similar to `Timeline.now` in `dart:developer` + /// and `Dart_TimelineGetMicros` in the VM embedding API. + /// + /// See [Timestamp] and [VmServiceInterface.getVMTimeline]. + Future getVMTimelineMicros(); + + /// The `pause` RPC is used to interrupt a running isolate. The RPC enqueues + /// the interrupt request and potentially returns before the isolate is + /// paused. + /// + /// When the isolate is paused an event will be sent on the `Debug` stream. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future pause(String isolateId); + + /// The `kill` RPC is used to kill an isolate as if by dart:isolate's + /// `Isolate.kill(IMMEDIATE)`. + /// + /// The isolate is killed regardless of whether it is paused or running. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future kill(String isolateId); + + /// The `lookupResolvedPackageUris` RPC is used to convert a list of URIs to + /// their resolved (or absolute) paths. For example, URIs passed to this RPC + /// are mapped in the following ways: + /// + /// - `dart:io` -> `org-dartlang-sdk:///sdk/lib/io/io.dart` + /// - `package:test/test.dart` -> + /// `file:///$PACKAGE_INSTALLATION_DIR/lib/test.dart` + /// - `file:///foo/bar/bazz.dart` -> `file:///foo/bar/bazz.dart` + /// + /// If a URI is not known, the corresponding entry in the [UriList] response + /// will be `null`. + /// + /// If `local` is true, the VM will attempt to return local file paths instead + /// of relative paths, but this is not guaranteed. + /// + /// See [UriList]. + Future lookupResolvedPackageUris(String isolateId, List uris, + {bool? local}); + + /// The `lookupPackageUris` RPC is used to convert a list of URIs to their + /// unresolved paths. For example, URIs passed to this RPC are mapped in the + /// following ways: + /// + /// - `org-dartlang-sdk:///sdk/lib/io/io.dart` -> `dart:io` + /// - `file:///$PACKAGE_INSTALLATION_DIR/lib/test.dart` -> + /// `package:test/test.dart` + /// - `file:///foo/bar/bazz.dart` -> `file:///foo/bar/bazz.dart` + /// + /// If a URI is not known, the corresponding entry in the [UriList] response + /// will be `null`. + /// + /// See [UriList]. + Future lookupPackageUris(String isolateId, List uris); + + /// Registers a service that can be invoked by other VM service clients, where + /// `service` is the name of the service to advertise and `alias` is an + /// alternative name for the registered service. + /// + /// Requests made to the new service will be forwarded to the client which + /// originally registered the service. + /// + /// See [Success]. + Future registerService(String service, String alias); + + /// The `reloadSources` RPC is used to perform a hot reload of the sources of + /// all isolates in the same isolate group as the isolate specified by + /// `isolateId`. + /// + /// If the `force` parameter is provided, it indicates that all sources should + /// be reloaded regardless of modification time. + /// + /// The `pause` parameter has been deprecated, so providing it no longer has + /// any effect. + /// + /// If the `rootLibUri` parameter is provided, it indicates the new uri to the + /// isolate group's root library. + /// + /// If the `packagesUri` parameter is provided, it indicates the new uri to + /// the isolate group's package map (.packages) file. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future reloadSources( + String isolateId, { + bool? force, + bool? pause, + String? rootLibUri, + String? packagesUri, + }); + + /// The `removeBreakpoint` RPC is used to remove a breakpoint by its `id`. + /// + /// Note that breakpoints are added and removed on a per-isolate basis. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future removeBreakpoint(String isolateId, String breakpointId); + + /// Requests a dump of the Dart heap of the given isolate. + /// + /// This method immediately returns success. The VM will then begin delivering + /// binary events on the `HeapSnapshot` event stream. The binary data in these + /// events, when concatenated together, conforms to the [HeapSnapshotGraph] + /// type. The splitting of the SnapshotGraph into events can happen at any + /// byte offset. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future requestHeapSnapshot(String isolateId); + + /// The `resume` RPC is used to resume execution of a paused isolate. + /// + /// If the `step` parameter is not provided, the program will resume regular + /// execution. + /// + /// If the `step` parameter is provided, it indicates what form of + /// single-stepping to use. + /// + /// step | meaning + /// ---- | ------- + /// Into | Single step, entering function calls + /// Over | Single step, skipping over function calls + /// Out | Single step until the current function exits + /// Rewind | Immediately exit the top frame(s) without executing any code. + /// Isolate will be paused at the call of the last exited function. + /// + /// The `frameIndex` parameter is only used when the `step` parameter is + /// Rewind. It specifies the stack frame to rewind to. Stack frame 0 is the + /// currently executing function, so `frameIndex` must be at least 1. + /// + /// If the `frameIndex` parameter is not provided, it defaults to 1. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success], [StepOption]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future resume(String isolateId, + {/*StepOption*/ String? step, int? frameIndex}); + + /// The `setBreakpointState` RPC allows for breakpoints to be enabled or + /// disabled, without requiring for the breakpoint to be completely removed. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// The returned [Breakpoint] is the updated breakpoint with its new values. + /// + /// See [Breakpoint]. + Future setBreakpointState( + String isolateId, String breakpointId, bool enable); + + /// The `setExceptionPauseMode` RPC is used to control if an isolate pauses + /// when an exception is thrown. + /// + /// mode | meaning + /// ---- | ------- + /// None | Do not pause isolate on thrown exceptions + /// Unhandled | Pause isolate on unhandled exceptions + /// All | Pause isolate on all thrown exceptions + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + @Deprecated('Use setIsolatePauseMode instead') + Future setExceptionPauseMode( + String isolateId, /*ExceptionPauseMode*/ String mode); + + /// The `setIsolatePauseMode` RPC is used to control if or when an isolate + /// will pause due to a change in execution state. + /// + /// The `shouldPauseOnExit` parameter specify whether the target isolate + /// should pause on exit. + /// + /// mode | meaning + /// ---- | ------- + /// None | Do not pause isolate on thrown exceptions + /// Unhandled | Pause isolate on unhandled exceptions + /// All | Pause isolate on all thrown exceptions + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future setIsolatePauseMode(String isolateId, + {/*ExceptionPauseMode*/ String? exceptionPauseMode, + bool? shouldPauseOnExit}); + + /// The `setFlag` RPC is used to set a VM flag at runtime. Returns an error if + /// the named flag does not exist, the flag may not be set at runtime, or the + /// value is of the wrong type for the flag. + /// + /// The following flags may be set at runtime: + /// + /// - pause_isolates_on_start + /// - pause_isolates_on_exit + /// - pause_isolates_on_unhandled_exceptions + /// - profile_period + /// - profiler + /// + /// Notes: + /// + /// - `profile_period` can be set to a minimum value of 50. Attempting to set + /// `profile_period` to a lower value will result in a value of 50 being set. + /// - Setting `profiler` will enable or disable the profiler depending on the + /// provided value. If set to false when the profiler is already running, the + /// profiler will be stopped but may not free its sample buffer depending on + /// platform limitations. + /// - Isolate pause settings will only be applied to newly spawned isolates. + /// + /// See [Success]. + /// + /// The return value can be one of [Success] or [Error]. + Future setFlag(String name, String value); + + /// The `setLibraryDebuggable` RPC is used to enable or disable whether + /// breakpoints and stepping work for a given library. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future setLibraryDebuggable( + String isolateId, String libraryId, bool isDebuggable); + + /// The `setName` RPC is used to change the debugging name for an isolate. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future setName(String isolateId, String name); + + /// The `setTraceClassAllocation` RPC allows for enabling or disabling + /// allocation tracing for a specific type of object. Allocation traces can be + /// retrieved with the `getAllocationTraces` RPC. + /// + /// If `enable` is true, allocations of objects of the class represented by + /// `classId` will be traced. + /// + /// If `isolateId` refers to an isolate which has exited, then the `Collected` + /// [Sentinel] is returned. + /// + /// See [Success]. + /// + /// This method will throw a [SentinelException] in the case a [Sentinel] is + /// returned. + Future setTraceClassAllocation( + String isolateId, String classId, bool enable); + + /// The `setVMName` RPC is used to change the debugging name for the vm. + /// + /// See [Success]. + Future setVMName(String name); + + /// The `setVMTimelineFlags` RPC is used to set which timeline streams are + /// enabled. + /// + /// The `recordedStreams` parameter is the list of all timeline streams which + /// are to be enabled. Streams not explicitly specified will be disabled. + /// Invalid stream names are ignored. + /// + /// A `TimelineStreamSubscriptionsUpdate` event is sent on the `Timeline` + /// stream as a result of invoking this RPC. + /// + /// To get the list of currently enabled timeline streams, see + /// [VmServiceInterface.getVMTimelineFlags]. + /// + /// See [Success]. + Future setVMTimelineFlags(List recordedStreams); + + /// The `streamCancel` RPC cancels a stream subscription in the VM. + /// + /// If the client is not subscribed to the stream, the `104` (Stream not + /// subscribed) RPC error code is returned. + /// + /// See [Success]. + Future streamCancel(String streamId); + + /// The `streamCpuSamplesWithUserTag` RPC allows for clients to specify which + /// CPU samples collected by the profiler should be sent over the `Profiler` + /// stream. When called, the VM will stream `CpuSamples` events containing + /// `CpuSample`'s collected while a user tag contained in `userTags` was + /// active. + /// + /// See [Success]. + Future streamCpuSamplesWithUserTag(List userTags); + + /// The `streamListen` RPC subscribes to a stream in the VM. Once subscribed, + /// the client will begin receiving events from the stream. + /// + /// If the client is already subscribed to the stream, the `103` (Stream + /// already subscribed) RPC error code is returned. + /// + /// The `streamId` parameter may have the following published values: + /// + /// streamId | event types provided + /// -------- | ----------- + /// VM | VMUpdate, VMFlagUpdate + /// Isolate | IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, + /// IsolateReload, ServiceExtensionAdded + /// Debug | PauseStart, PauseExit, PauseBreakpoint, PauseInterrupted, + /// PauseException, PausePostRequest, Resume, BreakpointAdded, + /// BreakpointResolved, BreakpointRemoved, BreakpointUpdated, Inspect, None + /// Profiler | CpuSamples, UserTagChanged + /// GC | GC + /// Extension | Extension + /// Timeline | TimelineEvents, TimelineStreamsSubscriptionUpdate + /// Logging | Logging + /// Service | ServiceRegistered, ServiceUnregistered + /// HeapSnapshot | HeapSnapshot + /// + /// Additionally, some embedders provide the `Stdout` and `Stderr` streams. + /// These streams allow the client to subscribe to writes to stdout and + /// stderr. + /// + /// streamId | event types provided + /// -------- | ----------- + /// Stdout | WriteEvent + /// Stderr | WriteEvent + /// + /// It is considered a `backwards compatible` change to add a new type of + /// event to an existing stream. Clients should be written to handle this + /// gracefully, perhaps by warning and ignoring. + /// + /// See [Success]. + Future streamListen(String streamId); +} + +class _PendingServiceRequest { + Future> get future => _completer.future; + final _completer = Completer>(); + + final dynamic originalId; + + _PendingServiceRequest(this.originalId); + + void complete(Map 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> _requestStream; + final StreamSink> _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 = {}; + + /// Completes when [_requestStream] is done. + Future get done => _doneCompleter.future; + final _doneCompleter = Completer(); + + /// Pending service extension requests to this client by id. + final _pendingServiceExtensionRequests = {}; + + 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> _forwardServiceExtensionRequest( + Map request) { + final originalId = request['id']; + request = Map.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 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.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?; + late Response response; + + switch (method) { + case 'registerService': + _serviceExtensionRegistry.registerExtension(params!['service'], this); + response = Success(); + break; + case 'addBreakpoint': + response = await _serviceImplementation.addBreakpoint( + params!['isolateId'], + params['scriptId'], + params['line'], + column: params['column'], + ); + break; + case 'addBreakpointWithScriptUri': + response = await _serviceImplementation.addBreakpointWithScriptUri( + params!['isolateId'], + params['scriptUri'], + params['line'], + column: params['column'], + ); + break; + case 'addBreakpointAtEntry': + response = await _serviceImplementation.addBreakpointAtEntry( + params!['isolateId'], + params['functionId'], + ); + break; + case 'clearCpuSamples': + response = await _serviceImplementation.clearCpuSamples( + params!['isolateId'], + ); + break; + case 'clearVMTimeline': + response = await _serviceImplementation.clearVMTimeline(); + break; + case 'invoke': + response = await _serviceImplementation.invoke( + params!['isolateId'], + params['targetId'], + params['selector'], + List.from(params['argumentIds'] ?? []), + disableBreakpoints: params['disableBreakpoints'], + ); + break; + case 'evaluate': + response = await _serviceImplementation.evaluate( + params!['isolateId'], + params['targetId'], + params['expression'], + scope: params['scope']?.cast(), + disableBreakpoints: params['disableBreakpoints'], + ); + break; + case 'evaluateInFrame': + response = await _serviceImplementation.evaluateInFrame( + params!['isolateId'], + params['frameIndex'], + params['expression'], + scope: params['scope']?.cast(), + disableBreakpoints: params['disableBreakpoints'], + ); + break; + case 'getAllocationProfile': + response = await _serviceImplementation.getAllocationProfile( + params!['isolateId'], + reset: params['reset'], + gc: params['gc'], + ); + break; + case 'getAllocationTraces': + response = await _serviceImplementation.getAllocationTraces( + params!['isolateId'], + timeOriginMicros: params['timeOriginMicros'], + timeExtentMicros: params['timeExtentMicros'], + classId: params['classId'], + ); + break; + case 'getClassList': + response = await _serviceImplementation.getClassList( + params!['isolateId'], + ); + break; + case 'getCpuSamples': + response = await _serviceImplementation.getCpuSamples( + params!['isolateId'], + params['timeOriginMicros'], + params['timeExtentMicros'], + ); + break; + case 'getFlagList': + response = await _serviceImplementation.getFlagList(); + break; + case 'getInboundReferences': + response = await _serviceImplementation.getInboundReferences( + params!['isolateId'], + params['targetId'], + params['limit'], + ); + break; + case 'getInstances': + response = await _serviceImplementation.getInstances( + params!['isolateId'], + params['objectId'], + params['limit'], + includeSubclasses: params['includeSubclasses'], + includeImplementers: params['includeImplementers'], + ); + break; + case 'getInstancesAsList': + response = await _serviceImplementation.getInstancesAsList( + params!['isolateId'], + params['objectId'], + includeSubclasses: params['includeSubclasses'], + includeImplementers: params['includeImplementers'], + ); + break; + case 'getIsolate': + response = await _serviceImplementation.getIsolate( + params!['isolateId'], + ); + break; + case 'getIsolateGroup': + response = await _serviceImplementation.getIsolateGroup( + params!['isolateGroupId'], + ); + break; + case 'getIsolatePauseEvent': + response = await _serviceImplementation.getIsolatePauseEvent( + params!['isolateId'], + ); + break; + case 'getMemoryUsage': + response = await _serviceImplementation.getMemoryUsage( + params!['isolateId'], + ); + break; + case 'getIsolateGroupMemoryUsage': + response = await _serviceImplementation.getIsolateGroupMemoryUsage( + params!['isolateGroupId'], + ); + break; + case 'getScripts': + response = await _serviceImplementation.getScripts( + params!['isolateId'], + ); + break; + case 'getObject': + response = await _serviceImplementation.getObject( + params!['isolateId'], + params['objectId'], + offset: params['offset'], + count: params['count'], + ); + break; + case 'getPerfettoCpuSamples': + response = await _serviceImplementation.getPerfettoCpuSamples( + params!['isolateId'], + timeOriginMicros: params['timeOriginMicros'], + timeExtentMicros: params['timeExtentMicros'], + ); + break; + case 'getPerfettoVMTimeline': + response = await _serviceImplementation.getPerfettoVMTimeline( + timeOriginMicros: params!['timeOriginMicros'], + timeExtentMicros: params['timeExtentMicros'], + ); + break; + case 'getPorts': + response = await _serviceImplementation.getPorts( + params!['isolateId'], + ); + break; + case 'getRetainingPath': + response = await _serviceImplementation.getRetainingPath( + params!['isolateId'], + params['targetId'], + params['limit'], + ); + break; + case 'getProcessMemoryUsage': + response = await _serviceImplementation.getProcessMemoryUsage(); + break; + case 'getStack': + response = await _serviceImplementation.getStack( + params!['isolateId'], + limit: params['limit'], + ); + break; + case 'getSupportedProtocols': + response = await _serviceImplementation.getSupportedProtocols(); + break; + case 'getSourceReport': + response = await _serviceImplementation.getSourceReport( + params!['isolateId'], + List.from(params['reports'] ?? []), + scriptId: params['scriptId'], + tokenPos: params['tokenPos'], + endTokenPos: params['endTokenPos'], + forceCompile: params['forceCompile'], + reportLines: params['reportLines'], + libraryFilters: params['libraryFilters'], + librariesAlreadyCompiled: params['librariesAlreadyCompiled'], + ); + break; + case 'getVersion': + response = await _serviceImplementation.getVersion(); + break; + case 'getVM': + response = await _serviceImplementation.getVM(); + break; + case 'getVMTimeline': + response = await _serviceImplementation.getVMTimeline( + timeOriginMicros: params!['timeOriginMicros'], + timeExtentMicros: params['timeExtentMicros'], + ); + break; + case 'getVMTimelineFlags': + response = await _serviceImplementation.getVMTimelineFlags(); + break; + case 'getVMTimelineMicros': + response = await _serviceImplementation.getVMTimelineMicros(); + break; + case 'pause': + response = await _serviceImplementation.pause( + params!['isolateId'], + ); + break; + case 'kill': + response = await _serviceImplementation.kill( + params!['isolateId'], + ); + break; + case 'lookupResolvedPackageUris': + response = await _serviceImplementation.lookupResolvedPackageUris( + params!['isolateId'], + List.from(params['uris'] ?? []), + local: params['local'], + ); + break; + case 'lookupPackageUris': + response = await _serviceImplementation.lookupPackageUris( + params!['isolateId'], + List.from(params['uris'] ?? []), + ); + break; + case 'reloadSources': + response = await _serviceImplementation.reloadSources( + params!['isolateId'], + force: params['force'], + pause: params['pause'], + rootLibUri: params['rootLibUri'], + packagesUri: params['packagesUri'], + ); + break; + case 'removeBreakpoint': + response = await _serviceImplementation.removeBreakpoint( + params!['isolateId'], + params['breakpointId'], + ); + break; + case 'requestHeapSnapshot': + response = await _serviceImplementation.requestHeapSnapshot( + params!['isolateId'], + ); + break; + case 'resume': + response = await _serviceImplementation.resume( + params!['isolateId'], + step: params['step'], + frameIndex: params['frameIndex'], + ); + break; + case 'setBreakpointState': + response = await _serviceImplementation.setBreakpointState( + params!['isolateId'], + params['breakpointId'], + params['enable'], + ); + break; + case 'setExceptionPauseMode': + // ignore: deprecated_member_use_from_same_package + response = await _serviceImplementation.setExceptionPauseMode( + params!['isolateId'], + params['mode'], + ); + break; + case 'setIsolatePauseMode': + response = await _serviceImplementation.setIsolatePauseMode( + params!['isolateId'], + exceptionPauseMode: params['exceptionPauseMode'], + shouldPauseOnExit: params['shouldPauseOnExit'], + ); + break; + case 'setFlag': + response = await _serviceImplementation.setFlag( + params!['name'], + params['value'], + ); + break; + case 'setLibraryDebuggable': + response = await _serviceImplementation.setLibraryDebuggable( + params!['isolateId'], + params['libraryId'], + params['isDebuggable'], + ); + break; + case 'setName': + response = await _serviceImplementation.setName( + params!['isolateId'], + params['name'], + ); + break; + case 'setTraceClassAllocation': + response = await _serviceImplementation.setTraceClassAllocation( + params!['isolateId'], + params['classId'], + params['enable'], + ); + break; + case 'setVMName': + response = await _serviceImplementation.setVMName( + params!['name'], + ); + break; + case 'setVMTimelineFlags': + response = await _serviceImplementation.setVMTimelineFlags( + List.from(params!['recordedStreams'] ?? []), + ); + break; + case 'streamCancel': + var id = params!['streamId']; + var existing = _streamSubscriptions.remove(id); + if (existing == null) { + throw RPCError.withDetails( + 'streamCancel', + 104, + 'Stream not subscribed', + details: "The stream '$id' is not subscribed", + ); + } + await existing.cancel(); + response = Success(); + break; + case 'streamCpuSamplesWithUserTag': + response = await _serviceImplementation.streamCpuSamplesWithUserTag( + List.from(params!['userTags'] ?? []), + ); + break; + case 'streamListen': + var id = params!['streamId']; + if (_streamSubscriptions.containsKey(id)) { + throw RPCError.withDetails( + 'streamListen', + 103, + 'Stream already subscribed', + details: "The stream '$id' is already subscribed", + ); + } + + var stream = id == 'Service' + ? _serviceExtensionRegistry.onExtensionEvent + : _serviceImplementation.onEvent(id); + _streamSubscriptions[id] = stream.listen((e) { + _responseSink.add({ + 'jsonrpc': '2.0', + 'method': 'streamNotify', + 'params': { + 'streamId': id, + 'event': e.toJson(), + }, + }); + }); + response = Success(); + break; + default: + 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.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); + } + } + _responseSink.add({ + 'jsonrpc': '2.0', + 'id': id, + 'result': response.toJson(), + }); + } 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, + }); + } + } +} diff --git a/pkg/vm_service_interface/lib/vm_service_interface.dart b/pkg/vm_service_interface/lib/vm_service_interface.dart new file mode 100644 index 00000000000..99ff465d8f5 --- /dev/null +++ b/pkg/vm_service_interface/lib/vm_service_interface.dart @@ -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'; diff --git a/pkg/vm_service_interface/pubspec.yaml b/pkg/vm_service_interface/pubspec.yaml new file mode 100644 index 00000000000..140bbc80173 --- /dev/null +++ b/pkg/vm_service_interface/pubspec.yaml @@ -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 \ No newline at end of file diff --git a/pkg/vm_service/test/server_test.dart b/pkg/vm_service_interface/test/server_test.dart similarity index 93% rename from pkg/vm_service/test/server_test.dart rename to pkg/vm_service_interface/test/server_test.dart index eb8c9782178..a62c40b39b4 100644 --- a/pkg/vm_service/test/server_test.dart +++ b/pkg/vm_service_interface/test/server_test.dart @@ -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> requestsController; late StreamController> responsesController; late ServiceExtensionRegistry serviceRegistry; setUp(() { - serviceMock = MockVmService(); + serviceMock = MockVmServiceImplementation(); requestsController = StreamController>(); responsesController = StreamController>(); 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 rpcRequest(String method, - {Map? params = const {}, String id = "1"}) => + {Map? 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 rpcResponse(Response response, {String id = "1"}) => { +Map rpcResponse(Response response, {String id = '1'}) => { 'jsonrpc': '2.0', 'id': id, 'result': response.toJson(), }; -Map rpcErrorResponse(Object error, {String id = "1"}) { +Map rpcErrorResponse(Object error, {String id = '1'}) { Map errorJson; if (error is RPCError) { errorJson = { @@ -488,7 +491,7 @@ Map streamNotifyResponse(String streamId, Event event) { 'jsonrpc': '2.0', 'method': 'streamNotify', 'params': { - 'streamId': '$streamId', + 'streamId': streamId, 'event': event.toJson(), }, }; @@ -502,7 +505,7 @@ Map stripEventTimestamp(Map response) { return response as Map; } -class MockVmService extends Mock implements VmServiceInterface { +class MockVmServiceImplementation extends Mock implements VmServiceInterface { final streamControllers = >{}; @override