| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| /// @docImport 'dart:io'; | |
| library; | |
| import 'dart:async'; | |
| import 'package:meta/meta.dart' show visibleForTesting; | |
| import 'package:vm_service/vm_service.dart' as vm_service; | |
| import 'base/common.dart'; | |
| import 'base/context.dart'; | |
| import 'base/io.dart' as io; | |
| import 'base/logger.dart'; | |
| import 'base/utils.dart'; | |
| import 'cache.dart'; | |
| import 'convert.dart'; | |
| import 'device.dart'; | |
| import 'globals.dart' as globals; | |
| import 'project.dart'; | |
| import 'version.dart'; | |
| const kResultType = 'type'; | |
| const kResultTypeSuccess = 'Success'; | |
| const kError = 'error'; | |
| const kSetAssetBundlePathMethod = '_flutter.setAssetBundlePath'; | |
| const kFlushUIThreadTasksMethod = '_flutter.flushUIThreadTasks'; | |
| const kRunInViewMethod = '_flutter.runInView'; | |
| const kListViewsMethod = '_flutter.listViews'; | |
| const kScreenshotSkpMethod = '_flutter.screenshotSkp'; | |
| const kReloadAssetFonts = '_flutter.reloadAssetFonts'; | |
| const kFlutterToolAlias = 'Flutter Tools'; | |
| const kReloadSourcesServiceName = 'reloadSources'; | |
| const kHotRestartServiceName = 'hotRestart'; | |
| const kFlutterVersionServiceName = 'flutterVersion'; | |
| const kCompileExpressionServiceName = 'compileExpression'; | |
| const kFlutterMemoryInfoServiceName = 'flutterMemoryInfo'; | |
| /// The error response code from an unrecoverable compilation failure. | |
| const kIsolateReloadBarred = 1005; | |
| /// Used to build RegExp instances which can detect the VM service message. | |
| final kVMServiceMessageRegExp = RegExp( | |
| r'The Dart VM service is listening on ((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)', | |
| ); | |
| /// Override `WebSocketConnector` in [context] to use a different constructor | |
| /// for [io.WebSocket]s (used by tests). | |
| typedef WebSocketConnector = Future<io.WebSocket> Function( | |
| String url, { | |
| io.CompressionOptions compression, | |
| required Logger logger, | |
| }); | |
| typedef PrintStructuredErrorLogMethod = void Function(vm_service.Event); | |
| WebSocketConnector _openChannel = _defaultOpenChannel; | |
| /// A testing only override of the WebSocket connector. | |
| /// | |
| /// Provide a `null` value to restore the original connector. | |
| @visibleForTesting | |
| set openChannelForTesting(WebSocketConnector? connector) { | |
| _openChannel = connector ?? _defaultOpenChannel; | |
| } | |
| /// A function that reacts to the invocation of the 'reloadSources' service. | |
| /// | |
| /// The VM Service Protocol allows clients to register custom services that | |
| /// can be invoked by other clients through the service protocol itself. | |
| /// | |
| /// Clients like VmService use external 'reloadSources' services, | |
| /// when available, instead of the VM internal one. This allows these clients to | |
| /// invoke Flutter HotReload when connected to a Flutter Application started in | |
| /// hot mode. | |
| /// | |
| /// See: https://github.com/dart-lang/sdk/issues/30023 | |
| typedef ReloadSources = Future<void> Function(String isolateId, {bool force, bool pause}); | |
| typedef Restart = Future<void> Function({bool pause}); | |
| typedef CompileExpression = Future<String> Function( | |
| String isolateId, | |
| String expression, | |
| List<String> definitions, | |
| List<String> definitionTypes, | |
| List<String> typeDefinitions, | |
| List<String> typeBounds, | |
| List<String> typeDefaults, | |
| String libraryUri, | |
| String? klass, | |
| String? method, | |
| bool isStatic, | |
| ); | |
| Future<io.WebSocket> _defaultOpenChannel( | |
| String url, { | |
| io.CompressionOptions compression = io.CompressionOptions.compressionDefault, | |
| required Logger logger, | |
| }) async { | |
| var delay = const Duration(milliseconds: 100); | |
| var attempts = 0; | |
| io.WebSocket? socket; | |
| Future<void> handleError(Object? e) async { | |
| void Function(String) printVisibleTrace = logger.printTrace; | |
| if (attempts == 10) { | |
| logger.printStatus('Connecting to the VM Service is taking longer than expected...'); | |
| } else if (attempts == 20) { | |
| logger.printStatus('Still attempting to connect to the VM Service...'); | |
| logger.printStatus( | |
| 'If you do NOT see the Flutter application running, it might have ' | |
| 'crashed. The device logs (e.g. from adb or XCode) might have more ' | |
| 'details.', | |
| ); | |
| logger.printStatus( | |
| 'If you do see the Flutter application running on the device, try ' | |
| 're-running with --host-vmservice-port to use a specific port known to ' | |
| 'be available.', | |
| ); | |
| } else if (attempts % 50 == 0) { | |
| printVisibleTrace = logger.printStatus; | |
| } | |
| printVisibleTrace('Exception attempting to connect to the VM Service: $e'); | |
| printVisibleTrace('This was attempt #$attempts. Will retry in $delay.'); | |
| // Delay next attempt. | |
| await Future<void>.delayed(delay); | |
| // Back off exponentially, up to 1600ms per attempt. | |
| if (delay < const Duration(seconds: 1)) { | |
| delay *= 2; | |
| } | |
| } | |
| final WebSocketConnector constructor = | |
| context.get<WebSocketConnector>() ?? | |
| ( | |
| String url, { | |
| io.CompressionOptions compression = io.CompressionOptions.compressionDefault, | |
| Logger? logger, | |
| }) => io.WebSocket.connect(url, compression: compression); | |
| while (socket == null) { | |
| attempts += 1; | |
| try { | |
| socket = await constructor(url, compression: compression, logger: logger); | |
| } on io.IOException catch (e) { | |
| await handleError(e); | |
| } | |
| } | |
| return socket; | |
| } | |
| /// Override `VMServiceConnector` in [context] to return a different | |
| /// [vm_service.VmService] from [connectToVmService] (used by tests). | |
| typedef VMServiceConnector = Future<FlutterVmService> Function( | |
| Uri httpUri, { | |
| ReloadSources? reloadSources, | |
| Restart? restart, | |
| CompileExpression? compileExpression, | |
| FlutterProject? flutterProject, | |
| PrintStructuredErrorLogMethod? printStructuredErrorLogMethod, | |
| io.CompressionOptions compression, | |
| Device? device, | |
| required Logger logger, | |
| }); | |
| /// Set up the VM Service client by attaching services for each of the provided | |
| /// callbacks. | |
| /// | |
| /// All parameters besides [vmService] may be null. | |
| Future<vm_service.VmService> setUpVmService({ | |
| ReloadSources? reloadSources, | |
| Restart? restart, | |
| CompileExpression? compileExpression, | |
| Device? device, | |
| FlutterProject? flutterProject, | |
| PrintStructuredErrorLogMethod? printStructuredErrorLogMethod, | |
| required vm_service.VmService vmService, | |
| }) async { | |
| // Each service registration requires a request to the attached VM service. Since the | |
| // order of these requests does not matter, store each future in a list and await | |
| // all at the end of this method. | |
| final registrationRequests = <Future<vm_service.Success?>>[]; | |
| if (reloadSources != null) { | |
| vmService.registerServiceCallback(kReloadSourcesServiceName, ( | |
| Map<String, Object?> params, | |
| ) async { | |
| final String isolateId = _validateRpcStringParam('reloadSources', params, 'isolateId'); | |
| final bool force = _validateRpcBoolParam('reloadSources', params, 'force'); | |
| final bool pause = _validateRpcBoolParam('reloadSources', params, 'pause'); | |
| await reloadSources(isolateId, force: force, pause: pause); | |
| return <String, Object>{ | |
| 'result': <String, Object>{kResultType: kResultTypeSuccess}, | |
| }; | |
| }); | |
| registrationRequests.add( | |
| vmService.registerService(kReloadSourcesServiceName, kFlutterToolAlias), | |
| ); | |
| } | |
| if (restart != null) { | |
| vmService.registerServiceCallback(kHotRestartServiceName, (Map<String, Object?> params) async { | |
| final bool pause = _validateRpcBoolParam('compileExpression', params, 'pause'); | |
| await restart(pause: pause); | |
| return <String, Object>{ | |
| 'result': <String, Object>{kResultType: kResultTypeSuccess}, | |
| }; | |
| }); | |
| registrationRequests.add(vmService.registerService(kHotRestartServiceName, kFlutterToolAlias)); | |
| } | |
| vmService.registerServiceCallback(kFlutterVersionServiceName, ( | |
| Map<String, Object?> params, | |
| ) async { | |
| final FlutterVersion version = | |
| context.get<FlutterVersion>() ?? | |
| FlutterVersion(fs: globals.fs, flutterRoot: Cache.flutterRoot!, git: globals.git); | |
| final Map<String, Object> versionJson = version.toJson(); | |
| versionJson['frameworkRevisionShort'] = version.frameworkRevisionShort; | |
| versionJson['engineRevisionShort'] = version.engineRevisionShort; | |
| return <String, Object>{ | |
| 'result': <String, Object>{kResultType: kResultTypeSuccess, ...versionJson}, | |
| }; | |
| }); | |
| registrationRequests.add( | |
| vmService.registerService(kFlutterVersionServiceName, kFlutterToolAlias), | |
| ); | |
| if (compileExpression != null) { | |
| vmService.registerServiceCallback(kCompileExpressionServiceName, ( | |
| Map<String, Object?> params, | |
| ) async { | |
| final String isolateId = _validateRpcStringParam('compileExpression', params, 'isolateId'); | |
| final String expression = _validateRpcStringParam('compileExpression', params, 'expression'); | |
| final definitions = List<String>.from(params['definitions']! as List<Object?>); | |
| final definitionTypes = List<String>.from(params['definitionTypes']! as List<Object?>); | |
| final typeDefinitions = List<String>.from(params['typeDefinitions']! as List<Object?>); | |
| final typeBounds = List<String>.from(params['typeBounds']! as List<Object?>); | |
| final typeDefaults = List<String>.from(params['typeDefaults']! as List<Object?>); | |
| final libraryUri = params['libraryUri']! as String; | |
| final klass = params['klass'] as String?; | |
| final method = params['method'] as String?; | |
| final bool isStatic = _validateRpcBoolParam('compileExpression', params, 'isStatic'); | |
| try { | |
| final String kernelBytesBase64 = await compileExpression( | |
| isolateId, | |
| expression, | |
| definitions, | |
| definitionTypes, | |
| typeDefinitions, | |
| typeBounds, | |
| typeDefaults, | |
| libraryUri, | |
| klass, | |
| method, | |
| isStatic, | |
| ); | |
| return <String, Object>{ | |
| kResultType: kResultTypeSuccess, | |
| 'result': <String, String>{'kernelBytes': kernelBytesBase64}, | |
| }; | |
| } on VmServiceExpressionCompilationException catch (e) { | |
| // In most situations, we'd just let VmService catch this exception and | |
| // build the error response. However, in this case we build the error | |
| // response manually and return it to avoid including the stack trace | |
| // from the tool in the response, instead returning the compilation | |
| // error message in the 'details' property of the returned error object. | |
| return <String, Object>{ | |
| kError: vm_service.RPCError.withDetails( | |
| 'compileExpression', | |
| vm_service.RPCErrorKind.kExpressionCompilationError.code, | |
| vm_service.RPCErrorKind.kExpressionCompilationError.message, | |
| details: e.errorMessage, | |
| ).toMap(), | |
| }; | |
| } | |
| }); | |
| registrationRequests.add( | |
| vmService.registerService(kCompileExpressionServiceName, kFlutterToolAlias), | |
| ); | |
| } | |
| if (device != null) { | |
| vmService.registerServiceCallback(kFlutterMemoryInfoServiceName, ( | |
| Map<String, Object?> params, | |
| ) async { | |
| final MemoryInfo result = await device.queryMemoryInfo(); | |
| return <String, Object>{ | |
| 'result': <String, Object>{kResultType: kResultTypeSuccess, ...result.toJson()}, | |
| }; | |
| }); | |
| registrationRequests.add( | |
| vmService.registerService(kFlutterMemoryInfoServiceName, kFlutterToolAlias), | |
| ); | |
| } | |
| if (printStructuredErrorLogMethod != null) { | |
| vmService.onExtensionEvent.listen(printStructuredErrorLogMethod); | |
| registrationRequests.add( | |
| vmService | |
| .streamListen(vm_service.EventStreams.kExtension) | |
| .then<vm_service.Success?>( | |
| (vm_service.Success success) => success, | |
| // It is safe to ignore this error because we expect an error to be | |
| // thrown if we're already subscribed. | |
| onError: (Object error, StackTrace stackTrace) { | |
| if (error is vm_service.RPCError) { | |
| return null; | |
| } | |
| return Future<vm_service.Success?>.error(error, stackTrace); | |
| }, | |
| ), | |
| ); | |
| } | |
| try { | |
| await Future.wait(registrationRequests); | |
| } on vm_service.RPCError catch (e) { | |
| if (e.isConnectionDisposedException) { | |
| rethrow; | |
| } | |
| throwToolExit('Failed to register service methods on attached VM Service: $e'); | |
| } | |
| return vmService; | |
| } | |
| /// Connect to a Dart VM Service at [httpUri]. | |
| /// | |
| /// If the [reloadSources] parameter is not null, the 'reloadSources' service | |
| /// will be registered. The VM Service Protocol allows clients to register | |
| /// custom services that can be invoked by other clients through the service | |
| /// protocol itself. | |
| /// | |
| /// See: https://github.com/dart-lang/sdk/commit/df8bf384eb815cf38450cb50a0f4b62230fba217 | |
| Future<FlutterVmService> connectToVmService( | |
| Uri httpUri, { | |
| ReloadSources? reloadSources, | |
| Restart? restart, | |
| CompileExpression? compileExpression, | |
| FlutterProject? flutterProject, | |
| PrintStructuredErrorLogMethod? printStructuredErrorLogMethod, | |
| io.CompressionOptions compression = io.CompressionOptions.compressionDefault, | |
| Device? device, | |
| required Logger logger, | |
| }) async { | |
| final VMServiceConnector connector = context.get<VMServiceConnector>() ?? _connect; | |
| return connector( | |
| httpUri, | |
| reloadSources: reloadSources, | |
| restart: restart, | |
| compileExpression: compileExpression, | |
| compression: compression, | |
| device: device, | |
| flutterProject: flutterProject, | |
| printStructuredErrorLogMethod: printStructuredErrorLogMethod, | |
| logger: logger, | |
| ); | |
| } | |
| Future<vm_service.VmService> createVmServiceDelegate( | |
| Uri wsUri, { | |
| io.CompressionOptions compression = io.CompressionOptions.compressionDefault, | |
| required Logger logger, | |
| }) async { | |
| final io.WebSocket channel = await _openChannel( | |
| wsUri.toString(), | |
| compression: compression, | |
| logger: logger, | |
| ); | |
| return vm_service.VmService( | |
| channel, | |
| channel.add, | |
| disposeHandler: () async { | |
| await channel.close(); | |
| }, | |
| ); | |
| } | |
| Future<FlutterVmService> _connect( | |
| Uri httpUri, { | |
| ReloadSources? reloadSources, | |
| Restart? restart, | |
| CompileExpression? compileExpression, | |
| FlutterProject? flutterProject, | |
| PrintStructuredErrorLogMethod? printStructuredErrorLogMethod, | |
| io.CompressionOptions compression = io.CompressionOptions.compressionDefault, | |
| Device? device, | |
| required Logger logger, | |
| }) async { | |
| final Uri wsUri = httpUri.replace(scheme: 'ws', path: urlContext.join(httpUri.path, 'ws')); | |
| final vm_service.VmService delegateService = await createVmServiceDelegate( | |
| wsUri, | |
| compression: compression, | |
| logger: logger, | |
| ); | |
| final vm_service.VmService service = await setUpVmService( | |
| reloadSources: reloadSources, | |
| restart: restart, | |
| compileExpression: compileExpression, | |
| device: device, | |
| flutterProject: flutterProject, | |
| printStructuredErrorLogMethod: printStructuredErrorLogMethod, | |
| vmService: delegateService, | |
| ); | |
| // This call is to ensure we are able to establish a connection instead of | |
| // keeping on trucking and failing farther down the process. | |
| await delegateService.getVersion(); | |
| return FlutterVmService(service, httpAddress: httpUri, wsAddress: wsUri); | |
| } | |
| String _validateRpcStringParam(String methodName, Map<String, Object?> params, String paramName) { | |
| final Object? value = params[paramName]; | |
| if (value is! String || value.isEmpty) { | |
| throw vm_service.RPCError( | |
| methodName, | |
| vm_service.RPCErrorKind.kInvalidParams.code, | |
| "Invalid '$paramName': $value", | |
| ); | |
| } | |
| return value; | |
| } | |
| bool _validateRpcBoolParam(String methodName, Map<String, Object?> params, String paramName) { | |
| final Object? value = params[paramName]; | |
| if (value != null && value is! bool) { | |
| throw vm_service.RPCError( | |
| methodName, | |
| vm_service.RPCErrorKind.kInvalidParams.code, | |
| "Invalid '$paramName': $value", | |
| ); | |
| } | |
| return (value as bool?) ?? false; | |
| } | |
| /// Peered to an Android/iOS FlutterView widget on a device. | |
| class FlutterView { | |
| FlutterView({required this.id, required this.uiIsolate}); | |
| factory FlutterView.parse(Map<String, Object?> json) { | |
| final rawIsolate = json['isolate'] as Map<String, Object?>?; | |
| vm_service.IsolateRef? isolate; | |
| if (rawIsolate != null) { | |
| rawIsolate['number'] = rawIsolate['number']?.toString(); | |
| isolate = vm_service.IsolateRef.parse(rawIsolate); | |
| } | |
| return FlutterView(id: json['id']! as String, uiIsolate: isolate); | |
| } | |
| final vm_service.IsolateRef? uiIsolate; | |
| final String id; | |
| bool get hasIsolate => uiIsolate != null; | |
| @override | |
| String toString() => id; | |
| Map<String, Object?> toJson() { | |
| return <String, Object?>{'id': id, 'isolate': uiIsolate?.toJson()}; | |
| } | |
| } | |
| /// Flutter specific VM Service functionality. | |
| class FlutterVmService { | |
| FlutterVmService(this.service, {this.wsAddress, this.httpAddress}); | |
| final vm_service.VmService service; | |
| final Uri? wsAddress; | |
| final Uri? httpAddress; | |
| /// Calls [vm_service.VmService.getVM]. However, in the case that an [vm_service.RPCError] | |
| /// is thrown due to the service being disconnected, the error is discarded | |
| /// and null is returned. | |
| Future<vm_service.VM?> getVmGuarded() async { | |
| try { | |
| return await service.getVM(); | |
| } on vm_service.RPCError catch (err) { | |
| if (err.isConnectionDisposedException) { | |
| globals.printTrace('VmService.getVm call failed: $err'); | |
| return null; | |
| } | |
| rethrow; | |
| } | |
| } | |
| Future<vm_service.Response?> callMethodWrapper( | |
| String method, { | |
| String? isolateId, | |
| Map<String, Object?>? args, | |
| }) async { | |
| try { | |
| return await service.callMethod(method, isolateId: isolateId, args: args); | |
| } on vm_service.RPCError catch (e) { | |
| // If the service disappears mid-request the tool is unable to recover | |
| // and should begin to shutdown due to the service connection closing. | |
| // Swallow the exception here and let the shutdown logic elsewhere deal | |
| // with cleaning up. | |
| if (e.isConnectionDisposedException) { | |
| return null; | |
| } | |
| rethrow; | |
| } | |
| } | |
| /// Set the asset directory for the an attached Flutter view. | |
| Future<void> setAssetDirectory({ | |
| required Uri assetsDirectory, | |
| required String? viewId, | |
| required String? uiIsolateId, | |
| required bool windows, | |
| }) async { | |
| await callMethodWrapper( | |
| kSetAssetBundlePathMethod, | |
| isolateId: uiIsolateId, | |
| args: <String, Object?>{ | |
| 'viewId': viewId, | |
| 'assetDirectory': assetsDirectory.toFilePath(windows: windows), | |
| }, | |
| ); | |
| } | |
| /// Flush all tasks on the UI thread for an attached Flutter view. | |
| /// | |
| /// This method is currently used only for benchmarking. | |
| Future<void> flushUIThreadTasks({required String uiIsolateId}) async { | |
| await callMethodWrapper( | |
| kFlushUIThreadTasksMethod, | |
| args: <String, String>{'isolateId': uiIsolateId}, | |
| ); | |
| } | |
| /// Launch the Dart isolate with entrypoint [main] in the Flutter engine [viewId] | |
| /// with [assetsDirectory] as the devFS. | |
| /// | |
| /// This method is used by the tool to hot restart an already running Flutter | |
| /// engine. | |
| Future<void> runInView({ | |
| required String viewId, | |
| required Uri main, | |
| required Uri assetsDirectory, | |
| }) async { | |
| try { | |
| await service.streamListen(vm_service.EventStreams.kIsolate); | |
| } on vm_service.RPCError catch (e) { | |
| // Do nothing if the tool is already subscribed. | |
| if (e.code != vm_service.RPCErrorKind.kStreamAlreadySubscribed.code) { | |
| rethrow; | |
| } | |
| } | |
| final Future<void> onRunnable = service.onIsolateEvent.firstWhere((vm_service.Event event) { | |
| return event.kind == vm_service.EventKind.kIsolateRunnable; | |
| }); | |
| await callMethodWrapper( | |
| kRunInViewMethod, | |
| args: <String, Object>{ | |
| 'viewId': viewId, | |
| 'mainScript': main.toString(), | |
| 'assetDirectory': assetsDirectory.toString(), | |
| }, | |
| ); | |
| await onRunnable; | |
| } | |
| Future<String> flutterDebugDumpApp({required String isolateId}) async { | |
| final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw( | |
| 'ext.flutter.debugDumpApp', | |
| isolateId: isolateId, | |
| ); | |
| return response?['data']?.toString() ?? ''; | |
| } | |
| Future<String> flutterDebugDumpRenderTree({required String isolateId}) async { | |
| final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw( | |
| 'ext.flutter.debugDumpRenderTree', | |
| isolateId: isolateId, | |
| args: <String, Object>{}, | |
| ); | |
| return response?['data']?.toString() ?? ''; | |
| } | |
| Future<String> flutterDebugDumpLayerTree({required String isolateId}) async { | |
| final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw( | |
| 'ext.flutter.debugDumpLayerTree', | |
| isolateId: isolateId, | |
| ); | |
| return response?['data']?.toString() ?? ''; | |
| } | |
| Future<String> flutterDebugDumpFocusTree({required String isolateId}) async { | |
| final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw( | |
| 'ext.flutter.debugDumpFocusTree', | |
| isolateId: isolateId, | |
| ); | |
| return response?['data']?.toString() ?? ''; | |
| } | |
| Future<String> flutterDebugDumpSemanticsTreeInTraversalOrder({required String isolateId}) async { | |
| final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw( | |
| 'ext.flutter.debugDumpSemanticsTreeInTraversalOrder', | |
| isolateId: isolateId, | |
| ); | |
| return response?['data']?.toString() ?? ''; | |
| } | |
| Future<String> flutterDebugDumpSemanticsTreeInInverseHitTestOrder({ | |
| required String isolateId, | |
| }) async { | |
| final Map<String, Object?>? response = await invokeFlutterExtensionRpcRaw( | |
| 'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder', | |
| isolateId: isolateId, | |
| ); | |
| if (response != null) { | |
| return response['data']?.toString() ?? ''; | |
| } | |
| return ''; | |
| } | |
| Future<Map<String, Object?>?> _flutterToggle(String name, {required String isolateId}) async { | |
| Map<String, Object?>? state = await invokeFlutterExtensionRpcRaw( | |
| 'ext.flutter.$name', | |
| isolateId: isolateId, | |
| ); | |
| if (state != null && state.containsKey('enabled') && state['enabled'] is String) { | |
| state = await invokeFlutterExtensionRpcRaw( | |
| 'ext.flutter.$name', | |
| isolateId: isolateId, | |
| args: <String, Object>{'enabled': state['enabled'] == 'true' ? 'false' : 'true'}, | |
| ); | |
| } | |
| return state; | |
| } | |
| Future<Map<String, Object?>?> flutterToggleDebugPaintSizeEnabled({required String isolateId}) => | |
| _flutterToggle('debugPaint', isolateId: isolateId); | |
| Future<Map<String, Object?>?> flutterTogglePerformanceOverlayOverride({ | |
| required String isolateId, | |
| }) => _flutterToggle('showPerformanceOverlay', isolateId: isolateId); | |
| Future<Map<String, Object?>?> flutterToggleWidgetInspector({required String isolateId}) => | |
| _flutterToggle('inspector.show', isolateId: isolateId); | |
| Future<Map<String, Object?>?> flutterToggleInvertOversizedImages({required String isolateId}) => | |
| _flutterToggle('invertOversizedImages', isolateId: isolateId); | |
| Future<Map<String, Object?>?> flutterToggleProfileWidgetBuilds({required String isolateId}) => | |
| _flutterToggle('profileWidgetBuilds', isolateId: isolateId); | |
| Future<Map<String, Object?>?> flutterDebugAllowBanner(bool show, {required String isolateId}) { | |
| return invokeFlutterExtensionRpcRaw( | |
| 'ext.flutter.debugAllowBanner', | |
| isolateId: isolateId, | |
| args: <String, Object>{'enabled': show ? 'true' : 'false'}, | |
| ); | |
| } | |
| Future<Map<String, Object?>?> flutterReassemble({required String? isolateId}) { | |
| return invokeFlutterExtensionRpcRaw('ext.flutter.reassemble', isolateId: isolateId); | |
| } | |
| Future<bool> flutterAlreadyPaintedFirstUsefulFrame({required String isolateId}) async { | |
| final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw( | |
| 'ext.flutter.didSendFirstFrameRasterizedEvent', | |
| isolateId: isolateId, | |
| ); | |
| // result might be null when the service extension is not initialized | |
| return result?['enabled'] == 'true'; | |
| } | |
| Future<Map<String, Object?>?> uiWindowScheduleFrame({required String isolateId}) { | |
| return invokeFlutterExtensionRpcRaw('ext.ui.window.scheduleFrame', isolateId: isolateId); | |
| } | |
| Future<Map<String, Object?>?> flutterEvictAsset(String assetPath, {required String isolateId}) { | |
| return invokeFlutterExtensionRpcRaw( | |
| 'ext.flutter.evict', | |
| isolateId: isolateId, | |
| args: <String, Object?>{'value': assetPath}, | |
| ); | |
| } | |
| Future<Map<String, Object?>?> flutterEvictShader(String assetPath, {required String isolateId}) { | |
| return invokeFlutterExtensionRpcRaw( | |
| 'ext.ui.window.reinitializeShader', | |
| isolateId: isolateId, | |
| args: <String, Object?>{'assetKey': assetPath}, | |
| ); | |
| } | |
| /// Reload the Flutter GPU shader library compiled from the shader bundle at | |
| /// [assetPath]. | |
| /// | |
| /// Invokes `ext.ui.gpu.reinitializeShaderLibrary`, which the engine registers | |
| /// lazily on the first `ShaderLibrary.fromAsset` in debug mode and no-ops if | |
| /// no library is registered at [assetPath]. Returns null when the extension is | |
| /// not registered (no Flutter GPU shader library has been loaded yet). | |
| Future<Map<String, Object?>?> flutterReinitializeShaderLibrary( | |
| String assetPath, { | |
| required String isolateId, | |
| }) { | |
| return invokeFlutterExtensionRpcRaw( | |
| 'ext.ui.gpu.reinitializeShaderLibrary', | |
| isolateId: isolateId, | |
| args: <String, Object?>{'assetKey': assetPath}, | |
| ); | |
| } | |
| /// Exit the application by calling [exit] from `dart:io`. | |
| /// | |
| /// This method is only supported by certain embedders. This is | |
| /// described by [Device.supportsFlutterExit]. | |
| Future<bool> flutterExit({required String isolateId}) async { | |
| try { | |
| final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw( | |
| 'ext.flutter.exit', | |
| isolateId: isolateId, | |
| ); | |
| // A response of `null` indicates that `invokeFlutterExtensionRpcRaw` caught an RPCError | |
| // with a missing method code. This can happen when attempting to quit a Flutter app | |
| // that never registered the methods in the bindings. | |
| if (result == null) { | |
| return false; | |
| } | |
| } on vm_service.SentinelException { | |
| // Do nothing on sentinel, the isolate already exited. | |
| } on vm_service.RPCError { | |
| // Do nothing on RPCError, the isolate already exited. | |
| } | |
| return true; | |
| } | |
| /// Return the current platform override for the flutter view running with | |
| /// the main isolate [isolateId]. | |
| /// | |
| /// If a non-null value is provided for [platform], the platform override | |
| /// is updated with this value. | |
| Future<String> flutterPlatformOverride({String? platform, required String isolateId}) async { | |
| final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw( | |
| 'ext.flutter.platformOverride', | |
| isolateId: isolateId, | |
| args: platform != null ? <String, Object>{'value': platform} : <String, String>{}, | |
| ); | |
| if (result case {'value': final String value}) { | |
| return value; | |
| } | |
| return 'unknown'; | |
| } | |
| /// Return the current brightness value for the flutter view running with | |
| /// the main isolate [isolateId]. | |
| /// | |
| /// If a non-null value is provided for [brightness], the brightness override | |
| /// is updated with this value. | |
| Future<Brightness?> flutterBrightnessOverride({ | |
| Brightness? brightness, | |
| required String isolateId, | |
| }) async { | |
| final Map<String, Object?>? result = await invokeFlutterExtensionRpcRaw( | |
| 'ext.flutter.brightnessOverride', | |
| isolateId: isolateId, | |
| args: brightness != null | |
| ? <String, String>{'value': brightness.toString()} | |
| : <String, String>{}, | |
| ); | |
| if (result != null && result['value'] is String) { | |
| return result['value'] == 'Brightness.light' ? Brightness.light : Brightness.dark; | |
| } | |
| return null; | |
| } | |
| Future<vm_service.Response?> _checkedCallServiceExtension( | |
| String method, { | |
| Map<String, Object?>? args, | |
| }) async { | |
| try { | |
| return await service.callServiceExtension(method, args: args); | |
| } on vm_service.RPCError catch (err) { | |
| // If an application is not using the framework or the VM service | |
| // disappears while handling a request, return null. | |
| if (err.isServiceExtensionUnregisteredError || err.isConnectionDisposedException) { | |
| return null; | |
| } | |
| rethrow; | |
| } | |
| } | |
| /// Invoke a flutter extension method, if the flutter extension is not | |
| /// available, returns null. | |
| Future<Map<String, Object?>?> invokeFlutterExtensionRpcRaw( | |
| String method, { | |
| required String? isolateId, | |
| Map<String, Object?>? args, | |
| }) async { | |
| final vm_service.Response? response = await _checkedCallServiceExtension( | |
| method, | |
| args: <String, Object?>{'isolateId': ?isolateId, ...?args}, | |
| ); | |
| return response?.json; | |
| } | |
| /// List all [FlutterView]s attached to the current VM. | |
| /// | |
| /// If this returns an empty list, it will poll forever unless [returnEarly] | |
| /// is set to true. | |
| /// | |
| /// By default, the poll duration is 50 milliseconds. | |
| Future<List<FlutterView>> getFlutterViews({ | |
| bool returnEarly = false, | |
| Duration delay = const Duration(milliseconds: 50), | |
| }) async { | |
| while (true) { | |
| final vm_service.Response? response = await callMethodWrapper(kListViewsMethod); | |
| if (response == null) { | |
| // The service may have disappeared mid-request. | |
| // Return an empty list now, and let the shutdown logic elsewhere deal | |
| // with cleaning up. | |
| return <FlutterView>[]; | |
| } | |
| final rawViews = response.json?['views'] as List<Object?>?; | |
| final views = <FlutterView>[ | |
| if (rawViews != null) | |
| for (final Map<String, Object?> rawView in rawViews.whereType<Map<String, Object?>>()) | |
| FlutterView.parse(rawView), | |
| ]; | |
| if (views.isNotEmpty || returnEarly) { | |
| return views; | |
| } | |
| await Future<void>.delayed(delay); | |
| } | |
| } | |
| /// Tell the provided flutter view that the font manifest has been updated | |
| /// and asset fonts should be reloaded. | |
| Future<void> reloadAssetFonts({required String isolateId, required String viewId}) async { | |
| try { | |
| await callMethodWrapper( | |
| kReloadAssetFonts, | |
| isolateId: isolateId, | |
| args: <String, Object?>{'viewId': viewId}, | |
| ); | |
| } on vm_service.RPCError catch (e) { | |
| if (e.code == vm_service.RPCErrorKind.kMethodNotFound.code) { | |
| // Some platforms or embedders (like web) may not implement this VM | |
| // service protocol method. Just ignore the error and return. | |
| return; | |
| } | |
| rethrow; | |
| } | |
| } | |
| /// Waits for a signal from the VM service that [extensionName] is registered. | |
| /// | |
| /// Looks at the list of loaded extensions for first Flutter view, as well as | |
| /// the stream of added extensions to avoid races. | |
| /// | |
| /// Throws a [VmServiceDisappearedException] should the VM Service disappear | |
| /// while making calls to it. | |
| Future<vm_service.IsolateRef> findExtensionIsolate(String extensionName) async { | |
| try { | |
| await service.streamListen(vm_service.EventStreams.kIsolate); | |
| } on vm_service.RPCError { | |
| // Do nothing, since the tool is already subscribed. | |
| } | |
| final extensionAdded = Completer<vm_service.IsolateRef>(); | |
| late final StreamSubscription<vm_service.Event> isolateEvents; | |
| isolateEvents = service.onIsolateEvent.listen((vm_service.Event event) { | |
| if (event.kind == vm_service.EventKind.kServiceExtensionAdded && | |
| event.extensionRPC == extensionName) { | |
| isolateEvents.cancel(); | |
| extensionAdded.complete(event.isolate!); | |
| } | |
| }); | |
| try { | |
| final List<vm_service.IsolateRef> refs = await _getIsolateRefs(); | |
| for (final ref in refs) { | |
| final vm_service.Isolate? isolate = await getIsolateOrNull(ref.id!); | |
| if (isolate != null && (isolate.extensionRPCs?.contains(extensionName) ?? false)) { | |
| return ref; | |
| } | |
| } | |
| return await extensionAdded.future; | |
| } on vm_service.RPCError { | |
| // Translate this exception into something the outer layer understands | |
| throw VmServiceDisappearedException(); | |
| } finally { | |
| await isolateEvents.cancel(); | |
| } | |
| } | |
| Future<List<vm_service.IsolateRef>> _getIsolateRefs() async { | |
| final List<FlutterView> flutterViews = await getFlutterViews(); | |
| if (flutterViews.isEmpty) { | |
| throw VmServiceDisappearedException(); | |
| } | |
| return <vm_service.IsolateRef>[ | |
| for (final FlutterView flutterView in flutterViews) | |
| if (flutterView.uiIsolate case final vm_service.IsolateRef uiIsolate) uiIsolate, | |
| ]; | |
| } | |
| /// Attempt to retrieve the isolate with id [isolateId], or `null` if it has | |
| /// been collected. | |
| Future<vm_service.Isolate?> getIsolateOrNull(String isolateId) async { | |
| return service | |
| .getIsolate(isolateId) | |
| .then<vm_service.Isolate?>( | |
| (vm_service.Isolate isolate) => isolate, | |
| onError: (Object? error, StackTrace stackTrace) { | |
| if (error is vm_service.SentinelException || | |
| error == null || | |
| (error is vm_service.RPCError && | |
| error.code == vm_service.RPCErrorKind.kServiceDisappeared.code)) { | |
| return null; | |
| } | |
| return Future<vm_service.Isolate?>.error(error, stackTrace); | |
| }, | |
| ); | |
| } | |
| /// Attempt to retrieve the isolate pause event with id [isolateId], or `null` if it has | |
| /// been collected. | |
| Future<vm_service.Event?> getIsolatePauseEventOrNull(String isolateId) async { | |
| return service | |
| .getIsolatePauseEvent(isolateId) | |
| .then<vm_service.Event?>( | |
| (vm_service.Event event) => event, | |
| onError: (Object? error, StackTrace stackTrace) { | |
| if (error is vm_service.SentinelException || | |
| error == null || | |
| (error is vm_service.RPCError && | |
| error.code == vm_service.RPCErrorKind.kServiceDisappeared.code)) { | |
| return null; | |
| } | |
| return Future<vm_service.Event?>.error(error, stackTrace); | |
| }, | |
| ); | |
| } | |
| /// Create a new development file system on the device. | |
| Future<vm_service.Response> createDevFS(String fsName) { | |
| // Call the unchecked version of `callServiceExtension` because the caller | |
| // has custom handling of certain RPCErrors. | |
| return service.callServiceExtension('_createDevFS', args: <String, Object?>{'fsName': fsName}); | |
| } | |
| /// Delete an existing file system. | |
| Future<void> deleteDevFS(String fsName) async { | |
| await _checkedCallServiceExtension('_deleteDevFS', args: <String, Object?>{'fsName': fsName}); | |
| } | |
| Future<vm_service.Response?> screenshotSkp() { | |
| return _checkedCallServiceExtension(kScreenshotSkpMethod); | |
| } | |
| /// Set the VM timeline flags. | |
| Future<void> setTimelineFlags(List<String> recordedStreams) async { | |
| await _checkedCallServiceExtension( | |
| 'setVMTimelineFlags', | |
| args: <String, Object?>{'recordedStreams': recordedStreams}, | |
| ); | |
| } | |
| Future<vm_service.Response?> getTimeline() { | |
| return _checkedCallServiceExtension('getVMTimeline'); | |
| } | |
| Future<void> dispose() async { | |
| await service.dispose(); | |
| } | |
| } | |
| /// Thrown when the VM Service disappears while calls are being made to it. | |
| class VmServiceDisappearedException implements Exception {} | |
| /// Thrown when the frontend service fails to compile an expression due to an | |
| /// error. | |
| class VmServiceExpressionCompilationException implements Exception { | |
| const VmServiceExpressionCompilationException(this.errorMessage); | |
| final String errorMessage; | |
| } | |
| /// Whether the event attached to an [vm_service.Isolate.pauseEvent] should be | |
| /// considered a "pause" event. | |
| bool isPauseEvent(String kind) { | |
| return kind == vm_service.EventKind.kPauseStart || | |
| kind == vm_service.EventKind.kPauseExit || | |
| kind == vm_service.EventKind.kPauseBreakpoint || | |
| kind == vm_service.EventKind.kPauseInterrupted || | |
| kind == vm_service.EventKind.kPauseException || | |
| kind == vm_service.EventKind.kPausePostRequest || | |
| kind == vm_service.EventKind.kNone; | |
| } | |
| /// A brightness enum that matches the values defined in | |
| /// https://github.com/flutter/flutter/blob/230240c56880f2c19bf92d2c32203b064054f173/engine/src/flutter/lib/ui/window.dart#L1073 | |
| /// Describes the contrast of a theme or color palette. | |
| enum Brightness { | |
| /// The color is dark and will require a light text color to achieve readable | |
| /// contrast. | |
| /// | |
| /// For example, the color might be dark grey, requiring white text. | |
| dark, | |
| /// The color is light and will require a dark text color to achieve readable | |
| /// contrast. | |
| /// | |
| /// For example, the color might be bright white, requiring black text. | |
| light, | |
| } | |
| /// Process a VM service log event into a string message. | |
| /// | |
| /// Uses a permissive UTF-8 decoder because app-generated logs may contain | |
| /// invalid UTF-8 from external sources (Bluetooth devices, network APIs, etc.). | |
| String processVmServiceMessage(vm_service.Event event) { | |
| // Use permissive decoder for app logs that may have invalid UTF-8 | |
| final String message = utf8AllowMalformed.decode(base64.decode(event.bytes!)); | |
| // Remove extra trailing newlines appended by the vm service. | |
| if (message.endsWith('\n')) { | |
| return message.substring(0, message.length - 1); | |
| } | |
| return message; | |
| } | |
| extension RPCErrorExtension on vm_service.RPCError { | |
| bool get isConnectionDisposedException => | |
| code == vm_service.RPCErrorKind.kServiceDisappeared.code || | |
| code == vm_service.RPCErrorKind.kConnectionDisposed.code || | |
| message.contains('Service connection disposed'); | |
| /// DWDS throws an internal error (-32603) when a service extension is called | |
| /// but has not been registered yet, due to a null-assertion on the looked up method in JS. | |
| /// On native platforms, this throws `kMethodNotFound` (-32601). | |
| // TODO(kevmoo): Remove this work-around once https://github.com/dart-lang/sdk/issues/63424 is fixed. | |
| bool get isServiceExtensionUnregisteredError => | |
| code == vm_service.RPCErrorKind.kMethodNotFound.code || | |
| (code == vm_service.RPCErrorKind.kInternalError.code && | |
| message.contains('Unexpected null value')); | |
| } |