@@ -14,21 +14,36 @@ import 'package:dwds/src/utilities/synchronized.dart';
14
14
import 'package:logging/logging.dart' ;
15
15
import 'package:uuid/uuid.dart' ;
16
16
import 'package:vm_service/vm_service.dart' ;
17
+ import 'package:vm_service/vm_service_io.dart' ;
17
18
import 'package:vm_service_interface/vm_service_interface.dart' ;
18
19
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' ;
19
20
20
21
final _logger = Logger ('DwdsVmClient' );
21
22
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
+
22
40
// A client of the vm service that registers some custom extensions like
23
41
// hotRestart.
24
42
class DwdsVmClient {
25
43
final VmService client;
26
44
final StreamController <Map <String , Object >> _requestController;
27
45
final StreamController <Map <String , Object ?>> _responseController;
28
46
29
- static const int kFeatureDisabled = 100 ;
30
- static const String kFeatureDisabledMessage = 'Feature is disabled.' ;
31
-
32
47
/// Null until [close] is called.
33
48
///
34
49
/// All subsequent calls to [close] will return this future.
@@ -48,54 +63,210 @@ class DwdsVmClient {
48
63
static Future <DwdsVmClient > create (
49
64
DebugService debugService,
50
65
DwdsStats dwdsStats,
66
+ Uri ? ddsUri,
51
67
) 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,
60
106
);
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) {
63
130
if (requestController.isClosed) {
64
131
_logger.warning (
65
132
'Attempted to send a request but the connection is closed:\n\n '
66
133
'$request ' );
67
134
return ;
68
135
}
69
- requestController.sink .add (Map <String , Object >.from (jsonDecode (request)));
136
+ requestSink .add (Map <String , Object >.from (jsonDecode (request)));
70
137
});
71
- final chromeProxyService =
72
- debugService.chromeProxyService as ChromeProxyService ;
73
138
74
- final dwdsVmClient =
75
- DwdsVmClient (client, requestController, responseController);
139
+ return client;
140
+ }
76
141
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
+ }
96
170
});
97
- await client.registerService ('_flutter.listViews' , 'DWDS' );
98
171
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 {
99
270
client.registerServiceCallback (
100
271
'hotRestart' ,
101
272
(request) => captureElapsedTime (
@@ -113,55 +284,6 @@ class DwdsVmClient {
113
284
),
114
285
);
115
286
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;
165
287
}
166
288
167
289
Future <Map <String , dynamic >> hotRestart (
@@ -173,9 +295,11 @@ class DwdsVmClient {
173
295
}
174
296
175
297
void _processSendEvent (
176
- Map <String , dynamic > event ,
298
+ Map <String , dynamic > request ,
177
299
DwdsStats dwdsStats,
178
300
) {
301
+ final event = request['params' ] as Map <String , dynamic >? ;
302
+ if (event == null ) return ;
179
303
final type = event['type' ] as String ? ;
180
304
final payload = event['payload' ] as Map <String , dynamic >? ;
181
305
switch (type) {
0 commit comments