Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Register service extensions on a client that is connected to DDS #2388

Merged
merged 42 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
365a0ed
wip
elliette Mar 13, 2024
71eefee
Clean up
elliette Mar 14, 2024
207c6d9
Added a lot more logging
elliette Mar 18, 2024
4017b04
More logs and such
elliette Mar 19, 2024
bb51ce7
more logs
elliette Mar 19, 2024
6618001
Time for cleanup
elliette Mar 19, 2024
9ea40b1
Clean up
elliette Mar 19, 2024
cf329dd
Reset pubspec overrides
elliette Mar 19, 2024
0efc683
Add a new line in pubspec overrides
elliette Mar 19, 2024
9c09364
Implement yieldControlToDDS
elliette Mar 20, 2024
7c9ec16
Connect to DDS websocket with vmServiceConnectUri
elliette Mar 20, 2024
058b4db
Merge branch 'master' into dds-client
elliette Mar 20, 2024
8492849
Update CHANGELOG
elliette Mar 20, 2024
5b10085
Remove unused import
elliette Mar 20, 2024
5410498
Fix debug_service_test and cleanup
elliette Mar 22, 2024
1104b5d
Fix reload_correctness_test
elliette Mar 22, 2024
fcad8f4
Partially fix events_test
elliette Mar 22, 2024
23dd74b
Directly register all namespaced service extensions
elliette Mar 23, 2024
52feeb7
Pubspec overrides
elliette Mar 25, 2024
0502e78
Update versions for vm_service and vm_service_interface
elliette Mar 25, 2024
9d4b1ba
Format
elliette Mar 25, 2024
fa76c02
Trying to fix events_test
elliette Mar 25, 2024
77e0942
Fix reload_test
elliette Mar 25, 2024
0dcd257
Missing trailing comma
elliette Mar 25, 2024
a910bde
Trying to fix test
elliette Mar 25, 2024
821c155
Clean up
elliette Mar 25, 2024
338f038
Events tests passing
elliette Mar 26, 2024
19c4d6a
Analyzer error
elliette Mar 26, 2024
7539f21
More analyzer errors
elliette Mar 26, 2024
de41eca
Resolve merge conflicts
elliette Mar 26, 2024
c2d91c8
Added TODO
elliette Mar 26, 2024
6a94ad5
Some clean up
elliette Mar 26, 2024
d6ef4c4
Resolve merge conflicts
elliette Apr 24, 2024
5c9f737
Remove callServiceExtension from webdev
elliette Apr 24, 2024
749a55e
Add callServiceExtension back
elliette May 1, 2024
e93d469
Add test case back
elliette May 1, 2024
b8f2f39
Format
elliette May 1, 2024
329fecd
Skip other tests
elliette May 1, 2024
8ebf447
Fix restart when debugging webdev apps from VS Code
elliette May 6, 2024
018c9f2
Webdev creates its own connection with the VM service
elliette May 7, 2024
c845ba6
Remove webdev restart method
elliette May 7, 2024
d1b9b96
Fix and enable webdev tests
elliette May 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dwds/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## 24.1.0-wip

