Skip to content

Commit 239cb7f

Browse files
authored
Register service extensions on a client that is connected to DDS (#2388)
1 parent c233e45 commit 239cb7f

File tree

15 files changed

+446
-193
lines changed

15 files changed

+446
-193
lines changed

dwds/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## 24.1.0-wip
22

3+
- 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)
4+
35
## 24.0.0
46

57
- Implement `setFlag` when it is called with `pause_isolates_on_start`. - [#2373](https://github.com/dart-lang/webdev/pull/2373)

dwds/lib/src/connections/debug_connection.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ class DebugConnection {
3333
/// The endpoint of the Dart VM Service.
3434
String get uri => _appDebugServices.debugService.uri;
3535

36+
// The endpoint of the Dart Development Service (DDS).
37+
String? get ddsUri => _appDebugServices.ddsUri?.toString();
38+
3639
/// A client of the Dart VM Service with DWDS specific extensions.
3740
VmService get vmService => _appDebugServices.dwdsVmClient.client;
3841

dwds/lib/src/dwds_vm_client.dart

Lines changed: 212 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,36 @@ import 'package:dwds/src/utilities/synchronized.dart';
1414
import 'package:logging/logging.dart';
1515
import 'package:uuid/uuid.dart';
1616
import 'package:vm_service/vm_service.dart';
17+
import 'package:vm_service/vm_service_io.dart';
1718
import 'package:vm_service_interface/vm_service_interface.dart';
1819
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
1920

2021
final _logger = Logger('DwdsVmClient');
2122

23+
/// Type of requests added to the request controller.
24+
typedef VmRequest = Map<String, Object>;
25+
26+
/// Type of responses added to the response controller.
27+
typedef VmResponse = Map<String, Object?>;
28+
29+
enum _NamespacedServiceExtension {
30+
extDwdsEmitEvent(method: 'ext.dwds.emitEvent'),
31+
extDwdsScreenshot(method: 'ext.dwds.screenshot'),
32+
extDwdsSendEvent(method: 'ext.dwds.sendEvent'),
33+
flutterListViews(method: '_flutter.listViews');
34+
35+
const _NamespacedServiceExtension({required this.method});
36+
37+
final String method;
38+
}
39+
2240
// A client of the vm service that registers some custom extensions like
2341
// hotRestart.
2442
class DwdsVmClient {
2543
final VmService client;
2644
final StreamController<Map<String, Object>> _requestController;
2745
final StreamController<Map<String, Object?>> _responseController;
2846

29-
static const int kFeatureDisabled = 100;
30-
static const String kFeatureDisabledMessage = 'Feature is disabled.';
31-
3247
/// Null until [close] is called.
3348
///
3449
/// All subsequent calls to [close] will return this future.
@@ -48,54 +63,210 @@ class DwdsVmClient {
4863
static Future<DwdsVmClient> create(
4964
DebugService debugService,
5065
DwdsStats dwdsStats,
66+
Uri? ddsUri,
5167
) async {
52-
// Set up hot restart as an extension.
53-
final requestController = StreamController<Map<String, Object>>();
54-
final responseController = StreamController<Map<String, Object?>>();
55-
VmServerConnection(
56-
requestController.stream,
57-
responseController.sink,
58-
debugService.serviceExtensionRegistry,
59-
debugService.chromeProxyService,
68+
final chromeProxyService =
69+
debugService.chromeProxyService as ChromeProxyService;
70+
final responseController = StreamController<VmResponse>();
71+
final responseSink = responseController.sink;
72+
// Response stream must be a broadcast stream so that it can have multiple
73+
// listeners:
74+
final responseStream = responseController.stream.asBroadcastStream();
75+
final requestController = StreamController<VmRequest>();
76+
final requestSink = requestController.sink;
77+
final requestStream = requestController.stream;
78+
79+
_setUpVmServerConnection(
80+
chromeProxyService: chromeProxyService,
81+
debugService: debugService,
82+
responseStream: responseStream,
83+
responseSink: responseSink,
84+
requestStream: requestStream,
85+
requestSink: requestSink,
86+
dwdsStats: dwdsStats,
87+
);
88+
89+
final client = ddsUri == null
90+
? _setUpVmClient(
91+
responseStream: responseStream,
92+
requestController: requestController,
93+
requestSink: requestSink,
94+
)
95+
: await _setUpDdsClient(
96+
ddsUri: ddsUri,
97+
);
98+
99+
final dwdsVmClient =
100+
DwdsVmClient(client, requestController, responseController);
101+
102+
await _registerServiceExtensions(
103+
client: client,
104+
chromeProxyService: chromeProxyService,
105+
dwdsVmClient: dwdsVmClient,
60106
);
61-
final client =
62-
VmService(responseController.stream.map(jsonEncode), (request) {
107+
108+
return dwdsVmClient;
109+
}
110+
111+
/// Establishes a VM service client that is connected via DDS and registers
112+
/// the service extensions on that client.
113+
static Future<VmService> _setUpDdsClient({
114+
required Uri ddsUri,
115+
}) async {
116+
final client = await vmServiceConnectUri(ddsUri.toString());
117+
return client;
118+
}
119+
120+
/// Establishes a VM service client that bypasses DDS and registers service
121+
/// extensions on that client.
122+
///
123+
/// Note: This is only used in the rare cases where DDS is disabled.
124+
static VmService _setUpVmClient({
125+
required Stream<VmResponse> responseStream,
126+
required StreamSink<VmRequest> requestSink,
127+
required StreamController<VmRequest> requestController,
128+
}) {
129+
final client = VmService(responseStream.map(jsonEncode), (request) {
63130
if (requestController.isClosed) {
64131
_logger.warning(
65132
'Attempted to send a request but the connection is closed:\n\n'
66133
'$request');
67134
return;
68135
}
69-
requestController.sink.add(Map<String, Object>.from(jsonDecode(request)));
136+
requestSink.add(Map<String, Object>.from(jsonDecode(request)));
70137
});
71-
final chromeProxyService =
72-
debugService.chromeProxyService as ChromeProxyService;
73138

74-
final dwdsVmClient =
75-
DwdsVmClient(client, requestController, responseController);
139+
return client;
140+
}
76141

77-
// Register '_flutter.listViews' method on the chrome proxy service vm.
78-
// In native world, this method is provided by the engine, but the web
79-
// engine is not aware of the VM uri or the isolates.
80-
//
81-
// Issue: https://github.com/dart-lang/webdev/issues/1315
82-
client.registerServiceCallback('_flutter.listViews', (request) async {
83-
final vm = await chromeProxyService.getVM();
84-
final isolates = vm.isolates;
85-
return <String, dynamic>{
86-
'result': <String, Object>{
87-
'views': <Object>[
88-
for (var isolate in isolates ?? [])
89-
<String, Object>{
90-
'id': isolate.id,
91-
'isolate': isolate.toJson(),
92-
},
93-
],
94-
},
95-
};
142+
/// Establishes a direct connection with the VM Server.
143+
///
144+
/// This is used to register the [_NamespacedServiceExtension]s. Because
145+
/// namespaced service extensions are supposed to be registered by the engine,
146+
/// we need to register them on the VM server connection instead of via DDS.
147+
///
148+
/// TODO(https://github.com/dart-lang/webdev/issues/1315): Ideally the engine
149+
/// should register all Flutter service extensions. However, to do so we will
150+
/// need to implement the missing isolate-related dart:developer APIs so that
151+
/// the engine has access to this information.
152+
static void _setUpVmServerConnection({
153+
required ChromeProxyService chromeProxyService,
154+
required DwdsStats dwdsStats,
155+
required DebugService debugService,
156+
required Stream<VmResponse> responseStream,
157+
required StreamSink<VmResponse> responseSink,
158+
required Stream<VmRequest> requestStream,
159+
required StreamSink<VmRequest> requestSink,
160+
}) {
161+
responseStream.listen((request) async {
162+
final response = await _maybeHandleServiceExtensionRequest(
163+
request,
164+
chromeProxyService: chromeProxyService,
165+
dwdsStats: dwdsStats,
166+
);
167+
if (response != null) {
168+
requestSink.add(response);
169+
}
96170
});
97-
await client.registerService('_flutter.listViews', 'DWDS');
98171

172+
final vmServerConnection = VmServerConnection(
173+
requestStream,
174+
responseSink,
175+
debugService.serviceExtensionRegistry,
176+
debugService.chromeProxyService,
177+
);
178+
179+
for (final extension in _NamespacedServiceExtension.values) {
180+
debugService.serviceExtensionRegistry
181+
.registerExtension(extension.method, vmServerConnection);
182+
}
183+
}
184+
185+
static Future<VmRequest?> _maybeHandleServiceExtensionRequest(
186+
VmResponse request, {
187+
required ChromeProxyService chromeProxyService,
188+
required DwdsStats dwdsStats,
189+
}) async {
190+
VmRequest? response;
191+
final method = request['method'];
192+
if (method == _NamespacedServiceExtension.flutterListViews.method) {
193+
response = await _flutterListViewsHandler(chromeProxyService);
194+
} else if (method == _NamespacedServiceExtension.extDwdsEmitEvent.method) {
195+
response = _extDwdsEmitEventHandler(request);
196+
} else if (method == _NamespacedServiceExtension.extDwdsSendEvent.method) {
197+
response = await _extDwdsSendEventHandler(request, dwdsStats);
198+
} else if (method == _NamespacedServiceExtension.extDwdsScreenshot.method) {
199+
response = await _extDwdsScreenshotHandler(chromeProxyService);
200+
}
201+
202+
if (response != null) {
203+
response['id'] = request['id'] as String;
204+
// This is necessary even though DWDS doesn't use package:json_rpc_2.
205+
// Without it, the response will be treated as invalid:
206+
// https://github.com/dart-lang/json_rpc_2/blob/639857be892050159f5164c749d7947694976a4a/lib/src/server.dart#L252
207+
response['jsonrpc'] = '2.0';
208+
}
209+
210+
return response;
211+
}
212+
213+
static Future<Map<String, Object>> _flutterListViewsHandler(
214+
ChromeProxyService chromeProxyService,
215+
) async {
216+
final vm = await chromeProxyService.getVM();
217+
final isolates = vm.isolates;
218+
return <String, Object>{
219+
'result': <String, Object>{
220+
'views': <Object>[
221+
for (var isolate in isolates ?? [])
222+
<String, Object>{
223+
'id': isolate.id,
224+
'isolate': isolate.toJson(),
225+
},
226+
],
227+
},
228+
};
229+
}
230+
231+
static Future<Map<String, Object>> _extDwdsScreenshotHandler(
232+
ChromeProxyService chromeProxyService,
233+
) async {
234+
await chromeProxyService.remoteDebugger.enablePage();
235+
final response = await chromeProxyService.remoteDebugger
236+
.sendCommand('Page.captureScreenshot');
237+
return {'result': response.result as Object};
238+
}
239+
240+
static Future<Map<String, Object>> _extDwdsSendEventHandler(
241+
VmResponse request,
242+
DwdsStats dwdsStats,
243+
) async {
244+
_processSendEvent(request, dwdsStats);
245+
return {'result': Success().toJson()};
246+
}
247+
248+
static Map<String, Object> _extDwdsEmitEventHandler(
249+
VmResponse request,
250+
) {
251+
final event = request['params'] as Map<String, dynamic>?;
252+
if (event != null) {
253+
final type = event['type'] as String?;
254+
final payload = event['payload'] as Map<String, dynamic>?;
255+
if (type != null && payload != null) {
256+
emitEvent(
257+
DwdsEvent(type, payload),
258+
);
259+
}
260+
}
261+
262+
return {'result': Success().toJson()};
263+
}
264+
265+
static Future<void> _registerServiceExtensions({
266+
required VmService client,
267+
required ChromeProxyService chromeProxyService,
268+
required DwdsVmClient dwdsVmClient,
269+
}) async {
99270
client.registerServiceCallback(
100271
'hotRestart',
101272
(request) => captureElapsedTime(
@@ -113,55 +284,6 @@ class DwdsVmClient {
113284
),
114285
);
115286
await client.registerService('fullReload', 'DWDS');
116-
117-
client.registerServiceCallback('ext.dwds.screenshot', (_) async {
118-
await chromeProxyService.remoteDebugger.enablePage();
119-
final response = await chromeProxyService.remoteDebugger
120-
.sendCommand('Page.captureScreenshot');
121-
return {'result': response.result};
122-
});
123-
await client.registerService('ext.dwds.screenshot', 'DWDS');
124-
125-
client.registerServiceCallback('ext.dwds.sendEvent', (event) async {
126-
_processSendEvent(event, dwdsStats);
127-
return {'result': Success().toJson()};
128-
});
129-
await client.registerService('ext.dwds.sendEvent', 'DWDS');
130-
131-
client.registerServiceCallback('ext.dwds.emitEvent', (event) async {
132-
emitEvent(
133-
DwdsEvent(
134-
event['type'] as String,
135-
event['payload'] as Map<String, dynamic>,
136-
),
137-
);
138-
return {'result': Success().toJson()};
139-
});
140-
await client.registerService('ext.dwds.emitEvent', 'DWDS');
141-
142-
client.registerServiceCallback('_yieldControlToDDS', (request) async {
143-
final ddsUri = request['uri'] as String?;
144-
if (ddsUri == null) {
145-
return RPCError(
146-
request['method'] as String,
147-
RPCErrorKind.kInvalidParams.code,
148-
"'Missing parameter: 'uri'",
149-
).toMap();
150-
}
151-
return DebugService.yieldControlToDDS(ddsUri)
152-
? {'result': Success().toJson()}
153-
: {
154-
'error': {
155-
'code': kFeatureDisabled,
156-
'message': kFeatureDisabledMessage,
157-
'data':
158-
'Existing VM service clients prevent DDS from taking control.',
159-
},
160-
};
161-
});
162-
await client.registerService('_yieldControlToDDS', 'DWDS');
163-
164-
return dwdsVmClient;
165287
}
166288

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

175297
void _processSendEvent(
176-
Map<String, dynamic> event,
298+
Map<String, dynamic> request,
177299
DwdsStats dwdsStats,
178300
) {
301+
final event = request['params'] as Map<String, dynamic>?;
302+
if (event == null) return;
179303
final type = event['type'] as String?;
180304
final payload = event['payload'] as Map<String, dynamic>?;
181305
switch (type) {

dwds/lib/src/handlers/dev_handler.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -509,12 +509,14 @@ class DevHandler {
509509
DebugService debugService,
510510
) async {
511511
final dwdsStats = DwdsStats();
512-
final webdevClient = await DwdsVmClient.create(debugService, dwdsStats);
512+
Uri? ddsUri;
513513
if (_spawnDds) {
514-
await debugService.startDartDevelopmentService();
514+
final dds = await debugService.startDartDevelopmentService();
515+
ddsUri = dds.wsUri;
515516
}
517+
final vmClient = await DwdsVmClient.create(debugService, dwdsStats, ddsUri);
516518
final appDebugService =
517-
AppDebugServices(debugService, webdevClient, dwdsStats);
519+
AppDebugServices(debugService, vmClient, dwdsStats, ddsUri);
518520
final encodedUri = await debugService.encodedUri;
519521
_logger.info('Debug service listening on $encodedUri\n');
520522
await appDebugService.chromeProxyService.remoteDebugger.sendCommand(

0 commit comments

Comments
 (0)