[ Service ] Add support for binary events in package:dart_runtime_service_vm

package:vm_service test suite is ~98% passing with this change.
Change-Id: I7455eb6414ac6a9f28408613543d18be7940bbb4
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/490600
Reviewed-by: Jessy Yameogo <yjessy@google.com>
This commit is contained in:
Ben Konyi
2026-04-10 12:04:28 -07:00
committed by Commit Queue
parent ae1b83869a
commit cb72e886d5
5 changed files with 94 additions and 38 deletions
+41 -3
View File
@@ -2,6 +2,9 @@
// 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:typed_data';
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
@@ -21,7 +24,7 @@ typedef ServiceNameAliasPair = ({ServiceName service, ServiceAlias alias});
/// Represents a client that is connected to a service.
base class Client {
Client({
required StreamChannel<String> connection,
required this.connection,
required UnmodifiableClientNamedLookup clients,
required EventStreamMethods eventStreamMethods,
required this.backend,
@@ -29,7 +32,24 @@ base class Client {
String? name,
}) {
_name = name ?? defaultClientName;
_clientPeer = json_rpc.Peer(connection, strictProtocolChecks: false);
// Manually create a StreamChannel<String> instead of calling
// .cast<String>() as cast() results in addStream() being called,
// binding the underlying sink. This results in a StateError being thrown
// if we try and add directly to the sink, which we do for binary events
// in [EventStreamMethod]'s streamNotify().
final manualConnectionSinkCast = StreamController<String>(sync: true)
..stream
.cast<String>()
.listen((event) => connection.sink.add(event))
.onDone(() => connection.sink.close());
final manualConnectionStreamCast = connection.stream.cast<String>();
_clientPeer = json_rpc.Peer(
StreamChannel<String>(
manualConnectionStreamCast,
manualConnectionSinkCast,
),
strictProtocolChecks: false,
);
_internalRpcs = DartRuntimeServiceRpcs(
clients: clients,
eventStreamMethods: eventStreamMethods,
@@ -40,6 +60,7 @@ base class Client {
late final String namespace;
final StreamChannel<Object?> connection;
late json_rpc.Peer _clientPeer;
late final DartRuntimeServiceRpcs _internalRpcs;
final DartRuntimeServiceBackend backend;
@@ -160,6 +181,23 @@ base class Client {
}
}
/// Sends raw binary [data] to the client.
///
/// This technically isn't compliant with the JSON-RPC specification and
/// should only be used to send binary events to streams.
void sendBinaryData({required Uint8List data}) {
if (_clientPeer.isClosed) {
RpcException.serviceDisappeared.throwException();
}
try {
connection.sink.add(data);
// ignore: avoid_catching_errors
} on StateError {
RpcException.serviceDisappeared.throwException();
}
}
/// The set of services registered by this [Client].
Iterable<ServiceNameAliasPair> get services =>
_services.entries.map((e) => (service: e.key, alias: e.value));
@@ -257,7 +295,7 @@ base class ClientManager implements ClientConnectionController {
/// This should be called when a client connects to the service.
@mustCallSuper
Client addClient({
required StreamChannel<String> connection,
required StreamChannel<Object?> connection,
String? name,
bool artificial = false,
}) {
@@ -262,7 +262,7 @@ class DartRuntimeService {
}
/// Send a [StreamEvent] to subscribed clients.
void sendEvent({required StreamEvent event}) {
void sendEvent({required StreamEventBase event}) {
event.send(eventStreamMethods: eventStreamManager);
}
@@ -14,16 +14,11 @@ import 'dart_runtime_service_backend.dart';
import 'rpc_exceptions.dart';
import 'utils.dart';
/// A base class for events to be sent on [streamId] with a given [kind].
abstract base class StreamEvent {
StreamEvent({required this.streamId, required this.kind});
static const kStreamId = 'streamId';
static const kEvent = 'event';
/// A base class for events to be sent on [streamId].
abstract base class StreamEventBase {
const StreamEventBase({required this.streamId});
final String streamId;
final String kind;
final int timestamp = DateTime.now().millisecondsSinceEpoch;
void send({
required EventStreamMethods eventStreamMethods,
@@ -35,11 +30,30 @@ abstract base class StreamEvent {
excludedClient: excludedClient,
);
}
}
/// A base class for JSON-RPC compliant events to be sent on [streamId] with a
/// given [kind].
abstract base class StreamEvent extends StreamEventBase {
StreamEvent({required super.streamId, required this.kind});
static const kStreamId = 'streamId';
static const kEvent = 'event';
final String kind;
final int timestamp = DateTime.now().millisecondsSinceEpoch;
@mustCallSuper
Map<String, Object?> toJson();
}
/// A class for sending non-JSON-RPC compliant binary events on [streamId].
final class BinaryStreamEvent extends StreamEventBase {
const BinaryStreamEvent({required super.streamId, required this.data});
final Uint8List data;
}
/// Base class for service registration events which are sent on the Service
/// stream.
abstract base class ServiceRegistrationEvent extends StreamEvent {
@@ -187,11 +201,8 @@ class EventStreamManager implements EventStreamMethods {
continue;
}
switch (data) {
case Uint8List():
// TODO(bkonyi): support sending binary events (e.g., for heap
// snapshots).
// listener.connection.sink.add(data);
throw StateError('Cannot send binary data');
case BinaryStreamEvent(data: final binaryData):
listener.sendBinaryData(data: binaryData);
case StreamEvent():
listener.sendNotification(
method: kStreamNotify,
@@ -171,7 +171,7 @@ Handler webSocketClientHandler({required ClientManager clientManager}) {
// package:shelf_web_socket v2.
final handler = webSocketHandler((WebSocketChannel ws, _) {
logger.info('New web socket connection. Creating $Client.');
clientManager.addClient(connection: ws.cast<String>());
clientManager.addClient(connection: ws.cast<Object?>());
});
return (request) {
@@ -7,6 +7,7 @@ import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:dart_runtime_service/dart_runtime_service.dart';
@@ -213,12 +214,9 @@ class DartRuntimeServiceVMBackend
void _vmMessageHandler(List<Object?> message) {
_logger.fine('VM message: $message');
switch (message) {
case [final String streamId, final String eventJsonString]:
case [final String streamId, final Object event]:
// This is an event.
_eventMessageHandler(
streamId,
json.decode(eventJsonString) as Map<String, Object?>,
);
_eventMessageHandler(streamId, event);
case [final int opcode]:
// This is a control message directing the vm service to exit.
assert(opcode == _kServiceExitMessageId);
@@ -257,9 +255,25 @@ class DartRuntimeServiceVMBackend
}
/// Forward VM service events sent from the VM.
void _eventMessageHandler(String streamId, Map<String, Object?> event) {
void _eventMessageHandler(String streamId, Object event) {
frontend.sendEvent(
event: ForwardingStreamEvent(streamId: streamId, event: event),
event: switch (event) {
final String jsonString => ForwardingStreamEvent(
streamId: streamId,
event: json.decode(jsonString) as Map<String, Object?>,
),
[final Uint8List utf8String] => ForwardingStreamEvent(
streamId: streamId,
event: json.decode(utf8.decode(utf8String)) as Map<String, Object?>,
),
final Uint8List binaryData => BinaryStreamEvent(
streamId: streamId,
data: binaryData,
),
_ => throw UnimplementedError(
'Unexpected event type: ${event.runtimeType}.',
),
},
);
}
@@ -316,23 +330,16 @@ class DartRuntimeServiceVMBackend
List<int> message,
SendPort replyPort,
) async {
// The original VM service implementation could, in theory, handle binary
// and "UTF8String" responses. However, no binary or "UTF8String" responses
// are sent in response to service RPCs so we should be able to assume that
// `message` can be converted to a `String`.
//
// If this decode throws an exception, we'll need to revisit this.
final messageStr = utf8.decode(message);
_logger.info('Native RPC request: $messageStr');
_nativeRpcClientStreamChannelController.local.sink.add(messageStr);
// TODO(bkonyi): handle non-string results
/*
late List<int> bytes;
switch (response.kind) {
case ResponsePayloadKind.String:
bytes = utf8.encode(response.payload as String);
bytes = bytes is Uint8List ? bytes : Uint8List.fromList(bytes);
case ResponsePayloadKind.Binary:
case ResponsePayloadKind.Utf8String:
bytes = response.payload as Uint8List;
}
*/
if (!await _nativeRpcClientResponseStream.moveNext()) {
_logger.warning('Native RPC client stream has closed.');
return;