| // Copyright (c) 2020, 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 'dart:collection'; |
| |
| import 'package:async/async.dart'; |
| // ignore: implementation_imports |
| import 'package:vm_service/src/vm_service.dart'; |
| |
| extension DdsExtension on VmService { |
| static bool _factoriesRegistered = false; |
| static Version? _ddsVersion; |
| |
| /// The [getDartDevelopmentServiceVersion] RPC is used to determine what version of |
| /// the Dart Development Service Protocol is served by a DDS instance. |
| /// |
| /// The result of this call is cached for subsequent invocations. |
| Future<Version> getDartDevelopmentServiceVersion() async { |
| _ddsVersion ??= await _callHelper<Version>( |
| 'getDartDevelopmentServiceVersion', |
| ); |
| return _ddsVersion!; |
| } |
| |
| /// The [getCachedCpuSamples] RPC is used to retrieve a cache of CPU samples |
| /// collected under a [UserTag] with name `userTag`. |
| Future<CachedCpuSamples> getCachedCpuSamples( |
| String isolateId, String userTag) async { |
| if (!(await _versionCheck(1, 3))) { |
| throw UnimplementedError('getCachedCpuSamples requires DDS version 1.3'); |
| } |
| return _callHelper<CachedCpuSamples>('getCachedCpuSamples', args: { |
| 'isolateId': isolateId, |
| 'userTag': userTag, |
| }); |
| } |
| |
| /// The [getPerfettoVMTimelineWithCpuSamples] RPC functions nearly identically |
| /// to [VmService.getPerfettoVMTimeline], except the `trace` field of the |
| /// [PerfettoTimeline] response returned by this RPC will be a Base64 string |
| /// encoding a Perfetto-format trace that includes not only all timeline |
| /// events in the specified time range, but also all CPU samples from all |
| /// isolates in the specified time range. |
| Future<PerfettoTimeline> getPerfettoVMTimelineWithCpuSamples( |
| {int? timeOriginMicros, int? timeExtentMicros}) async { |
| if (!(await _versionCheck(1, 5))) { |
| throw UnimplementedError( |
| 'getPerfettoVMTimelineWithCpuSamples requires DDS version 1.5'); |
| } |
| return _callHelper<PerfettoTimeline>('getPerfettoVMTimelineWithCpuSamples', |
| args: { |
| 'timeOriginMicros': timeOriginMicros, |
| 'timeExtentMicros': timeExtentMicros, |
| }); |
| } |
| |
| /// Send an event to the [stream]. |
| /// |
| /// [stream] must be a registered custom stream (i.e., not a stream specified |
| /// as part of the VM service protocol). |
| /// |
| /// If [stream] is not a registered custom stream, an [RPCError] with code |
| /// [kCustomStreamDoesNotExist] will be thrown. |
| /// |
| /// If [stream] is a core stream, an [RPCError] with code |
| /// [kCoreStreamNotAllowed] will be thrown. |
| Future<void> postEvent( |
| String stream, |
| String eventKind, |
| Map<String, Object?> eventData, |
| ) async { |
| if (!(await _versionCheck(1, 6))) { |
| throw UnimplementedError('postEvent requires DDS version 1.6'); |
| } |
| return _callHelper<void>('postEvent', args: { |
| 'eventKind': eventKind, |
| 'eventData': eventData, |
| 'stream': stream, |
| }); |
| } |
| |
| /// The [getAvailableCachedCpuSamples] RPC is used to determine which caches of CPU samples |
| /// are available. Caches are associated with individual [UserTag] names and are specified |
| /// when DDS is started via the `cachedUserTags` parameter. |
| Future<AvailableCachedCpuSamples> getAvailableCachedCpuSamples() async { |
| if (!(await _versionCheck(1, 3))) { |
| throw UnimplementedError( |
| 'getAvailableCachedCpuSamples requires DDS version 1.3', |
| ); |
| } |
| return _callHelper<AvailableCachedCpuSamples>( |
| 'getAvailableCachedCpuSamples', |
| ); |
| } |
| |
| /// The [getLogHistorySize] RPC is used to retrieve the current size of the |
| /// log history buffer. |
| /// |
| /// If the returned [Size] is zero, then log history is disabled. |
| Future<Size> getLogHistorySize(String isolateId) async { |
| // No version check needed, present since v1.0 of the protocol. |
| return _callHelper<Size>('getLogHistorySize', args: { |
| 'isolateId': isolateId, |
| }); |
| } |
| |
| /// The [setLogHistorySize] RPC is used to set the size of the ring buffer |
| /// used for caching a limited set of historical log messages. |
| /// |
| /// If [size] is 0, logging history will be disabled. |
| /// |
| /// The maximum history size is 100,000 messages, with the default set to |
| /// 10,000 messages. |
| Future<Success> setLogHistorySize(String isolateId, int size) async { |
| // No version check needed, present since v1.0 of the protocol. |
| return _callHelper<Success>('setLogHistorySize', args: { |
| 'isolateId': isolateId, |
| 'size': size, |
| }); |
| } |
| |
| /// Retrieve the event history for `stream`. |
| /// |
| /// If `stream` does not have event history collected, a parameter error is |
| /// returned. |
| Future<StreamHistory> getStreamHistory(String stream) async { |
| if (!(await _versionCheck(1, 2))) { |
| throw UnimplementedError('getStreamHistory requires DDS version 1.2'); |
| } |
| return _callHelper<StreamHistory>('getStreamHistory', args: { |
| 'stream': stream, |
| }); |
| } |
| |
| /// Returns the stream for a given stream id which includes historical |
| /// events. |
| /// |
| /// If `stream` does not have event history collected, a parameter error is |
| /// sent over the returned [Stream]. |
| Stream<Event> onEventWithHistory(String stream) { |
| late StreamController<Event> controller; |
| late StreamQueue<Event> streamEvents; |
| |
| controller = StreamController<Event>(onListen: () async { |
| streamEvents = StreamQueue<Event>(onEvent(stream)); |
| final history = (await getStreamHistory(stream)).history; |
| Event? firstStreamEvent; |
| unawaited(streamEvents.peek.then((e) { |
| firstStreamEvent = e; |
| })); |
| for (final event in history) { |
| if (firstStreamEvent != null && |
| event.timestamp! > firstStreamEvent!.timestamp!) { |
| break; |
| } |
| controller.sink.add(event); |
| } |
| unawaited(controller.sink.addStream(streamEvents.rest)); |
| }, onCancel: () { |
| try { |
| streamEvents.cancel(); |
| } on StateError { |
| // Underlying stream may have already been cancelled. |
| } |
| }); |
| |
| return controller.stream; |
| } |
| |
| /// Returns a new [Stream<Event>] of `Logging` events which outputs |
| /// historical events before streaming real-time events. |
| /// |
| /// Note: unlike [onLoggingEvent], the returned stream is a single |
| /// subscription stream and a new stream is created for each invocation of |
| /// this getter. |
| Stream<Event> get onLoggingEventWithHistory => onEventWithHistory('Logging'); |
| |
| /// Returns a new [Stream<Event>] of `Stdout` events which outputs |
| /// historical events before streaming real-time events. |
| /// |
| /// Note: unlike [onStdoutEvent], the returned stream is a single |
| /// subscription stream and a new stream is created for each invocation of |
| /// this getter. |
| Stream<Event> get onStdoutEventWithHistory => onEventWithHistory('Stdout'); |
| |
| /// Returns a new [Stream<Event>] of `Stderr` events which outputs |
| /// historical events before streaming real-time events. |
| /// |
| /// Note: unlike [onStderrEvent], the returned stream is a single |
| /// subscription stream and a new stream is created for each invocation of |
| /// this getter. |
| Stream<Event> get onStderrEventWithHistory => onEventWithHistory('Stderr'); |
| |
| /// Returns a new [Stream<Event>] of `Extension` events which outputs |
| /// historical events before streaming real-time events. |
| /// |
| /// Note: unlike [onExtensionEvent], the returned stream is a single |
| /// subscription stream and a new stream is created for each invocation of |
| /// this getter. |
| Stream<Event> get onExtensionEventWithHistory => |
| onEventWithHistory('Extension'); |
| |
| /// The [getClientName] RPC is used to retrieve the name associated with the |
| /// currently connected VM service client. |
| /// |
| /// If no name was previously set through the [setClientName] RPC, a default |
| /// name will be returned. |
| Future<ClientName> getClientName() async { |
| // No version check needed, present since v1.0 of the protocol. |
| return _callHelper<ClientName>( |
| 'getClientName', |
| ); |
| } |
| |
| /// The [setClientName] RPC is used to set a name to be associated with the |
| /// currently connected VM service client. |
| /// |
| /// If the [name] parameter is a non-empty string, [name] will become the new |
| /// name associated with the client. If [name] is an empty string, the |
| /// client's name will be reset to its default name. |
| Future<Success> setClientName([String name = '']) async { |
| // No version check needed, present since v1.0 of the protocol. |
| return _callHelper<Success>( |
| 'setClientName', |
| args: { |
| 'name': name, |
| }, |
| ); |
| } |
| |
| /// The [requirePermissionToResume] RPC is used to change the pause/resume |
| /// behavior of isolates. |
| /// |
| /// This provides a way for the VM service to wait for approval to resume |
| /// from some set of clients. This is useful for clients which want to |
| /// perform some operation on an isolate after a pause without it being |
| /// resumed by another client. These clients should invoke [readyToResume] |
| /// instead of [VmService.resume] to indicate to DDS that they have finished |
| /// their work and the isolate can be resumed. |
| /// |
| /// If the [onPauseStart] parameter is `true`, isolates will not resume after |
| /// pausing on start until the client sends a `resume` request and all other |
| /// clients which need to provide resume approval for this pause type have |
| /// done so. |
| /// |
| /// If the [onPauseReload] parameter is `true`, isolates will not resume |
| /// after pausing after a reload until the client sends a `resume` request |
| /// and all other clients which need to provide resume approval for this |
| /// pause type have done so. |
| /// |
| /// If the [onPauseExit] parameter is `true`, isolates will not resume after |
| /// pausing on exit until the client sends a `resume` request and all other |
| /// clients which need to provide resume approval for this pause type have |
| /// done so. |
| /// |
| /// **Important Notes:** |
| /// |
| /// - All clients with the same client name share resume permissions. Only a |
| /// single client of a given name is required to provide resume approval. |
| /// - When a client requiring approval disconnects from the service, a paused |
| /// isolate may resume if all other clients requiring resume approval have |
| /// already given approval. In the case that no other client requires |
| /// resume approval for the current pause event, the isolate will be |
| /// resumed if at least one other client has attempted to resume the |
| /// isolate. |
| /// - Resume permission behavior can be bypassed using the [VmService.resume] |
| /// RPC, which is treated as a user-initiated resume that force resumes |
| /// the isolate. Tooling relying on resume permissions should use |
| /// [readyToResume] instead of [VmService.resume] to avoid force resuming |
| /// the isolate. |
| Future<Success> requirePermissionToResume({ |
| bool onPauseStart = false, |
| bool onPauseReload = false, |
| bool onPauseExit = false, |
| }) async { |
| // No version check needed, present since v1.0 of the protocol. |
| return _callHelper<Success>( |
| 'requirePermissionToResume', |
| args: { |
| 'onPauseStart': onPauseStart, |
| 'onPauseReload': onPauseReload, |
| 'onPauseExit': onPauseExit, |
| }, |
| ); |
| } |
| |
| /// The [readyToResume] RPC indicates to DDS that the current client is ready |
| /// to resume the isolate. |
| /// |
| /// If the current client requires that approval be given before resuming an |
| /// isolate, this method will: |
| /// |
| /// - Update the approval state for the isolate. |
| /// - Resume the isolate if approval has been given by all clients which |
| /// require approval. |
| /// |
| /// Throws a [SentinelException] if the isolate no longer exists. |
| Future<Success> readyToResume(String isolateId) async { |
| if (!(await _versionCheck(2, 0))) { |
| throw UnimplementedError('readyToResume requires DDS version 2.0'); |
| } |
| return _callHelper<Success>( |
| 'readyToResume', |
| isolateId: isolateId, |
| ); |
| } |
| |
| /// The [requireUserPermissionToResume] RPC notifies DDS if it should wait |
| /// for a [VmService.resume] request to resume isolates paused on start or |
| /// exit. |
| /// |
| /// This RPC should only be invoked by tooling which launched the target Dart |
| /// process and knows if the user indicated they wanted isolates paused on |
| /// start or exit. |
| Future<Success> requireUserPermissionToResume({ |
| bool onPauseStart = false, |
| bool onPauseExit = false, |
| }) async { |
| if (!(await _versionCheck(2, 0))) { |
| throw UnimplementedError( |
| 'requireUserPermissionToResume requires DDS version 2.0', |
| ); |
| } |
| return _callHelper<Success>( |
| 'requireUserPermissionToResume', |
| args: { |
| 'onPauseStart': onPauseStart, |
| 'onPauseExit': onPauseExit, |
| }, |
| ); |
| } |
| |
| Future<bool> _versionCheck(int major, int minor) async { |
| _ddsVersion ??= await getDartDevelopmentServiceVersion(); |
| return ((_ddsVersion!.major == major && _ddsVersion!.minor! >= minor) || |
| (_ddsVersion!.major! > major)); |
| } |
| |
| Future<T> _callHelper<T>(String method, |
| {String? isolateId, Map args = const {}}) { |
| if (!_factoriesRegistered) { |
| _registerFactories(); |
| } |
| return callMethod( |
| method, |
| args: { |
| if (isolateId != null) 'isolateId': isolateId, |
| ...args, |
| }, |
| ).then((e) => e as T); |
| } |
| |
| static void _registerFactories() { |
| addTypeFactory('StreamHistory', StreamHistory.parse); |
| addTypeFactory( |
| 'AvailableCachedCpuSamples', |
| AvailableCachedCpuSamples.parse, |
| ); |
| addTypeFactory('CachedCpuSamples', CachedCpuSamples.parse); |
| addTypeFactory('Size', Size.parse); |
| addTypeFactory('ClientName', ClientName.parse); |
| addTypeFactory( |
| 'ResumePermissionsRequired', |
| ResumePermissionsRequired.parse, |
| ); |
| _factoriesRegistered = true; |
| } |
| } |
| |
| /// A simple object representing the name of a DDS client. |
| /// |
| /// See [DdsExtension.getClientName] and [DdsExtension.setClientName]. |
| class ClientName extends Response { |
| static ClientName? parse(Map<String, dynamic>? json) => |
| json == null ? null : ClientName._fromJson(json); |
| |
| ClientName({required this.name}); |
| |
| ClientName._fromJson(Map<String, dynamic> json) : name = json['name']; |
| |
| final String name; |
| } |
| |
| /// A simple object representing a size response. |
| class Size extends Response { |
| static Size? parse(Map<String, dynamic>? json) => |
| json == null ? null : Size._fromJson(json); |
| |
| Size({required this.size}); |
| |
| Size._fromJson(Map<String, dynamic> json) : size = json['size']; |
| |
| final int size; |
| } |
| |
| /// A collection of historical [Event]s from some stream. |
| class StreamHistory extends Response { |
| static StreamHistory? parse(Map<String, dynamic>? json) => |
| json == null ? null : StreamHistory._fromJson(json); |
| |
| StreamHistory({required List<Event> history}) : _history = history; |
| |
| StreamHistory._fromJson(Map<String, dynamic> json) |
| : _history = json['history'] |
| .map( |
| (e) => Event.parse(e), |
| ) |
| .toList() |
| .cast<Event>() { |
| this.json = json; |
| } |
| |
| @override |
| String get type => 'StreamHistory'; |
| |
| /// Historical [Event]s for a stream. |
| List<Event> get history => UnmodifiableListView(_history); |
| final List<Event> _history; |
| } |
| |
| /// An extension of [CpuSamples] which represents a set of cached samples, |
| /// associated with a particular [UserTag] name. |
| class CachedCpuSamples extends CpuSamples { |
| static CachedCpuSamples? parse(Map<String, dynamic>? json) => |
| json == null ? null : CachedCpuSamples._fromJson(json); |
| |
| CachedCpuSamples({ |
| required this.userTag, |
| this.truncated, |
| required int? samplePeriod, |
| required int? maxStackDepth, |
| required int? sampleCount, |
| required int? timeOriginMicros, |
| required int? timeExtentMicros, |
| required int? pid, |
| required List<ProfileFunction>? functions, |
| required List<CpuSample>? samples, |
| }) : super( |
| samplePeriod: samplePeriod, |
| maxStackDepth: maxStackDepth, |
| sampleCount: sampleCount, |
| timeOriginMicros: timeOriginMicros, |
| timeExtentMicros: timeExtentMicros, |
| pid: pid, |
| functions: functions, |
| samples: samples, |
| ); |
| |
| CachedCpuSamples._fromJson(Map<String, dynamic> json) |
| : userTag = json['userTag']!, |
| truncated = json['truncated'], |
| super( |
| samplePeriod: json['samplePeriod'] ?? -1, |
| maxStackDepth: json['maxStackDepth'] ?? -1, |
| sampleCount: json['sampleCount'] ?? -1, |
| timeOriginMicros: json['timeOriginMicros'] ?? -1, |
| timeExtentMicros: json['timeExtentMicros'] ?? -1, |
| pid: json['pid'] ?? -1, |
| functions: List<ProfileFunction>.from( |
| createServiceObject(json['functions'], const ['ProfileFunction']) |
| as List? ?? |
| [], |
| ), |
| samples: List<CpuSample>.from( |
| createServiceObject(json['samples'], const ['CpuSample']) |
| as List? ?? |
| [], |
| ), |
| ); |
| |
| @override |
| String get type => 'CachedCpuSamples'; |
| |
| /// The name of the [UserTag] associated with this cache of [CpuSamples]. |
| final String userTag; |
| |
| /// Provided if the CPU sample cache has filled and older samples have been |
| /// dropped. |
| final bool? truncated; |
| } |
| |
| /// A collection of [UserTag] names associated with caches of CPU samples. |
| class AvailableCachedCpuSamples extends Response { |
| static AvailableCachedCpuSamples? parse(Map<String, dynamic>? json) => |
| json == null ? null : AvailableCachedCpuSamples._fromJson(json); |
| |
| AvailableCachedCpuSamples({ |
| required this.cacheNames, |
| }); |
| |
| AvailableCachedCpuSamples._fromJson(Map<String, dynamic> json) |
| : cacheNames = List<String>.from(json['cacheNames']); |
| |
| @override |
| String get type => 'AvailableCachedUserTagCpuSamples'; |
| |
| /// A [List] of [UserTag] names associated with CPU sample caches. |
| final List<String> cacheNames; |
| } |
| |
| class ResumePermissionsRequired extends Response { |
| static ResumePermissionsRequired? parse(Map<String, dynamic>? json) => |
| json == null ? null : ResumePermissionsRequired._fromJson(json); |
| |
| ResumePermissionsRequired({ |
| required this.onPauseStart, |
| required this.onPauseExit, |
| }); |
| |
| ResumePermissionsRequired._fromJson(Map<String, dynamic> json) |
| : onPauseStart = json['onPauseStart'], |
| onPauseExit = json['onPauseExit']; |
| |
| final bool onPauseStart; |
| final bool onPauseExit; |
| } |