1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
---|---|
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'dart:async'; |
6 | |
7 | import 'package:meta/meta.dart'show visibleForTesting; |
8 | import 'package:vm_service/vm_service.dart'as vm_service; |
9 | |
10 | import 'base/common.dart'; |
11 | import 'base/context.dart'; |
12 | import 'base/io.dart' as io; |
13 | import 'base/logger.dart'; |
14 | import 'base/utils.dart'; |
15 | import 'cache.dart'; |
16 | import 'convert.dart'; |
17 | import 'device.dart'; |
18 | import 'globals.dart' as globals; |
19 | import 'project.dart'; |
20 | import 'version.dart'; |
21 | |
22 | const String kResultType = 'type'; |
23 | const String kResultTypeSuccess = 'Success'; |
24 | const String kError = 'error'; |
25 | |
26 | const String kSetAssetBundlePathMethod = '_flutter.setAssetBundlePath'; |
27 | const String kFlushUIThreadTasksMethod = '_flutter.flushUIThreadTasks'; |
28 | const String kRunInViewMethod = '_flutter.runInView'; |
29 | const String kListViewsMethod = '_flutter.listViews'; |
30 | const String kScreenshotSkpMethod = '_flutter.screenshotSkp'; |
31 | const String kReloadAssetFonts = '_flutter.reloadAssetFonts'; |
32 | |
33 | const String kFlutterToolAlias = 'Flutter Tools'; |
34 | |
35 | const String kReloadSourcesServiceName = 'reloadSources'; |
36 | const String kHotRestartServiceName = 'hotRestart'; |
37 | const String kFlutterVersionServiceName = 'flutterVersion'; |
38 | const String kCompileExpressionServiceName = 'compileExpression'; |
39 | const String kFlutterMemoryInfoServiceName = 'flutterMemoryInfo'; |
40 | |
41 | /// The error response code from an unrecoverable compilation failure. |
42 | const int kIsolateReloadBarred = 1005; |
43 | |
44 | /// Override `WebSocketConnector` in [context] to use a different constructor |
45 | /// for [WebSocket]s (used by tests). |
46 | typedef WebSocketConnector = |
47 | Future<io.WebSocket> Function( |
48 | String url, { |
49 | io.CompressionOptions compression, |
50 | required Logger logger, |
51 | }); |
52 | |
53 | typedef PrintStructuredErrorLogMethod = void Function(vm_service.Event); |
54 | |
55 | WebSocketConnector _openChannel = _defaultOpenChannel; |
56 | |
57 | /// A testing only override of the WebSocket connector. |
58 | /// |
59 | /// Provide a `null` value to restore the original connector. |
60 | @visibleForTesting |
61 | set openChannelForTesting(WebSocketConnector? connector) { |
62 | _openChannel = connector ?? _defaultOpenChannel; |
63 | } |
64 | |
65 | /// A function that reacts to the invocation of the 'reloadSources' service. |
66 | /// |
67 | /// The VM Service Protocol allows clients to register custom services that |
68 | /// can be invoked by other clients through the service protocol itself. |
69 | /// |
70 | /// Clients like VmService use external 'reloadSources' services, |
71 | /// when available, instead of the VM internal one. This allows these clients to |
72 | /// invoke Flutter HotReload when connected to a Flutter Application started in |
73 | /// hot mode. |
74 | /// |
75 | /// See: https://github.com/dart-lang/sdk/issues/30023 |
76 | typedef ReloadSources = Future<void> Function(String isolateId, {bool force, bool pause}); |
77 | |
78 | typedef Restart = Future<void> Function({bool pause}); |
79 | |
80 | typedef CompileExpression = |
81 | Future<String> Function( |
82 | String isolateId, |
83 | String expression, |
84 | List<String> definitions, |
85 | List<String> definitionTypes, |
86 | List<String> typeDefinitions, |
87 | List<String> typeBounds, |
88 | List<String> typeDefaults, |
89 | String libraryUri, |
90 | String? klass, |
91 | String? method, |
92 | bool isStatic, |
93 | ); |
94 | |
95 | Future<io.WebSocket> _defaultOpenChannel( |
96 | String url, { |
97 | io.CompressionOptions compression = io.CompressionOptions.compressionDefault, |
98 | required Logger logger, |
99 | }) async { |
100 | Duration delay = const Duration(milliseconds: 100); |
101 | int attempts = 0; |
102 | io.WebSocket? socket; |
103 | |
104 | Future<void> handleError(Object? e) async { |
105 | void Function(String) printVisibleTrace = logger.printTrace; |
106 | if (attempts == 10) { |
107 | logger.printStatus('Connecting to the VM Service is taking longer than expected...'); |
108 | } else if (attempts == 20) { |
109 | logger.printStatus('Still attempting to connect to the VM Service...'); |
110 | logger.printStatus( |
111 | 'If you do NOT see the Flutter application running, it might have ' |
112 | 'crashed. The device logs (e.g. from adb or XCode) might have more ' |
113 | 'details.', |
114 | ); |
115 | logger.printStatus( |
116 | 'If you do see the Flutter application running on the device, try ' |
117 | 're-running with --host-vmservice-port to use a specific port known to ' |
118 | 'be available.', |
119 | ); |
120 | } else if (attempts % 50 == 0) { |
121 | printVisibleTrace = logger.printStatus; |
122 | } |
123 | |
124 | printVisibleTrace('Exception attempting to connect to the VM Service:$e '); |
125 | printVisibleTrace('This was attempt #$attempts . Will retry in$delay .'); |
126 | |
127 | // Delay next attempt. |
128 | await Future<void>.delayed(delay); |
129 | |
130 | // Back off exponentially, up to 1600ms per attempt. |
131 | if (delay < const Duration(seconds: 1)) { |
132 | delay *= 2; |
133 | } |
134 | } |
135 | |
136 | final WebSocketConnector constructor = |
137 | context.get<WebSocketConnector>() ?? |
138 | ( |
139 | String url, { |
140 | io.CompressionOptions compression = io.CompressionOptions.compressionDefault, |
141 | Logger? logger, |
142 | }) => io.WebSocket.connect(url, compression: compression); |
143 | |
144 | while (socket == null) { |
145 | attempts += 1; |
146 | try { |
147 | socket = await constructor(url, compression: compression, logger: logger); |
148 | } on io.WebSocketException catch (e) { |
149 | await handleError(e); |
150 | } on io.SocketException catch (e) { |
151 | await handleError(e); |
152 | } |
153 | } |
154 | return socket; |
155 | } |
156 | |
157 | /// Override `VMServiceConnector` in [context] to return a different VMService |
158 | /// from [VMService.connect] (used by tests). |
159 | typedef VMServiceConnector = |
160 | Future<FlutterVmService> Function( |
161 | Uri httpUri, { |
162 | ReloadSources? reloadSources, |
163 | Restart? restart, |
164 | CompileExpression? compileExpression, |
165 | FlutterProject? flutterProject, |
166 | PrintStructuredErrorLogMethod? printStructuredErrorLogMethod, |
167 | io.CompressionOptions compression, |
168 | Device? device, |
169 | required Logger logger, |
170 | }); |
171 | |
172 | /// Set up the VM Service client by attaching services for each of the provided |
173 | /// callbacks. |
174 | /// |
175 | /// All parameters besides [vmService] may be null. |
176 | Future<vm_service.VmService> setUpVmService({ |
177 | ReloadSources? reloadSources, |
178 | Restart? restart, |
179 | CompileExpression? compileExpression, |
180 | Device? device, |
181 | FlutterProject? flutterProject, |
182 | PrintStructuredErrorLogMethod? printStructuredErrorLogMethod, |
183 | required vm_service.VmService vmService, |
184 | }) async { |
185 | // Each service registration requires a request to the attached VM service. Since the |
186 | // order of these requests does not matter, store each future in a list and await |
187 | // all at the end of this method. |
188 | final List<Future<vm_service.Success?>> registrationRequests = <Future<vm_service.Success?>>[]; |
189 | if (reloadSources != null) { |
190 | vmService.registerServiceCallback(kReloadSourcesServiceName, ( |
191 | Map<String, Object?> params, |
192 | ) async { |
193 | final String isolateId = _validateRpcStringParam('reloadSources', params, 'isolateId'); |
194 | final bool force = _validateRpcBoolParam('reloadSources', params, 'force'); |
195 | final bool pause = _validateRpcBoolParam('reloadSources', params, 'pause'); |
196 | |
197 | await reloadSources(isolateId, force: force, pause: pause); |
198 | |
199 | return <String, Object>{ |
200 | 'result': <String, Object>{kResultType: kResultTypeSuccess}, |
201 | }; |
202 | }); |
203 | registrationRequests.add( |
204 | vmService.registerService(kReloadSourcesServiceName, kFlutterToolAlias), |
205 | ); |
206 | } |
207 | |
208 | if (restart != null) { |
209 | vmService.registerServiceCallback(kHotRestartServiceName, (Map<String, Object?> params) async { |
210 | final bool pause = _validateRpcBoolParam('compileExpression', params, 'pause'); |
211 | await restart(pause: pause); |
212 | return <String, Object>{ |
213 | 'result': <String, Object>{kResultType: kResultTypeSuccess}, |
214 | }; |
215 | }); |
216 | registrationRequests.add(vmService.registerService(kHotRestartServiceName, kFlutterToolAlias)); |
217 | } |
218 | |
219 | vmService.registerServiceCallback(kFlutterVersionServiceName, ( |
220 | Map<String, Object?> params, |
221 | ) async { |
222 | final FlutterVersion version = |
223 | context.get<FlutterVersion>() ?? |
224 | FlutterVersion(fs: globals.fs, flutterRoot: Cache.flutterRoot!); |
225 | final Map<String, Object> versionJson = version.toJson(); |
226 | versionJson['frameworkRevisionShort'] = version.frameworkRevisionShort; |
227 | versionJson['engineRevisionShort'] = version.engineRevisionShort; |
228 | return <String, Object>{ |
229 | 'result': <String, Object>{kResultType: kResultTypeSuccess, ...versionJson}, |
230 | }; |
231 | }); |
232 | registrationRequests.add( |
233 | vmService.registerService(kFlutterVersionServiceName, kFlutterToolAlias), |
234 | ); |
235 | |
236 | if (compileExpression != null) { |
237 | vmService.registerServiceCallback(kCompileExpressionServiceName, ( |
238 | Map<String, Object?> params, |
239 | ) async { |
240 | final String isolateId = _validateRpcStringParam('compileExpression', params, 'isolateId'); |
241 | final String expression = _validateRpcStringParam('compileExpression', params, 'expression'); |
242 | final List<String> definitions = List<String>.from(params['definitions']! as List<Object?>); |
243 | final List<String> definitionTypes = List<String>.from( |
244 | params['definitionTypes']! as List<Object?>, |
245 | ); |
246 | final List<String> typeDefinitions = List<String>.from( |
247 | params['typeDefinitions']! as List<Object?>, |
248 | ); |
249 | final List<String> typeBounds = List<String>.from(params['typeBounds']! as List<Object?>); |
250 | final List<String> typeDefaults = List<String>.from(params['typeDefaults']! as List<Object?>); |
251 | final String libraryUri = params['libraryUri']! as String; |
252 | final String? klass = params['klass'] as String?; |
253 | final String? method = params['method'] as String?; |
254 | final bool isStatic = _validateRpcBoolParam('compileExpression', params, 'isStatic'); |
255 | |
256 | try { |
257 | final String kernelBytesBase64 = await compileExpression( |
258 | isolateId, |
259 | expression, |
260 | definitions, |
261 | definitionTypes, |
262 | typeDefinitions, |
263 | typeBounds, |
264 | typeDefaults, |
265 | libraryUri, |
266 | klass, |
267 | method, |
268 | isStatic, |
269 | ); |
270 | return <String, Object>{ |
271 | kResultType: kResultTypeSuccess, |
272 | 'result': <String, String>{ 'kernelBytes': kernelBytesBase64}, |
273 | }; |
274 | } on VmServiceExpressionCompilationException catch (e) { |
275 | // In most situations, we'd just let VmService catch this exception and |
276 | // build the error response. However, in this case we build the error |
277 | // response manually and return it to avoid including the stack trace |
278 | // from the tool in the response, instead returning the compilation |
279 | // error message in the 'details' property of the returned error object. |
280 | return <String, Object>{ |
281 | kError: |
282 | vm_service.RPCError.withDetails( |
283 | 'compileExpression', |
284 | vm_service.RPCErrorKind.kExpressionCompilationError.code, |
285 | vm_service.RPCErrorKind.kExpressionCompilationError.message, |
286 | details: e.errorMessage, |
287 | ).toMap(), |
288 | }; |
289 | } |
290 | }); |
291 | registrationRequests.add( |
292 | vmService.registerService(kCompileExpressionServiceName, kFlutterToolAlias), |
293 | ); |
294 | } |
295 | if (device != null) { |
296 | vmService.registerServiceCallback(kFlutterMemoryInfoServiceName, ( |
297 | Map<String, Object?> params, |
298 | ) async { |
299 | final MemoryInfo result = await device.queryMemoryInfo(); |
300 | return <String, Object>{ |
301 | 'result': <String, Object>{kResultType: kResultTypeSuccess, ...result.toJson()}, |
302 | }; |
303 | }); |
304 | registrationRequests.add( |
305 | vmService.registerService(kFlutterMemoryInfoServiceName, kFlutterToolAlias), |
306 | ); |
307 | } |
308 | |
309 | if (printStructuredErrorLogMethod != null) { |
310 | vmService.onExtensionEvent.listen(printStructuredErrorLogMethod); |
311 | registrationRequests.add( |
312 | vmService |
313 | .streamListen(vm_service.EventStreams.kExtension) |
314 | .then<vm_service.Success?>( |
315 | (vm_service.Success success) => success, |
316 | // It is safe to ignore this error because we expect an error to be |
317 | // thrown if we're already subscribed. |
318 | onError: (Object error, StackTrace stackTrace) { |
319 | if (error is vm_service.RPCError) { |
320 | return null; |
321 | } |
322 | return Future<vm_service.Success?>.error(error, stackTrace); |
323 | }, |
324 | ), |
325 | ); |
326 | } |
327 | |
328 | try { |
329 | await Future.wait(registrationRequests); |
330 | } on vm_service.RPCError catch (e) { |
331 | throwToolExit('Failed to register service methods on attached VM Service:$e '); |
332 | } |
333 | return vmService; |
334 | } |
335 | |
336 | /// Connect to a Dart VM Service at [httpUri]. |
337 | /// |
338 | /// If the [reloadSources] parameter is not null, the 'reloadSources' service |
339 | /// will be registered. The VM Service Protocol allows clients to register |
340 | /// custom services that can be invoked by other clients through the service |
341 | /// protocol itself. |
342 | /// |
343 | /// See: https://github.com/dart-lang/sdk/commit/df8bf384eb815cf38450cb50a0f4b62230fba217 |
344 | Future<FlutterVmService> connectToVmService( |
345 | Uri httpUri, { |
346 | ReloadSources? reloadSources, |
347 | Restart? restart, |
348 | CompileExpression? compileExpression, |
349 | FlutterProject? flutterProject, |
350 | PrintStructuredErrorLogMethod? printStructuredErrorLogMethod, |
351 | io.CompressionOptions compression = io.CompressionOptions.compressionDefault, |
352 | Device? device, |
353 | required Logger logger, |
354 | }) async { |
355 | final VMServiceConnector connector = context.get<VMServiceConnector>() ?? _connect; |
356 | return connector( |
357 | httpUri, |
358 | reloadSources: reloadSources, |
359 | restart: restart, |
360 | compileExpression: compileExpression, |
361 | compression: compression, |
362 | device: device, |
363 | flutterProject: flutterProject, |
364 | printStructuredErrorLogMethod: printStructuredErrorLogMethod, |
365 | logger: logger, |
366 | ); |
367 | } |
368 | |
369 | Future<vm_service.VmService> createVmServiceDelegate( |
370 | Uri wsUri, { |
371 | io.CompressionOptions compression = io.CompressionOptions.compressionDefault, |
372 | required Logger logger, |
373 | }) async { |
374 | final io.WebSocket channel = await _openChannel( |
375 | wsUri.toString(), |
376 | compression: compression, |
377 | logger: logger, |
378 | ); |
379 | return vm_service.VmService( |
380 | channel, |
381 | channel.add, |
382 | disposeHandler: () async { |
383 | await channel.close(); |
384 | }, |
385 | ); |
386 | } |
387 | |
388 | Future<FlutterVmService> _connect( |
389 | Uri httpUri, { |
390 | ReloadSources? reloadSources, |
391 | Restart? restart, |
392 | CompileExpression? compileExpression, |
393 | FlutterProject? flutterProject, |
394 | PrintStructuredErrorLogMethod? printStructuredErrorLogMethod, |
395 | io.CompressionOptions compression = io.CompressionOptions.compressionDefault, |
396 | Device? device, |
397 | required Logger logger, |
398 | }) async { |
399 | final Uri wsUri = httpUri.replace(scheme: 'ws', path: urlContext.join(httpUri.path, 'ws')); |
400 | final vm_service.VmService delegateService = await createVmServiceDelegate( |
401 | wsUri, |
402 | compression: compression, |
403 | logger: logger, |
404 | ); |
405 | |
406 | final vm_service.VmService service = await setUpVmService( |
407 | reloadSources: reloadSources, |
408 | restart: restart, |
409 | compileExpression: compileExpression, |
410 | device: device, |
411 | flutterProject: flutterProject, |
412 | printStructuredErrorLogMethod: printStructuredErrorLogMethod, |
413 | vmService: delegateService, |
414 | ); |
415 | |
416 | // This call is to ensure we are able to establish a connection instead of |
417 | // keeping on trucking and failing farther down the process. |
418 | await delegateService.getVersion(); |
419 | return FlutterVmService(service, httpAddress: httpUri, wsAddress: wsUri); |
420 | } |
421 | |
422 | String _validateRpcStringParam(String methodName, Map<String, Object?> params, String paramName) { |
423 | final Object? value = params[paramName]; |
424 | if (value is! String || value.isEmpty) { |
425 | throw vm_service.RPCError( |
426 | methodName, |
427 | vm_service.RPCErrorKind.kInvalidParams.code, |
428 | "Invalid '$paramName ':$value ", |
429 | ); |
430 | } |
431 | return value; |
432 | } |
433 | |
434 | bool _validateRpcBoolParam(String methodName, Map<String, Object?> params, String paramName) { |
435 | final Object? value = params[paramName]; |
436 | if (value != null && value is! bool) { |
437 | throw vm_service.RPCError( |
438 | methodName, |
439 | vm_service.RPCErrorKind.kInvalidParams.code, |
440 | "Invalid '$paramName ':$value ", |
441 | ); |
442 | } |
443 | return (value as bool?) ?? false; |
444 | } |
445 | |
446 | /// Peered to an Android/iOS FlutterView widget on a device. |
447 | class FlutterView { |
448 | FlutterView({required this.id, required this.uiIsolate}); |
449 | |
450 | factory FlutterView.parse(Map<String, Object?> json) { |
451 | final Map<String, Object?>? rawIsolate = json['isolate'] as Map<String, Object?>?; |
452 | vm_service.IsolateRef? isolate; |
453 | if (rawIsolate != null) { |
454 | rawIsolate['number'] = rawIsolate[ 'number']?.toString(); |
455 | isolate = vm_service.IsolateRef.parse(rawIsolate); |
456 | } |
457 | return FlutterView(id: json['id']! as String, uiIsolate: isolate); |
458 | } |
459 | |
460 | final vm_service.IsolateRef? uiIsolate; |
461 | final String id; |
462 | |
463 | bool get hasIsolate => uiIsolate != null; |
464 | |
465 | @override |
466 | String toString() => id; |
467 | |
468 | Map<String, Object?> toJson() { |
469 | return <String, Object?>{'id': id, 'isolate': uiIsolate?.toJson()}; |
470 | } |
471 | } |
472 | |
473 | /// Flutter specific VM Service functionality. |
474 | class FlutterVmService { |
475 | FlutterVmService(this.service, {this.wsAddress, this.httpAddress}); |
476 | |
477 | final vm_service.VmService service; |
478 | final Uri? wsAddress; |
479 | final Uri? httpAddress; |
480 | |
481 | /// Calls [service.getVM]. However, in the case that an [vm_service.RPCError] |
482 | /// is thrown due to the service being disconnected, the error is discarded |
483 | /// and null is returned. |
484 | Future<vm_service.VM?> getVmGuarded() async { |
485 | try { |
486 | return await service.getVM(); |
487 | } on vm_service.RPCError catch (err) { |
488 | if (err.code == vm_service.RPCErrorKind.kServiceDisappeared.code || |
489 | err.code == vm_service.RPCErrorKind.kConnectionDisposed.code || |
490 | err.message.contains('Service connection disposed')) { |
491 | globals.printTrace('VmService.getVm call failed:$err '); |
492 | return null; |
493 | } |
494 | rethrow; |
495 | } |
496 | } |
497 | |
498 | Future<vm_service.Response?> callMethodWrapper( |
499 | String method, { |
500 | String? isolateId, |
501 | Map<String, Object?>? args, |
502 | }) async { |
503 | try { |
504 | return await service.callMethod(method, isolateId: isolateId, args: args); |
505 | } on vm_service.RPCError catch (e) { |
506 | // If the service disappears mid-request the tool is unable to recover |
507 | // and should begin to shutdown due to the service connection closing. |
508 | // Swallow the exception here and let the shutdown logic elsewhere deal |
509 | // with cleaning up. |
510 | if (e.code == vm_service.RPCErrorKind.kServiceDisappeared.code || |
511 | e.code == vm_service.RPCErrorKind.kConnectionDisposed.code || |
512 | e.message.contains('Service connection disposed')) { |
513 | return null; |
514 | } |
515 | rethrow; |
516 | } |
517 | } |
518 | |
519 | /// Set the asset directory for the an attached Flutter view. |
520 | Future<void> setAssetDirectory({ |
521 | required Uri assetsDirectory, |
522 | required String? viewId, |
523 | required String? uiIsolateId, |
524 | required bool windows, |
525 | }) async { |
526 | await callMethodWrapper( |
527 | kSetAssetBundlePathMethod, |
528 | isolateId: uiIsolateId, |
529 | args: <String, Object?>{ |
530 | 'viewId': viewId, |
531 | 'assetDirectory': assetsDirectory.toFilePath(windows: windows), |
532 | }, |
533 | ); |
534 | } |
535 | |
536 | /// Flush all tasks on the UI thread for an attached Flutter view. |
537 | /// |
538 | /// This method is currently used only for benchmarking. |
539 | Future<void> flushUIThreadTasks({required String uiIsolateId}) async { |
540 | await callMethodWrapper( |
541 | kFlushUIThreadTasksMethod, |
542 | args: <String, String>{'isolateId': uiIsolateId}, |
543 | ); |
544 | } |
545 | |
546 | /// Launch the Dart isolate with entrypoint [main] in the Flutter engine [viewId] |
547 | /// with [assetsDirectory] as the devFS. |
548 | /// |
549 | /// This method is used by the tool to hot restart an already running Flutter |
550 | /// engine. |
551 | Future<void> runInView({ |
552 | required String viewId, |
553 | required Uri main, |
554 | required Uri assetsDirectory, |
555 | }) async { |
556 | try { |
557 | await service.streamListen(vm_service.EventStreams.kIsolate); |
558 | } on vm_service.RPCError catch (e) { |
559 | // Do nothing if the tool is already subscribed. |
560 | if (e.code != vm_service.RPCErrorKind.kStreamAlreadySubscribed.code) { |
561 | rethrow; |
562 | } |
563 | } |
564 | |
565 | final Future<void> onRunnable = service.onIsolateEvent.firstWhere((vm_service.Event event) { |
566 | return event.kind == vm_service.EventKind.kIsolateRunnable; |
567 | }); |
568 | await callMethodWrapper( |
569 | kRunInViewMethod, |
570 | args: <String, Object>{ |
571 | 'viewId': viewId, |
572 | 'mainScript': main.toString(), |
573 | 'assetDirectory': assetsDirectory.toString(), |
574 | }, |
575 | ); |
576 | await onRunnable; |
577 | } |
578 | |
579 | Future<String> flutterDebugDumpApp({required String isolateId}) async { |
580 | final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw( |
581 | 'ext.flutter.debugDumpApp', |
582 | isolateId: isolateId, |
583 | ); |
584 | return response?['data']?.toString() ?? ''; |
585 | } |
586 | |
587 | Future<String> flutterDebugDumpRenderTree({required String isolateId}) async { |
588 | final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw( |
589 | 'ext.flutter.debugDumpRenderTree', |
590 | isolateId: isolateId, |
591 | args: <String, Object>{}, |
592 | ); |
593 | return response?['data']?.toString() ?? ''; |
594 | } |
595 | |
596 | Future<String> flutterDebugDumpLayerTree({required String isolateId}) async { |
597 | final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw( |
598 | 'ext.flutter.debugDumpLayerTree', |
599 | isolateId: isolateId, |
600 | ); |
601 | return response?['data']?.toString() ?? ''; |
602 | } |
603 | |
604 | Future<String> flutterDebugDumpFocusTree({required String isolateId}) async { |
605 | final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw( |
606 | 'ext.flutter.debugDumpFocusTree', |
607 | isolateId: isolateId, |
608 | ); |
609 | return response?['data']?.toString() ?? ''; |
610 | } |
611 | |
612 | Future<String> flutterDebugDumpSemanticsTreeInTraversalOrder({required String isolateId}) async { |
613 | final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw( |
614 | 'ext.flutter.debugDumpSemanticsTreeInTraversalOrder', |
615 | isolateId: isolateId, |
616 | ); |
617 | return response?['data']?.toString() ?? ''; |
618 | } |
619 | |
620 | Future<String> flutterDebugDumpSemanticsTreeInInverseHitTestOrder({ |
621 | required String isolateId, |
622 | }) async { |
623 | final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw( |
624 | 'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder', |
625 | isolateId: isolateId, |
626 | ); |
627 | if (response != null) { |
628 | return response['data']?.toString() ?? ''; |
629 | } |
630 | return ''; |
631 | } |
632 | |
633 | Future<Map<String, Object?>?> _flutterToggle(String name, {required String isolateId}) async { |
634 | Map<String, Object?>? state = await invokeFlutterExtensionRpcRaw( |
635 | 'ext.flutter.$name ', |
636 | isolateId: isolateId, |
637 | ); |
638 | if (state != null && state.containsKey('enabled') && state[ 'enabled'] is String) { |
639 | state = await invokeFlutterExtensionRpcRaw( |
640 | 'ext.flutter.$name ', |
641 | isolateId: isolateId, |
642 | args: <String, Object>{'enabled': state[ 'enabled'] == 'true'? 'false': 'true'}, |
643 | ); |
644 | } |
645 | |
646 | return state; |
647 | } |
648 | |
649 | Future<Map<String, Object?>?> flutterToggleDebugPaintSizeEnabled({required String isolateId}) => |
650 | _flutterToggle('debugPaint', isolateId: isolateId); |
651 | |
652 | Future<Map<String, Object?>?> flutterTogglePerformanceOverlayOverride({ |
653 | required String isolateId, |
654 | }) => _flutterToggle('showPerformanceOverlay', isolateId: isolateId); |
655 | |
656 | Future<Map<String, Object?>?> flutterToggleWidgetInspector({required String isolateId}) => |
657 | _flutterToggle('inspector.show', isolateId: isolateId); |
658 | |
659 | Future<Map<String, Object?>?> flutterToggleInvertOversizedImages({required String isolateId}) => |
660 | _flutterToggle('invertOversizedImages', isolateId: isolateId); |
661 | |
662 | Future<Map<String, Object?>?> flutterToggleProfileWidgetBuilds({required String isolateId}) => |
663 | _flutterToggle('profileWidgetBuilds', isolateId: isolateId); |
664 | |
665 | Future<Map<String, Object?>?> flutterDebugAllowBanner(bool show, {required String isolateId}) { |
666 | return invokeFlutterExtensionRpcRaw( |
667 | 'ext.flutter.debugAllowBanner', |
668 | isolateId: isolateId, |
669 | args: <String, Object>{'enabled': show ? 'true': 'false'}, |
670 | ); |
671 | } |
672 | |
673 | Future<Map<String, Object?>?> flutterReassemble({required String? isolateId}) { |
674 | return invokeFlutterExtensionRpcRaw('ext.flutter.reassemble', isolateId: isolateId); |
675 | } |
676 | |
677 | Future<bool> flutterAlreadyPaintedFirstUsefulFrame({required String isolateId}) async { |
678 | final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw( |
679 | 'ext.flutter.didSendFirstFrameRasterizedEvent', |
680 | isolateId: isolateId, |
681 | ); |
682 | // result might be null when the service extension is not initialized |
683 | return result?['enabled'] == 'true'; |
684 | } |
685 | |
686 | Future<Map<String, Object?>?> uiWindowScheduleFrame({required String isolateId}) { |
687 | return invokeFlutterExtensionRpcRaw('ext.ui.window.scheduleFrame', isolateId: isolateId); |
688 | } |
689 | |
690 | Future<Map<String, Object?>?> flutterEvictAsset(String assetPath, {required String isolateId}) { |
691 | return invokeFlutterExtensionRpcRaw( |
692 | 'ext.flutter.evict', |
693 | isolateId: isolateId, |
694 | args: <String, Object?>{'value': assetPath}, |
695 | ); |
696 | } |
697 | |
698 | Future<Map<String, Object?>?> flutterEvictShader(String assetPath, {required String isolateId}) { |
699 | return invokeFlutterExtensionRpcRaw( |
700 | 'ext.ui.window.reinitializeShader', |
701 | isolateId: isolateId, |
702 | args: <String, Object?>{'assetKey': assetPath}, |
703 | ); |
704 | } |
705 | |
706 | /// Exit the application by calling [exit] from `dart:io`. |
707 | /// |
708 | /// This method is only supported by certain embedders. This is |
709 | /// described by [Device.supportsFlutterExit]. |
710 | Future<bool> flutterExit({required String isolateId}) async { |
711 | try { |
712 | final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw( |
713 | 'ext.flutter.exit', |
714 | isolateId: isolateId, |
715 | ); |
716 | // A response of `null` indicates that `invokeFlutterExtensionRpcRaw` caught an RPCError |
717 | // with a missing method code. This can happen when attempting to quit a Flutter app |
718 | // that never registered the methods in the bindings. |
719 | if (result == null) { |
720 | return false; |
721 | } |
722 | } on vm_service.SentinelException { |
723 | // Do nothing on sentinel, the isolate already exited. |
724 | } on vm_service.RPCError { |
725 | // Do nothing on RPCError, the isolate already exited. |
726 | } |
727 | return true; |
728 | } |
729 | |
730 | /// Return the current platform override for the flutter view running with |
731 | /// the main isolate [isolateId]. |
732 | /// |
733 | /// If a non-null value is provided for [platform], the platform override |
734 | /// is updated with this value. |
735 | Future<String> flutterPlatformOverride({String? platform, required String isolateId}) async { |
736 | final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw( |
737 | 'ext.flutter.platformOverride', |
738 | isolateId: isolateId, |
739 | args: platform != null ? <String, Object>{'value': platform} : <String, String>{}, |
740 | ); |
741 | if (result case {'value': final String value}) { |
742 | return value; |
743 | } |
744 | return 'unknown'; |
745 | } |
746 | |
747 | /// Return the current brightness value for the flutter view running with |
748 | /// the main isolate [isolateId]. |
749 | /// |
750 | /// If a non-null value is provided for [brightness], the brightness override |
751 | /// is updated with this value. |
752 | Future<Brightness?> flutterBrightnessOverride({ |
753 | Brightness? brightness, |
754 | required String isolateId, |
755 | }) async { |
756 | final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw( |
757 | 'ext.flutter.brightnessOverride', |
758 | isolateId: isolateId, |
759 | args: |
760 | brightness != null |
761 | ? <String, String>{'value': brightness.toString()} |
762 | : <String, String>{}, |
763 | ); |
764 | if (result != null && result['value'] is String) { |
765 | return result['value'] == 'Brightness.light'? Brightness.light : Brightness.dark; |
766 | } |
767 | return null; |
768 | } |
769 | |
770 | Future<vm_service.Response?> _checkedCallServiceExtension( |
771 | String method, { |
772 | Map<String, Object?>? args, |
773 | }) async { |
774 | try { |
775 | return await service.callServiceExtension(method, args: args); |
776 | } on vm_service.RPCError catch (err) { |
777 | // If an application is not using the framework or the VM service |
778 | // disappears while handling a request, return null. |
779 | if ((err.code == vm_service.RPCErrorKind.kMethodNotFound.code) || |
780 | (err.code == vm_service.RPCErrorKind.kServiceDisappeared.code) || |
781 | (err.code == vm_service.RPCErrorKind.kConnectionDisposed.code) || |
782 | (err.message.contains('Service connection disposed'))) { |
783 | return null; |
784 | } |
785 | rethrow; |
786 | } |
787 | } |
788 | |
789 | /// Invoke a flutter extension method, if the flutter extension is not |
790 | /// available, returns null. |
791 | Future<Map<String, Object?>?> invokeFlutterExtensionRpcRaw( |
792 | String method, { |
793 | required String? isolateId, |
794 | Map<String, Object?>? args, |
795 | }) async { |
796 | final vm_service.Response? response = await _checkedCallServiceExtension( |
797 | method, |
798 | args: <String, Object?>{if (isolateId != null) 'isolateId': isolateId, ...?args}, |
799 | ); |
800 | return response?.json; |
801 | } |
802 | |
803 | /// List all [FlutterView]s attached to the current VM. |
804 | /// |
805 | /// If this returns an empty list, it will poll forever unless [returnEarly] |
806 | /// is set to true. |
807 | /// |
808 | /// By default, the poll duration is 50 milliseconds. |
809 | Future<List<FlutterView>> getFlutterViews({ |
810 | bool returnEarly = false, |
811 | Duration delay = const Duration(milliseconds: 50), |
812 | }) async { |
813 | while (true) { |
814 | final vm_service.Response? response = await callMethodWrapper(kListViewsMethod); |
815 | if (response == null) { |
816 | // The service may have disappeared mid-request. |
817 | // Return an empty list now, and let the shutdown logic elsewhere deal |
818 | // with cleaning up. |
819 | return <FlutterView>[]; |
820 | } |
821 | final List<Object?>? rawViews = response.json?['views'] as List<Object?>?; |
822 | final List<FlutterView> views = <FlutterView>[ |
823 | if (rawViews != null) |
824 | for (final Map<String, Object?> rawView in rawViews.whereType<Map<String, Object?>>()) |
825 | FlutterView.parse(rawView), |
826 | ]; |
827 | if (views.isNotEmpty || returnEarly) { |
828 | return views; |
829 | } |
830 | await Future<void>.delayed(delay); |
831 | } |
832 | } |
833 | |
834 | /// Tell the provided flutter view that the font manifest has been updated |
835 | /// and asset fonts should be reloaded. |
836 | Future<void> reloadAssetFonts({required String isolateId, required String viewId}) async { |
837 | await callMethodWrapper( |
838 | kReloadAssetFonts, |
839 | isolateId: isolateId, |
840 | args: <String, Object?>{'viewId': viewId}, |
841 | ); |
842 | } |
843 | |
844 | /// Waits for a signal from the VM service that [extensionName] is registered. |
845 | /// |
846 | /// Looks at the list of loaded extensions for first Flutter view, as well as |
847 | /// the stream of added extensions to avoid races. |
848 | /// |
849 | /// If [webIsolate] is true, this uses the VM Service isolate list instead of |
850 | /// the `_flutter.listViews` method, which is not implemented by DWDS. |
851 | /// |
852 | /// Throws a [VmServiceDisappearedException] should the VM Service disappear |
853 | /// while making calls to it. |
854 | Future<vm_service.IsolateRef> findExtensionIsolate(String extensionName) async { |
855 | try { |
856 | await service.streamListen(vm_service.EventStreams.kIsolate); |
857 | } on vm_service.RPCError { |
858 | // Do nothing, since the tool is already subscribed. |
859 | } |
860 | |
861 | final Completer<vm_service.IsolateRef> extensionAdded = Completer<vm_service.IsolateRef>(); |
862 | late final StreamSubscription<vm_service.Event> isolateEvents; |
863 | isolateEvents = service.onIsolateEvent.listen((vm_service.Event event) { |
864 | if (event.kind == vm_service.EventKind.kServiceExtensionAdded && |
865 | event.extensionRPC == extensionName) { |
866 | isolateEvents.cancel(); |
867 | extensionAdded.complete(event.isolate!); |
868 | } |
869 | }); |
870 | |
871 | try { |
872 | final List<vm_service.IsolateRef> refs = await _getIsolateRefs(); |
873 | for (final vm_service.IsolateRef ref in refs) { |
874 | final vm_service.Isolate? isolate = await getIsolateOrNull(ref.id!); |
875 | if (isolate != null && (isolate.extensionRPCs?.contains(extensionName) ?? false)) { |
876 | return ref; |
877 | } |
878 | } |
879 | return await extensionAdded.future; |
880 | } on vm_service.RPCError { |
881 | // Translate this exception into something the outer layer understands |
882 | throw VmServiceDisappearedException(); |
883 | } finally { |
884 | await isolateEvents.cancel(); |
885 | } |
886 | } |
887 | |
888 | Future<List<vm_service.IsolateRef>> _getIsolateRefs() async { |
889 | final List<FlutterView> flutterViews = await getFlutterViews(); |
890 | if (flutterViews.isEmpty) { |
891 | throw VmServiceDisappearedException(); |
892 | } |
893 | |
894 | return <vm_service.IsolateRef>[ |
895 | for (final FlutterView flutterView in flutterViews) |
896 | if (flutterView.uiIsolate case final vm_service.IsolateRef uiIsolate) uiIsolate, |
897 | ]; |
898 | } |
899 | |
900 | /// Attempt to retrieve the isolate with id [isolateId], or `null` if it has |
901 | /// been collected. |
902 | Future<vm_service.Isolate?> getIsolateOrNull(String isolateId) async { |
903 | return service |
904 | .getIsolate(isolateId) |
905 | .then<vm_service.Isolate?>( |
906 | (vm_service.Isolate isolate) => isolate, |
907 | onError: (Object? error, StackTrace stackTrace) { |
908 | if (error is vm_service.SentinelException || |
909 | error == null || |
910 | (error is vm_service.RPCError && |
911 | error.code == vm_service.RPCErrorKind.kServiceDisappeared.code)) { |
912 | return null; |
913 | } |
914 | return Future<vm_service.Isolate?>.error(error, stackTrace); |
915 | }, |
916 | ); |
917 | } |
918 | |
919 | /// Attempt to retrieve the isolate pause event with id [isolateId], or `null` if it has |
920 | /// been collected. |
921 | Future<vm_service.Event?> getIsolatePauseEventOrNull(String isolateId) async { |
922 | return service |
923 | .getIsolatePauseEvent(isolateId) |
924 | .then<vm_service.Event?>( |
925 | (vm_service.Event event) => event, |
926 | onError: (Object? error, StackTrace stackTrace) { |
927 | if (error is vm_service.SentinelException || |
928 | error == null || |
929 | (error is vm_service.RPCError && |
930 | error.code == vm_service.RPCErrorKind.kServiceDisappeared.code)) { |
931 | return null; |
932 | } |
933 | return Future<vm_service.Event?>.error(error, stackTrace); |
934 | }, |
935 | ); |
936 | } |
937 | |
938 | /// Create a new development file system on the device. |
939 | Future<vm_service.Response> createDevFS(String fsName) { |
940 | // Call the unchecked version of `callServiceExtension` because the caller |
941 | // has custom handling of certain RPCErrors. |
942 | return service.callServiceExtension('_createDevFS', args: <String, Object?>{ 'fsName': fsName}); |
943 | } |
944 | |
945 | /// Delete an existing file system. |
946 | Future<void> deleteDevFS(String fsName) async { |
947 | await _checkedCallServiceExtension('_deleteDevFS', args: <String, Object?>{ 'fsName': fsName}); |
948 | } |
949 | |
950 | Future<vm_service.Response?> screenshotSkp() { |
951 | return _checkedCallServiceExtension(kScreenshotSkpMethod); |
952 | } |
953 | |
954 | /// Set the VM timeline flags. |
955 | Future<void> setTimelineFlags(List<String> recordedStreams) async { |
956 | await _checkedCallServiceExtension( |
957 | 'setVMTimelineFlags', |
958 | args: <String, Object?>{'recordedStreams': recordedStreams}, |
959 | ); |
960 | } |
961 | |
962 | Future<vm_service.Response?> getTimeline() { |
963 | return _checkedCallServiceExtension('getVMTimeline'); |
964 | } |
965 | |
966 | Future<void> dispose() async { |
967 | await service.dispose(); |
968 | } |
969 | } |
970 | |
971 | /// Thrown when the VM Service disappears while calls are being made to it. |
972 | class VmServiceDisappearedException implements Exception {} |
973 | |
974 | /// Thrown when the frontend service fails to compile an expression due to an |
975 | /// error. |
976 | class VmServiceExpressionCompilationException implements Exception { |
977 | const VmServiceExpressionCompilationException(this.errorMessage); |
978 | |
979 | final String errorMessage; |
980 | } |
981 | |
982 | /// Whether the event attached to an [Isolate.pauseEvent] should be considered |
983 | /// a "pause" event. |
984 | bool isPauseEvent(String kind) { |
985 | return kind == vm_service.EventKind.kPauseStart || |
986 | kind == vm_service.EventKind.kPauseExit || |
987 | kind == vm_service.EventKind.kPauseBreakpoint || |
988 | kind == vm_service.EventKind.kPauseInterrupted || |
989 | kind == vm_service.EventKind.kPauseException || |
990 | kind == vm_service.EventKind.kPausePostRequest || |
991 | kind == vm_service.EventKind.kNone; |
992 | } |
993 | |
994 | /// A brightness enum that matches the values https://github.com/flutter/engine/blob/3a96741247528133c0201ab88500c0c3c036e64e/lib/ui/window.dart#L1328 |
995 | /// Describes the contrast of a theme or color palette. |
996 | enum Brightness { |
997 | /// The color is dark and will require a light text color to achieve readable |
998 | /// contrast. |
999 | /// |
1000 | /// For example, the color might be dark grey, requiring white text. |
1001 | dark, |
1002 | |
1003 | /// The color is light and will require a dark text color to achieve readable |
1004 | /// contrast. |
1005 | /// |
1006 | /// For example, the color might be bright white, requiring black text. |
1007 | light, |
1008 | } |
1009 | |
1010 | /// Process a VM service log event into a string message. |
1011 | String processVmServiceMessage(vm_service.Event event) { |
1012 | final String message = utf8.decode(base64.decode(event.bytes!)); |
1013 | // Remove extra trailing newlines appended by the vm service. |
1014 | if (message.endsWith('\n')) { |
1015 | return message.substring(0, message.length - 1); |
1016 | } |
1017 | return message; |
1018 | } |
1019 |
Definitions
- kResultType
- kResultTypeSuccess
- kError
- kSetAssetBundlePathMethod
- kFlushUIThreadTasksMethod
- kRunInViewMethod
- kListViewsMethod
- kScreenshotSkpMethod
- kReloadAssetFonts
- kFlutterToolAlias
- kReloadSourcesServiceName
- kHotRestartServiceName
- kFlutterVersionServiceName
- kCompileExpressionServiceName
- kFlutterMemoryInfoServiceName
- kIsolateReloadBarred
- _openChannel
- openChannelForTesting
- _defaultOpenChannel
- handleError
- setUpVmService
- connectToVmService
- createVmServiceDelegate
- _connect
- _validateRpcStringParam
- _validateRpcBoolParam
- FlutterView
- FlutterView
- parse
- hasIsolate
- toString
- toJson
- FlutterVmService
- FlutterVmService
- getVmGuarded
- callMethodWrapper
- setAssetDirectory
- flushUIThreadTasks
- runInView
- flutterDebugDumpApp
- flutterDebugDumpRenderTree
- flutterDebugDumpLayerTree
- flutterDebugDumpFocusTree
- flutterDebugDumpSemanticsTreeInTraversalOrder
- flutterDebugDumpSemanticsTreeInInverseHitTestOrder
- _flutterToggle
- flutterToggleDebugPaintSizeEnabled
- flutterTogglePerformanceOverlayOverride
- flutterToggleWidgetInspector
- flutterToggleInvertOversizedImages
- flutterToggleProfileWidgetBuilds
- flutterDebugAllowBanner
- flutterReassemble
- flutterAlreadyPaintedFirstUsefulFrame
- uiWindowScheduleFrame
- flutterEvictAsset
- flutterEvictShader
- flutterExit
- flutterPlatformOverride
- flutterBrightnessOverride
- _checkedCallServiceExtension
- invokeFlutterExtensionRpcRaw
- getFlutterViews
- reloadAssetFonts
- findExtensionIsolate
- _getIsolateRefs
- getIsolateOrNull
- getIsolatePauseEventOrNull
- createDevFS
- deleteDevFS
- screenshotSkp
- setTimelineFlags
- getTimeline
- dispose
- VmServiceDisappearedException
- VmServiceExpressionCompilationException
- VmServiceExpressionCompilationException
- isPauseEvent
- Brightness
Learn more about Flutter for embedded and desktop on industrialflutter.com