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
5import 'dart:async';
6
7import 'package:meta/meta.dart' show visibleForTesting;
8import 'package:vm_service/vm_service.dart' as vm_service;
9
10import 'base/common.dart';
11import 'base/context.dart';
12import 'base/io.dart' as io;
13import 'base/logger.dart';
14import 'base/utils.dart';
15import 'cache.dart';
16import 'convert.dart';
17import 'device.dart';
18import 'globals.dart' as globals;
19import 'project.dart';
20import 'version.dart';
21
22const String kResultType = 'type';
23const String kResultTypeSuccess = 'Success';
24const String kError = 'error';
25
26const String kSetAssetBundlePathMethod = '_flutter.setAssetBundlePath';
27const String kFlushUIThreadTasksMethod = '_flutter.flushUIThreadTasks';
28const String kRunInViewMethod = '_flutter.runInView';
29const String kListViewsMethod = '_flutter.listViews';
30const String kScreenshotSkpMethod = '_flutter.screenshotSkp';
31const String kReloadAssetFonts = '_flutter.reloadAssetFonts';
32
33const String kFlutterToolAlias = 'Flutter Tools';
34
35const String kReloadSourcesServiceName = 'reloadSources';
36const String kHotRestartServiceName = 'hotRestart';
37const String kFlutterVersionServiceName = 'flutterVersion';
38const String kCompileExpressionServiceName = 'compileExpression';
39const String kFlutterMemoryInfoServiceName = 'flutterMemoryInfo';
40
41/// The error response code from an unrecoverable compilation failure.
42const int kIsolateReloadBarred = 1005;
43
44/// Override `WebSocketConnector` in [context] to use a different constructor
45/// for [WebSocket]s (used by tests).
46typedef WebSocketConnector =
47 Future<io.WebSocket> Function(
48 String url, {
49 io.CompressionOptions compression,
50 required Logger logger,
51 });
52
53typedef PrintStructuredErrorLogMethod = void Function(vm_service.Event);
54
55WebSocketConnector _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
61set 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
76typedef ReloadSources = Future<void> Function(String isolateId, {bool force, bool pause});
77
78typedef Restart = Future<void> Function({bool pause});
79
80typedef 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
95Future<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).
159typedef 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.
176Future<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
344Future<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
369Future<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
388Future<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
422String _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
434bool _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.
447class 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.
474class 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.
972class VmServiceDisappearedException implements Exception {}
973
974/// Thrown when the frontend service fails to compile an expression due to an
975/// error.
976class 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.
984bool 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.
996enum 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.
1011String 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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com