Add a DTD method to get all registered services.

This CL contains breaking changes for package:dtd and prepares both package:dart_service_protocol_shared and package:dtd for publish.

This CL also fixes https://github.com/dart-lang/sdk/issues/60757 so that DTD-registered services are sent over the `Service` stream upon initial subscription like what is done for client-registered services.

Change-Id: I619af816e64af01864c7ed9b98743c6691bf7e0b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/429161
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Kenzie Davisson <kenzieschmoll@google.com>
This commit is contained in:
Kenzie Davisson
2025-05-22 13:52:54 -07:00
committed by Commit Queue
parent f4c87f2ae8
commit 10dd59c039
24 changed files with 645 additions and 72 deletions
+8
View File
@@ -64,6 +64,14 @@ To run just the analysis server integration tests:
./tools/test.py -mrelease pkg/analysis_server/test/integration/
```
To run a single test:
```
dart test pkg/analysis_server/test/some_test.dart
```
> Note: `dart` may need to point to a Dart SDK built from source
depending on the changes you are testing.
[building]: https://github.com/dart-lang/sdk/wiki/Building
[contributing]: https://github.com/dart-lang/sdk/blob/master/CONTRIBUTING.md
@@ -29,10 +29,10 @@ class ExperimentalEchoHandler extends SharedMessageHandler<Object?, Object?> {
MessageInfo message,
CancellationToken token,
) async {
// The DTD client automatically converts `null` params to an empty map, but
// (because of a previous bug) we want to test null results. So if the
// params are an empty map, return null. This is tested by
// `test_service_success_echo_nullResponse` in `SharedDtdTests`.
// The DTD client may send an empty map as params. Return null in this case.
// This is tested by
//`test_service_success_echo_nullResponse_with_empty_params` in
// `SharedDtdTests`.
if (params is Map && params.isEmpty) {
return success(null);
}
@@ -179,7 +179,7 @@ class DtdServices {
var message = IncomingMessage(
jsonrpc: jsonRpcVersion,
method: method,
params: params.asMap,
params: params.value,
);
var scheduler = _server.messageScheduler;
var completer = Completer<Map<String, Object?>>();
@@ -88,7 +88,10 @@ mixin SharedDtdTests
/// A list of service/methods that the test client has seen registered (and
/// not yet unregistered) over the DTD connection.
final availableMethods = <(String, Method)>[];
///
/// The service name is a nullable String because DTD-internal methods and
/// services do not have a service name.
final availableMethods = <(String?, Method)>[];
/// An invalid DTD URI used for testing connection failures.
final invalidUri = Uri.parse('ws://invalid:345/invalid');
@@ -207,12 +210,12 @@ mixin SharedDtdTests
switch (e.kind) {
case 'ServiceRegistered':
availableMethods.add((
e.data['service'] as String,
e.data['service'] as String?,
Method(e.data['method'] as String),
));
case 'ServiceUnregistered':
availableMethods.remove((
e.data['service'] as String,
e.data['service'] as String?,
Method(e.data['method'] as String),
));
}
@@ -470,7 +473,24 @@ FutureOr<void>? a;
expect(result, equals({'a': 'b'}));
}
Future<void> test_service_success_echo_nullResponse() async {
Future<void>
test_service_success_echo_nullResponse_with_empty_params() async {
await initializeServer();
await sendConnectToDtdRequest(registerExperimentalHandlers: true);
var response = await dtd.connection.call(
lspServiceName,
CustomMethods.experimentalEcho.toString(),
params: const <String, Object?>{},
);
var result = response.result['result'] as Map<String, Object?>?;
expect(response.type, 'Null');
expect(result, isNull);
}
Future<void> test_service_success_echo_nullResponse_with_null_params() async {
await initializeServer();
await sendConnectToDtdRequest(registerExperimentalHandlers: true);
@@ -512,7 +532,10 @@ void [!myFun^ction!]() {}
await initializeServer();
await sendConnectToDtdRequest();
expect(availableMethods, isNotEmpty);
var lspMethods = availableMethods.where(
(serviceMethod) => serviceMethod.$1 == 'Lsp',
);
expect(lspMethods, isNotEmpty);
// Send a request to the server to connect to DTD. This will only complete
// once all services are registered, however there's no guarantee about the
@@ -521,9 +544,9 @@ void [!myFun^ction!]() {}
await shutdownServer();
// Wait for the services to be unregistered.
while (availableMethods.isNotEmpty) {
while (lspMethods.isNotEmpty) {
await pumpEventQueue(times: 5000);
}
expect(availableMethods, isEmpty);
expect(lspMethods, isEmpty);
}
}
@@ -1,5 +1,8 @@
## 0.0.3-wip
## 0.0.3
- Update sdk constraint to '^3.5.0'
- Added `ClientServiceInfo.fromJson` and `ClientServiceInfo.toJson` methods.
- Added `ClientServiceMethodInfo.fromJson` and `ClientServiceMethodInfo.toJson`
methods.
## 0.0.2
- Fixed an issue with streamNotify data type being too specific.
@@ -48,6 +48,26 @@ class ClientServiceInfo {
ClientServiceInfo(this.name, [Map<String, ClientServiceMethodInfo>? methods])
: methods = methods ?? {};
/// Deserializes a [json] object to create a [ClientServiceInfo] object.
static ClientServiceInfo fromJson(Map<String, Object?> json) {
if (json case {_kName: final String name, _kMethods: final List methods}) {
return ClientServiceInfo(
name,
<String, ClientServiceMethodInfo>{
for (final method in methods
.cast<Map<String, Object?>>()
.map(ClientServiceMethodInfo.fromJson))
method.name: method
},
);
}
throw ArgumentError('Unexpected JSON format: $json');
}
static const _kName = 'name';
static const _kMethods = 'methods';
/// The name of the service.
///
/// A client can register multiple services each with multiple methods.
@@ -58,6 +78,12 @@ class ClientServiceInfo {
/// The service methods registered for this service.
final Map<String, ClientServiceMethodInfo> methods;
/// Serializes this [ClientServiceInfo] object to JSON.
Map<String, Object?> toJson() => {
_kName: name,
_kMethods: methods.values.map((m) => m.toJson()).toList(),
};
}
/// Information about an individual method of a service provided by a
@@ -65,6 +91,22 @@ class ClientServiceInfo {
class ClientServiceMethodInfo {
ClientServiceMethodInfo(this.name, [this.capabilities]);
/// Deserializes a [json] object to create a [ClientServiceMethodInfo] object.
static ClientServiceMethodInfo fromJson(Map<String, Object?> json) {
try {
return ClientServiceMethodInfo(
json[_kName] as String,
json[_kCapabilities] as Map<String, Object?>?,
);
} catch (e) {
throw ArgumentError('Unexpected JSON format: $json');
}
}
static const _kName = 'name';
static const _kCapabilities = 'capabilities';
/// The name of the method.
///
/// A client can register multiple methods for each service but can only use
@@ -74,6 +116,12 @@ class ClientServiceMethodInfo {
/// Optional capabilities of this service method provided by the client.
final Map<String, Object?>? capabilities;
/// Serializes this [ClientServiceMethodInfo] object to JSON.
Map<String, Object?> toJson() => {
_kName: name,
if (capabilities != null) _kCapabilities: capabilities,
};
}
/// Used for keeping track and managing clients that are connected to a given
@@ -1,7 +1,7 @@
name: dart_service_protocol_shared
description: A package that implements service extensions and stream managers.
version: 0.0.3-wip
version: 0.0.3
repository: https://github.com/dart-lang/sdk/tree/main/pkg/dart_service_protocol_shared
environment:
@@ -128,4 +128,82 @@ void main() {
);
});
});
group('ClientServiceInfo', () {
test('toJson and fromJson', () {
final serviceInfo = ClientServiceInfo(
'testService',
{
'method1': ClientServiceMethodInfo('method1', {'capability': true}),
'method2': ClientServiceMethodInfo('method2'),
},
);
final json = serviceInfo.toJson();
final deserialized = ClientServiceInfo.fromJson(json);
expect(deserialized.name, 'testService');
expect(deserialized.methods.length, 2);
expect(deserialized.methods['method1']?.name, 'method1');
expect(
deserialized.methods['method1']?.capabilities,
{'capability': true},
);
expect(deserialized.methods['method2']?.name, 'method2');
expect(deserialized.methods['method2']?.capabilities, isNull);
});
test('fromJson throws with invalid json', () {
expect(
() => ClientServiceInfo.fromJson({
'name': 'testService',
'methods': [
{'bad': 'format'}
],
}),
throwsA(isA<ArgumentError>()),
);
expect(
() => ClientServiceInfo.fromJson({
'methods': [
{'name': 'method1'}
],
}),
throwsA(isA<ArgumentError>()),
reason: 'Missing "name" field in top-level map',
);
});
});
group('ClientServiceMethodInfo', () {
test('toJson and parse', () {
final methodInfo =
ClientServiceMethodInfo('testMethod', {'capability': 123});
final json = methodInfo.toJson();
final deserialized = ClientServiceMethodInfo.fromJson(json);
expect(deserialized.name, 'testMethod');
expect(deserialized.capabilities, {'capability': 123});
});
test('toJson and parse without capabilities', () {
final methodInfo = ClientServiceMethodInfo('testMethod');
final json = methodInfo.toJson();
final deserialized = ClientServiceMethodInfo.fromJson(json);
expect(deserialized.name, 'testMethod');
expect(deserialized.capabilities, isNull);
});
});
test('ClientServiceMethodInfo throws with invalid json', () {
expect(
() => ClientServiceMethodInfo.fromJson({
'bad': 'format',
}),
throwsA(isA<ArgumentError>()),
);
});
}
+9 -2
View File
@@ -1,8 +1,15 @@
## 2.6.0
## 3.0.0
- Add `ConnectedAppService` to store the connections to Dart and Flutter
- Added `ConnectedAppService` to store the connections to Dart and Flutter
applications that DTD is aware of.
- Log exceptions from invalid `streamNotify` events.
- Added `getRegisteredServices` API.
- Added a new response type `RegisteredServicesResponse`.
- **Breaking Change**: Changed the `serviceName` parameter for the
`DartToolingDaemon.call` method to have type `String?` instead of `String.`
- **Breaking Change**: When the `params` parameter for the
`DartToolingDaemon.call` method is null, pass the null value along to the client
peer request instead of sending an empty Map value.
## 2.5.1
+6 -2
View File
@@ -34,9 +34,13 @@ void main(List<String> args) async {
clientB.onEvent('Service').listen((e) {
switch (e.kind) {
case 'ServiceRegistered':
serviceRegisteredCompleted.complete();
if (e.data['service'] == 'ExampleServer') {
serviceRegisteredCompleted.complete();
}
case 'ServiceUnregistered':
serviceUnregisteredCompleted.complete();
if (e.data['service'] == 'ExampleServer') {
serviceUnregisteredCompleted.complete();
}
}
print(jsonEncode({'stream': e.stream, 'kind': e.kind, 'data': e.data}));
});
+3 -4
View File
@@ -5,10 +5,9 @@
/// Support for communicating with the Dart Tooling Daemon.
library;
export 'src/connected_app_service.dart';
export 'src/constants.dart';
export 'src/dart_tooling_daemon.dart';
export 'src/file_system/file_system_service.dart';
export 'src/file_system/types.dart';
export 'src/response_types.dart';
export 'src/response_types/response_types.dart';
export 'src/rpc_error_codes.dart';
export 'src/services/connected_app_service.dart';
export 'src/services/file_system_service.dart';
+26 -6
View File
@@ -117,6 +117,17 @@ class DartToolingDaemon {
);
}
/// Returns a structured response with all the currently registered services
/// available on this DTD instance.
Future<RegisteredServicesResponse> getRegisteredServices() async {
final json = await _clientPeer.sendRequest(
'getRegisteredServices',
) as Map<String, Object?>;
final dtdResponse = _dtdResponseFromJson(json);
return RegisteredServicesResponse.fromDTDResponse(dtdResponse);
}
/// Subscribes this client to events posted on [streamId].
///
/// Once called, the Dart Tooling Daemon will then send any events on the
@@ -185,26 +196,35 @@ class DartToolingDaemon {
}
/// Invokes the service method registered with the name
/// `[serviceName].[methodName]`.
/// `[serviceName].[methodName]`, or with `[methodName]` when [serviceName] is
/// null.
///
/// [serviceName] may be null if the service method is a first party service
/// method registered by DTD or by an internal service.
///
/// If provided, [params] will be sent as the set of parameters used when
/// invoking the service.
///
/// If `[serviceName].[methodName]` is not a registered service method, an
/// [RpcException] will be thrown with [RpcErrorCodes.kMethodNotFound].
/// If `[serviceName].[methodName]`, or `[methodName]` when [serviceName] is
/// null, is not a registered service method, an [RpcException] will be thrown
/// with [RpcErrorCodes.kMethodNotFound].
///
/// If the parameters included in [params] are invalid, an [RpcException] will
/// be thrown with [RpcErrorCodes.kInvalidParams].
Future<DTDResponse> call(
String serviceName,
String? serviceName,
String methodName, {
Map<String, Object?>? params,
}) async {
final combinedName = [serviceName, methodName].nonNulls.join('.');
final json = await _clientPeer.sendRequest(
'$serviceName.$methodName',
params ?? <String, Object?>{},
combinedName,
params,
) as Map<String, Object?>;
return _dtdResponseFromJson(json);
}
DTDResponse _dtdResponseFromJson(Map<String, Object?> json) {
final type = json['type'] as String?;
if (type == null) {
throw DartToolingDaemonConnectionException.callResponseMissingType(json);
@@ -2,9 +2,7 @@
// 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 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
import '../../dtd.dart';
part of 'response_types.dart';
/// A list or [uris] on the system where the Dart Tooling Daemon is running.
class UriList {
@@ -2,9 +2,12 @@
// 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 'package:dart_service_protocol_shared/dart_service_protocol_shared.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
import 'dart_tooling_daemon.dart';
import '../dart_tooling_daemon.dart';
part '_file_system.dart';
/// A DTD response that indicates success.
class Success extends _SuccessResponse<Null> {
@@ -107,3 +110,80 @@ abstract class _SuccessResponse<T> {
@override
String toString() => '[$type value: $value]';
}
/// A DTD response that contains information about all the registered services
/// available on the Dart Tooling Daemon, including services provided by DTD
/// itself as well as services registered by DTD clients.
class RegisteredServicesResponse {
const RegisteredServicesResponse({
required this.dtdServices,
required this.clientServices,
});
factory RegisteredServicesResponse.fromDTDResponse(DTDResponse response) {
if (response.result[_kType] != type) {
throw json_rpc.RpcException.invalidParams(
'Expected $_kType param to be $type, got: ${response.result[_kType]}',
);
}
return RegisteredServicesResponse._fromDTDResponse(response);
}
RegisteredServicesResponse._fromDTDResponse(DTDResponse response)
: dtdServices = List<String>.from(
(response.result[_kDtdServices] as List).cast<String>(),
),
clientServices = List<Map<String, Object?>>.from(
(response.result[_kClientServices] as List)
.cast<Map<String, Object?>>(),
).map(ClientServiceInfo.fromJson).toList();
/// The key for the type parameter.
static const String _kType = 'type';
/// The key for the DTD services parameter.
static const String _kDtdServices = 'dtdServices';
/// The key for the client services parameter.
static const String _kClientServices = 'clientServices';
/// A list of DTD services.
final List<String> dtdServices;
/// A list of DTD client services.
final List<ClientServiceInfo> clientServices;
static String get type => 'RegisteredServicesResponse';
Map<String, Object?> toJson() => <String, Object?>{
_kType: type,
_kDtdServices: dtdServices,
_kClientServices:
clientServices.map((service) => service.toJson()).toList(),
};
@override
String toString() => '['
'$type '
'dtdServices: ${dtdServices.toString()}, '
'clientServices: '
'${clientServices.map((service) => service.display).toList().toString()}'
']';
}
extension on ClientServiceInfo {
String get display {
final sb = StringBuffer()
..write('$name (')
..write(
methods.values.map((method) {
final capabilities = method.capabilities != null
? ' ${method.capabilities.toString()}'
: '';
return '${method.name}$capabilities';
}).join(', '),
)
..write(')');
return sb.toString();
}
}
@@ -4,10 +4,10 @@
import 'package:json_rpc_2/json_rpc_2.dart';
import 'constants.dart';
import 'dart_tooling_daemon.dart';
import 'response_types.dart';
import 'rpc_error_codes.dart';
import '../constants.dart';
import '../dart_tooling_daemon.dart';
import '../response_types/response_types.dart';
import '../rpc_error_codes.dart';
/// Extension methods on the [DartToolingDaemon] that call the ConnectedApps
/// service.
@@ -9,8 +9,8 @@ import 'package:json_rpc_2/json_rpc_2.dart' show RpcException;
import '../constants.dart';
import '../dart_tooling_daemon.dart';
import '../response_types/response_types.dart';
import '../rpc_error_codes.dart' show RpcErrorCodes;
import 'types.dart';
extension FileSystemService on DartToolingDaemon {
/// Reads the file at [uri] from disk in the environment where the Dart
@@ -4,9 +4,9 @@
import 'package:unified_analytics/unified_analytics.dart';
import 'constants.dart';
import 'dart_tooling_daemon.dart';
import 'response_types.dart';
import '../constants.dart';
import '../dart_tooling_daemon.dart';
import '../response_types/response_types.dart';
/// Extension methods on the [DartToolingDaemon] that call the UnifiedAnalytics
/// service.
+2 -1
View File
@@ -1,5 +1,5 @@
name: dtd
version: 2.6.0
version: 3.0.0
description: A package for communicating with the Dart Tooling Daemon.
repository: https://github.com/dart-lang/sdk/tree/main/pkg/dtd
@@ -9,6 +9,7 @@ environment:
resolution: workspace
dependencies:
dart_service_protocol_shared: ^0.0.3
json_rpc_2: '>=3.0.2 <5.0.0'
stream_channel: ^2.1.2
unified_analytics: '>=7.0.0 <9.0.0'
+149 -10
View File
@@ -98,6 +98,106 @@ void main() {
{'type': 'test', 'data': data, 'params': params},
);
});
test('getRegisteredServices', () async {
await clientA.registerService(
'TestService',
'foo',
(Parameters params) async {
return {
'type': 'test',
'data': data,
'params': params.asMap,
};
},
);
await clientA.registerService(
'TestService',
'bar',
(Parameters params) async {
return {
'type': 'test',
'data': data,
'params': params.asMap,
};
},
capabilities: {
'language': 'french',
},
);
await clientB.registerService(
'OtherService',
'foo',
(Parameters params) async {
return {
'type': 'other',
'data': data,
'params': params.asMap,
};
},
capabilities: {
'skills': 'baking',
},
);
final response = await clientA.getRegisteredServices();
expect(
response.toJson(),
{
'type': 'RegisteredServicesResponse',
'dtdServices': [
'streamListen',
'streamCancel',
'postEvent',
'registerService',
'getRegisteredServices',
'ConnectedApp.registerVmService',
'ConnectedApp.unregisterVmService',
'ConnectedApp.getVmServiceUris',
'FileSystem.readFileAsString',
'FileSystem.writeFileAsString',
'FileSystem.listDirectoryContents',
'FileSystem.setIDEWorkspaceRoots',
'FileSystem.getIDEWorkspaceRoots',
'FileSystem.getProjectRoots',
'UnifiedAnalytics.getConsentMessage',
'UnifiedAnalytics.shouldShowMessage',
'UnifiedAnalytics.clientShowedMessage',
'UnifiedAnalytics.telemetryEnabled',
'UnifiedAnalytics.setTelemetry',
'UnifiedAnalytics.send',
'UnifiedAnalytics.listFakeAnalyticsSentEvents',
],
'clientServices': [
{
'name': 'TestService',
'methods': [
{
'name': 'foo',
},
{
'name': 'bar',
'capabilities': <String, Object?>{
'language': 'french',
},
}
],
},
{
'name': 'OtherService',
'methods': [
{
'name': 'foo',
'capabilities': <String, Object?>{
'skills': 'baking',
},
}
],
},
],
},
);
});
});
});
@@ -112,12 +212,6 @@ void main() {
'timestamp': 1,
},
};
const exampleCallToReceive = {
'jsonrpc': '2.0',
'method': 'foo.bar',
'id': 0,
'params': <String, Object?>{},
};
final clientToServer = StreamController<String>();
final serverToClient = StreamController<String>();
@@ -129,12 +223,57 @@ void main() {
final clientReceivedEvent = await client.onEvent('testStream').first;
expect(clientReceivedEvent.data['foo'], 'bar');
// Send a request and ensure it comes on the stream.
// Discard "Connection closed with pending 'foo.bar'"" error because the
// test doesn't respond to it.
// Send requests and ensure they comes over the stream.
// Discard "Connection closed with pending 'foo.bar'" errors because the
// test doesn't respond to these.
// Call service with no parameters.
unawaited(
client.call('foo', 'bar').onError((_, __) => DTDResponse('', '', {})),
);
expect(jsonDecode(await clientToServer.stream.first), exampleCallToReceive);
// Call service with parameters.
unawaited(
client.call('foo', 'bar', params: {'test': 'test'}).onError(
(_, __) => DTDResponse('', '', {}),
),
);
// Call method with null service.
unawaited(
client.call(null, 'bar').onError((_, __) => DTDResponse('', '', {})),
);
const expectedRequests = 3;
final requestsReceived = <String>[];
final allRequestsReceived = Completer<void>();
StreamSubscription<String>? sub;
sub = clientToServer.stream.asBroadcastStream().listen((request) {
requestsReceived.add(request);
if (requestsReceived.length == expectedRequests) {
sub!.cancel();
allRequestsReceived.complete();
}
});
addTearDown(() => sub?.cancel());
await allRequestsReceived.future;
expect(
jsonDecode(requestsReceived[0]),
{'jsonrpc': '2.0', 'method': 'foo.bar', 'id': 0},
);
expect(
jsonDecode(requestsReceived[1]),
{
'jsonrpc': '2.0',
'method': 'foo.bar',
'id': 1,
'params': <String, Object?>{'test': 'test'},
},
);
expect(
jsonDecode(requestsReceived[2]),
{'jsonrpc': '2.0', 'method': 'bar', 'id': 2},
);
});
}
+85
View File
@@ -0,0 +1,85 @@
// Copyright (c) 2025, 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 'package:dart_service_protocol_shared/dart_service_protocol_shared.dart';
import 'package:dtd/dtd.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
import 'package:test/test.dart';
void main() {
// TODO(kenz): add test coverage for other response types.
group('RegisteredServicesResponse', () {
test('fromDTDResponse and toJson', () {
final response = DTDResponse('id', 'method', {
'type': 'RegisteredServicesResponse',
'dtdServices': ['service1', 'service2'],
'clientServices': [
ClientServiceInfo('client1', {
'method1': ClientServiceMethodInfo('method1', {'cap1': true}),
}).toJson(),
],
});
final servicesResponse =
RegisteredServicesResponse.fromDTDResponse(response);
expect(
RegisteredServicesResponse.type,
equals('RegisteredServicesResponse'),
);
expect(servicesResponse.dtdServices, equals(['service1', 'service2']));
expect(servicesResponse.clientServices.length, equals(1));
expect(servicesResponse.clientServices[0].name, equals('client1'));
expect(
servicesResponse.toJson(),
equals(
{
'type': 'RegisteredServicesResponse',
'dtdServices': ['service1', 'service2'],
'clientServices': [
{
'name': 'client1',
'methods': [
{
'name': 'method1',
'capabilities': {'cap1': true},
},
],
},
],
},
),
);
});
test('fromDTDResponse throws on invalid type', () {
final response = DTDResponse('id', 'method', {
'type': 'InvalidType',
'dtdServices': ['service1'],
'clientServices': [],
});
expect(
() => RegisteredServicesResponse.fromDTDResponse(response),
throwsA(isA<json_rpc.RpcException>()),
);
});
test('toString formats correctly', () {
final response = RegisteredServicesResponse(
dtdServices: ['service1'],
clientServices: [
ClientServiceInfo('client1', {
'method1': ClientServiceMethodInfo('method1', {'cap1': true}),
}),
],
);
expect(
response.toString(),
equals(
'[RegisteredServicesResponse dtdServices: [service1], '
'clientServices: [client1 (method1 {cap1: true})]]',
),
);
});
});
}
@@ -3,7 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:dtd/dtd.dart';
import 'package:dtd/src/unified_analytics_service.dart';
import 'package:dtd/src/services/unified_analytics_service.dart';
import 'package:test/test.dart';
import 'package:unified_analytics/unified_analytics.dart';
+12 -1
View File
@@ -21,10 +21,21 @@ To run the tooling daemon compiled with the Dart SDK:
2. run `dart tooling-daemon`
> :info The dart binary should be the one you just built in step 1.
### Testing changes
### Testing changes locally
To quickly test changes to the tooling daemon, start it by running:
```bash
dart run bin/dtd.dart
```
## Running tests
To run the tests under the `test/` directory, run `dart test test/`.
However, if you are testing changes that span `pkg/dtd` and `pkg/dtd_impl`,
you'll need to build the Dart SDK, and then use the built Dart executable to run
the test.
1. Build the Dart SDK: `./tools/build.py create_platform_sdk`
2. Run the test: `xcodebuild/ReleaseARM64/dart-sdk/bin/dart test test/`
+80 -16
View File
@@ -9,7 +9,7 @@ import 'package:sse/server/sse_handler.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
import 'package:stream_channel/stream_channel.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:dtd/dtd.dart' show RpcErrorCodes;
import 'package:dtd/dtd.dart' show RpcErrorCodes, RegisteredServicesResponse;
import 'constants.dart';
import 'dart_tooling_daemon.dart';
@@ -67,7 +67,7 @@ class DTDClient extends Client {
return;
}
return await _clientPeer.sendRequest(method, parameters.asMap);
return await _clientPeer.sendRequest(method, parameters.value);
}
@override
@@ -82,12 +82,22 @@ class DTDClient extends Client {
(_) => dtd.streamManager.onClientDisconnect(this),
);
/// The set of RPC methods registered by DTD itself.
///
/// This will be a combination of first party DTD methods and methods
/// registered by internal services.
final _dtdRpcMethods = <String>{};
/// Registers handlers for the Dart Tooling Daemon JSON RPC method endpoints.
void _registerJsonRpcMethods() {
_clientPeer.registerMethod('streamListen', _streamListen);
_clientPeer.registerMethod('streamCancel', _streamCancel);
_clientPeer.registerMethod('postEvent', _postEvent);
_clientPeer.registerMethod('registerService', _registerService);
_registerDtdMethod('streamListen', _streamListen);
_registerDtdMethod('streamCancel', _streamCancel);
_registerDtdMethod('postEvent', _postEvent);
_registerDtdMethod('registerService', _registerService);
_registerDtdMethod(
'getRegisteredServices',
_getRegisteredServices,
);
// Handle service extension invocations.
_clientPeer.registerFallback(_fallback);
@@ -125,14 +135,36 @@ class DTDClient extends Client {
DTDStreamManager.servicesStreamId,
DTDStreamManager.serviceRegisteredId,
_buildServiceRegisteredData(
service.name,
method.name,
method.capabilities,
service: service.name,
method: method.name,
capabilities: method.capabilities,
),
);
}
}
}
for (final method in _dtdRpcMethods) {
// If the DTD service method has the form 'service.method', split up the
// two values. Otherwise, leave the service null and use the entire name
// as the method.
String? serviceName;
String methodName;
final parts = method.split('.');
if (parts.length == 2) {
serviceName = parts[0];
}
methodName = parts.last;
_streamNotifyHelper(
DTDStreamManager.servicesStreamId,
DTDStreamManager.serviceRegisteredId,
_buildServiceRegisteredData(
service: serviceName,
method: methodName,
),
);
}
}
return RPCResponses.success;
@@ -259,11 +291,31 @@ class DTDClient extends Client {
dtd.streamManager.postEventHelper(
DTDStreamManager.servicesStreamId,
DTDStreamManager.serviceRegisteredId,
_buildServiceRegisteredData(serviceName, methodName, capabilities),
_buildServiceRegisteredData(
service: serviceName,
method: methodName,
capabilities: capabilities,
),
);
return RPCResponses.success;
}
/// Returns a structured response with all the currently registered services
/// available on this DTD instance.
Map<String, Object?> _getRegisteredServices() {
final clientServices =
dtd.clientManager.clients.map((client) => client.services);
final combinedClientServices = {
// This will not create collisions because [_registerService] ensures
// the uniqueness of service methods across clients.
for (final servicesForClient in clientServices) ...servicesForClient,
};
return RegisteredServicesResponse(
dtdServices: _dtdRpcMethods.toList(),
clientServices: combinedClientServices.values.toList(),
).toJson();
}
/// Cleans up when this client is disconnecting, before it is removed from the
/// client manager.
void onClientDisconnect() {
@@ -279,13 +331,18 @@ class DTDClient extends Client {
}
}
Map<String, Object?> _buildServiceRegisteredData(
String service,
String method,
/// Builds a structured object describing a service method that is being
/// registered on DTD.
///
/// [service] may be null if this service method is a first party service
/// method registered by DTD or by an internal service.
Map<String, Object?> _buildServiceRegisteredData({
required String? service,
required String method,
Map<String, Object?>? capabilities,
) {
}) {
return {
'service': service,
if (service != null) 'service': service,
'method': method,
if (capabilities != null) 'capabilities': capabilities,
};
@@ -356,6 +413,13 @@ class DTDClient extends Client {
void Function(json_rpc.Parameters parameters) callback,
) {
final combinedName = '$service.$method';
_clientPeer.registerMethod(combinedName, callback);
_registerDtdMethod(combinedName, callback);
}
/// A helper method for registering a service method on DTD and adding the
/// service method to the [_dtdRpcMethods] set.
void _registerDtdMethod(String method, Function callback) {
_dtdRpcMethods.add(method);
_clientPeer.registerMethod(method, callback);
}
}
+8 -3
View File
@@ -457,7 +457,9 @@ void main() {
);
// Expect we had a service registered event.
final event = await serviceStream.stream.first;
final event = await serviceStream.stream.firstWhere(
(event) => (event['eventData'] as Map?)?['service'] == 'foo1',
);
expect(event['streamId'], DTDStreamManager.servicesStreamId);
expect(event['eventKind'], DTDStreamManager.serviceRegisteredId);
expect(
@@ -535,9 +537,12 @@ void main() {
// Expect we had a service unregistered event (after the registered
// event).
final event = await serviceStream.stream.skip(1).first;
final event = await serviceStream.stream.skip(1).firstWhere((event) {
return event['eventKind'] ==
DTDStreamManager.serviceUnregisteredId &&
(event['eventData'] as Map?)?['service'] == 'foo1';
});
expect(event['streamId'], DTDStreamManager.servicesStreamId);
expect(event['eventKind'], DTDStreamManager.serviceUnregisteredId);
expect(
event['eventData'],
{