- Fix bug where debugging clients are not aware of service extensions when connecting to a new web app. - [#2388](https://github.com/dart-lang/webdev/pull/2388)

## 24.0.0

- Implement `setFlag` when it is called with `pause_isolates_on_start`. - [#2373](https://github.com/dart-lang/webdev/pull/2373)
Expand Down
3 changes: 3 additions & 0 deletions dwds/lib/src/connections/debug_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ class DebugConnection {
/// The endpoint of the Dart VM Service.
String get uri => _appDebugServices.debugService.uri;

// The endpoint of the Dart Development Service (DDS).
String? get ddsUri => _appDebugServices.ddsUri?.toString();

/// A client of the Dart VM Service with DWDS specific extensions.
VmService get vmService => _appDebugServices.dwdsVmClient.client;

Expand Down
300 changes: 212 additions & 88 deletions dwds/lib/src/dwds_vm_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,36 @@ import 'package:dwds/src/utilities/synchronized.dart';
import 'package:logging/logging.dart';
import 'package:uuid/uuid.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
import 'package:vm_service_interface/vm_service_interface.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';

final _logger = Logger('DwdsVmClient');

/// Type of requests added to the request controller.
typedef VmRequest = Map<String, Object>;

/// Type of responses added to the response controller.
typedef VmResponse = Map<String, Object?>;

enum _NamespacedServiceExtension {
extDwdsEmitEvent(method: 'ext.dwds.emitEvent'),
extDwdsScreenshot(method: 'ext.dwds.screenshot'),
extDwdsSendEvent(method: 'ext.dwds.sendEvent'),
flutterListViews(method: '_flutter.listViews');

const _NamespacedServiceExtension({required this.method});

final String method;
}

// A client of the vm service that registers some custom extensions like
// hotRestart.
class DwdsVmClient {
final VmService client;
final StreamController<Map<String, Object>> _requestController;
final StreamController<Map<String, Object?>> _responseController;

static const int kFeatureDisabled = 100;
static const String kFeatureDisabledMessage = 'Feature is disabled.';

/// Null until [close] is called.
///
/// All subsequent calls to [close] will return this future.
Expand All @@ -48,54 +63,210 @@ class DwdsVmClient {
static Future<DwdsVmClient> create(
DebugService debugService,
DwdsStats dwdsStats,
Uri? ddsUri,
) async {
// Set up hot restart as an extension.
final requestController = StreamController<Map<String, Object>>();
final responseController = StreamController<Map<String, Object?>>();
VmServerConnection(
requestController.stream,
responseController.sink,
debugService.serviceExtensionRegistry,
debugService.chromeProxyService,
final chromeProxyService =
debugService.chromeProxyService as ChromeProxyService;
final responseController = StreamController<VmResponse>();
final responseSink = responseController.sink;
// Response stream must be a broadcast stream so that it can have multiple
// listeners:
final responseStream = responseController.stream.asBroadcastStream();
final requestController = StreamController<VmRequest>();
final requestSink = requestController.sink;
final requestStream = requestController.stream;

_setUpVmServerConnection(
chromeProxyService: chromeProxyService,
debugService: debugService,
responseStream: responseStream,
responseSink: responseSink,
requestStream: requestStream,
requestSink: requestSink,
dwdsStats: dwdsStats,
);

final client = ddsUri == null
? _setUpVmClient(
responseStream: responseStream,
requestController: requestController,
requestSink: requestSink,
)
: await _setUpDdsClient(
ddsUri: ddsUri,
);

final dwdsVmClient =
DwdsVmClient(client, requestController, responseController);

await _registerServiceExtensions(
client: client,
chromeProxyService: chromeProxyService,
dwdsVmClient: dwdsVmClient,
);
final client =
VmService(responseController.stream.map(jsonEncode), (request) {

return dwdsVmClient;
}

/// Establishes a VM service client that is connected via DDS and registers
/// the service extensions on that client.
static Future<VmService> _setUpDdsClient({
required Uri ddsUri,
}) async {
final client = await vmServiceConnectUri(ddsUri.toString());
return client;
}

/// Establishes a VM service client that bypasses DDS and registers service
/// extensions on that client.
///
/// Note: This is only used in the rare cases where DDS is disabled.
static VmService _setUpVmClient({
required Stream<VmResponse> responseStream,
required StreamSink<VmRequest> requestSink,
required StreamController<VmRequest> requestController,
}) {
final client = VmService(responseStream.map(jsonEncode), (request) {
if (requestController.isClosed) {
_logger.warning(
'Attempted to send a request but the connection is closed:\n\n'
'$request');
return;
}
requestController.sink.add(Map<String, Object>.from(jsonDecode(request)));
requestSink.add(Map<String, Object>.from(jsonDecode(request)));
});
final chromeProxyService =
debugService.chromeProxyService as ChromeProxyService;

final dwdsVmClient =
DwdsVmClient(client, requestController, responseController);
return client;
}

// Register '_flutter.listViews' method on the chrome proxy service vm.
// In native world, this method is provided by the engine, but the web
// engine is not aware of the VM uri or the isolates.
//
// Issue: https://github.com/dart-lang/webdev/issues/1315
client.registerServiceCallback('_flutter.listViews', (request) async {
final vm = await chromeProxyService.getVM();
final isolates = vm.isolates;
return <String, dynamic>{
'result': <String, Object>{
'views': <Object>[
for (var isolate in isolates ?? [])
<String, Object>{
'id': isolate.id,
'isolate': isolate.toJson(),
},
],
},
};
/// Establishes a direct connection with the VM Server.
///
/// This is used to register the [_NamespacedServiceExtension]s. Because
/// namespaced service extensions are supposed to be registered by the engine,
/// we need to register them on the VM server connection instead of via DDS.
///
/// TODO(https://github.com/dart-lang/webdev/issues/1315): Ideally the engine
/// should register all Flutter service extensions. However, to do so we will
/// need to implement the missing isolate-related dart:developer APIs so that
/// the engine has access to this information.
static void _setUpVmServerConnection({
required ChromeProxyService chromeProxyService,
required DwdsStats dwdsStats,
required DebugService debugService,
required Stream<VmResponse> responseStream,
required StreamSink<VmResponse> responseSink,
required Stream<VmRequest> requestStream,
required StreamSink<VmRequest> requestSink,
}) {
responseStream.listen((request) async {
final response = await _maybeHandleServiceExtensionRequest(
request,
chromeProxyService: chromeProxyService,
dwdsStats: dwdsStats,
);
if (response != null) {
requestSink.add(response);
}
});
await client.registerService('_flutter.listViews', 'DWDS');

final vmServerConnection = VmServerConnection(
requestStream,
responseSink,
debugService.serviceExtensionRegistry,
debugService.chromeProxyService,
);

for (final extension in _NamespacedServiceExtension.values) {
debugService.serviceExtensionRegistry
.registerExtension(extension.method, vmServerConnection);
}
}

static Future<VmRequest?> _maybeHandleServiceExtensionRequest(
VmResponse request, {
required ChromeProxyService chromeProxyService,
required DwdsStats dwdsStats,
}) async {
VmRequest? response;
final method = request['method'];
if (method == _NamespacedServiceExtension.flutterListViews.method) {
response = await _flutterListViewsHandler(chromeProxyService);
} else if (method == _NamespacedServiceExtension.extDwdsEmitEvent.method) {
response = _extDwdsEmitEventHandler(request);
} else if (method == _NamespacedServiceExtension.extDwdsSendEvent.method) {
response = await _extDwdsSendEventHandler(request, dwdsStats);
} else if (method == _NamespacedServiceExtension.extDwdsScreenshot.method) {
response = await _extDwdsScreenshotHandler(chromeProxyService);
}

if (response != null) {
response['id'] = request['id'] as String;
// This is necessary even though DWDS doesn't use package:json_rpc_2.
// Without it, the response will be treated as invalid:
// https://github.com/dart-lang/json_rpc_2/blob/639857be892050159f5164c749d7947694976a4a/lib/src/server.dart#L252
response['jsonrpc'] = '2.0';
}

return response;
}

static Future<Map<String, Object>> _flutterListViewsHandler(
ChromeProxyService chromeProxyService,
) async {
final vm = await chromeProxyService.getVM();
final isolates = vm.isolates;
return <String, Object>{
'result': <String, Object>{
'views': <Object>[
for (var isolate in isolates ?? [])
<String, Object>{
'id': isolate.id,
'isolate': isolate.toJson(),
},
],
},
};
}

static Future<Map<String, Object>> _extDwdsScreenshotHandler(
ChromeProxyService chromeProxyService,
) async {
await chromeProxyService.remoteDebugger.enablePage();
final response = await chromeProxyService.remoteDebugger
.sendCommand('Page.captureScreenshot');
return {'result': response.result as Object};
}

static Future<Map<String, Object>> _extDwdsSendEventHandler(
VmResponse request,
DwdsStats dwdsStats,
) async {
_processSendEvent(request, dwdsStats);
return {'result': Success().toJson()};
}

static Map<String, Object> _extDwdsEmitEventHandler(
VmResponse request,
) {
final event = request['params'] as Map<String, dynamic>?;
if (event != null) {
final type = event['type'] as String?;
final payload = event['payload'] as Map<String, dynamic>?;
if (type != null && payload != null) {
emitEvent(
DwdsEvent(type, payload),
);
}
}

return {'result': Success().toJson()};
}

static Future<void> _registerServiceExtensions({
required VmService client,
required ChromeProxyService chromeProxyService,
required DwdsVmClient dwdsVmClient,
}) async {
client.registerServiceCallback(
'hotRestart',
(request) => captureElapsedTime(
Expand All @@ -113,55 +284,6 @@ class DwdsVmClient {
),
);
await client.registerService('fullReload', 'DWDS');

client.registerServiceCallback('ext.dwds.screenshot', (_) async {
await chromeProxyService.remoteDebugger.enablePage();
final response = await chromeProxyService.remoteDebugger
.sendCommand('Page.captureScreenshot');
return {'result': response.result};
});
await client.registerService('ext.dwds.screenshot', 'DWDS');

client.registerServiceCallback('ext.dwds.sendEvent', (event) async {
_processSendEvent(event, dwdsStats);
return {'result': Success().toJson()};
});
await client.registerService('ext.dwds.sendEvent', 'DWDS');

client.registerServiceCallback('ext.dwds.emitEvent', (event) async {
emitEvent(
DwdsEvent(
event['type'] as String,
event['payload'] as Map<String, dynamic>,
),
);
return {'result': Success().toJson()};
});
await client.registerService('ext.dwds.emitEvent', 'DWDS');

client.registerServiceCallback('_yieldControlToDDS', (request) async {
final ddsUri = request['uri'] as String?;
if (ddsUri == null) {
return RPCError(
request['method'] as String,
RPCErrorKind.kInvalidParams.code,
"'Missing parameter: 'uri'",
).toMap();
}
return DebugService.yieldControlToDDS(ddsUri)
? {'result': Success().toJson()}
: {
'error': {
'code': kFeatureDisabled,
'message': kFeatureDisabledMessage,
'data':
'Existing VM service clients prevent DDS from taking control.',
},
};
});
await client.registerService('_yieldControlToDDS', 'DWDS');

return dwdsVmClient;
}

Future<Map<String, dynamic>> hotRestart(
Expand All @@ -173,9 +295,11 @@ class DwdsVmClient {
}

void _processSendEvent(
Map<String, dynamic> event,
Map<String, dynamic> request,
DwdsStats dwdsStats,
) {
final event = request['params'] as Map<String, dynamic>?;
if (event == null) return;
final type = event['type'] as String?;
final payload = event['payload'] as Map<String, dynamic>?;
switch (type) {
Expand Down
8 changes: 5 additions & 3 deletions dwds/lib/src/handlers/dev_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -509,12 +509,14 @@ class DevHandler {
DebugService debugService,
) async {
final dwdsStats = DwdsStats();
final webdevClient = await DwdsVmClient.create(debugService, dwdsStats);
Uri? ddsUri;
if (_spawnDds) {
await debugService.startDartDevelopmentService();
final dds = await debugService.startDartDevelopmentService();
ddsUri = dds.wsUri;
}
final vmClient = await DwdsVmClient.create(debugService, dwdsStats, ddsUri);
final appDebugService =
AppDebugServices(debugService, webdevClient, dwdsStats);
AppDebugServices(debugService, vmClient, dwdsStats, ddsUri);
final encodedUri = await debugService.encodedUri;
_logger.info('Debug service listening on $encodedUri\n');
await appDebugService.chromeProxyService.remoteDebugger.sendCommand(
Expand Down
Loading