// 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. 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 String kResultType = 'type'; const String kResultTypeSuccess = 'Success'; const String kGetSkSLsMethod = '_flutter.getSkSLs'; const String kSetAssetBundlePathMethod = '_flutter.setAssetBundlePath'; const String kFlushUIThreadTasksMethod = '_flutter.flushUIThreadTasks'; const String kRunInViewMethod = '_flutter.runInView'; const String kListViewsMethod = '_flutter.listViews'; const String kScreenshotSkpMethod = '_flutter.screenshotSkp'; const String kRenderFrameWithRasterStatsMethod = '_flutter.renderFrameWithRasterStats'; const String kReloadAssetFonts = '_flutter.reloadAssetFonts'; const String kFlutterToolAlias = 'Flutter Tools'; const String kReloadSourcesServiceName = 'reloadSources'; const String kHotRestartServiceName = 'hotRestart'; const String kFlutterVersionServiceName = 'flutterVersion'; const String kCompileExpressionServiceName = 'compileExpression'; const String kFlutterMemoryInfoServiceName = 'flutterMemoryInfo'; const String kFlutterGetSkSLServiceName = 'flutterGetSkSL'; /// The error response code from an unrecoverable compilation failure. const int kIsolateReloadBarred = 1005; /// Override `WebSocketConnector` in [context] to use a different constructor /// for [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; } /// The error codes for the JSON-RPC standard, including VM service specific /// error codes. /// /// See also: https://www.jsonrpc.org/specification#error_object abstract class RPCErrorCodes { /// The method does not exist or is not available. static const int kMethodNotFound = -32601; /// Invalid method parameter(s), such as a mismatched type. static const int kInvalidParams = -32602; /// Internal JSON-RPC error. static const int kInternalError = -32603; /// Application specific error codes. static const int kServerError = -32000; /// Non-standard JSON-RPC error codes: /// The VM service or extension service has disappeared. static const int kServiceDisappeared = 112; } /// 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, ); /// A method that pulls an SkSL shader from the device and writes it to a file. /// /// The name of the file returned as a result. typedef GetSkSLMethod = Future<String?> Function(); Future<io.WebSocket> _defaultOpenChannel(String url, { io.CompressionOptions compression = io.CompressionOptions.compressionDefault, required Logger logger, }) async { Duration delay = const Duration(milliseconds: 100); int 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.WebSocketException catch (e) { await handleError(e); } on io.SocketException catch (e) { await handleError(e); } } return socket; } /// Override `VMServiceConnector` in [context] to return a different VMService /// from [VMService.connect] (used by tests). typedef VMServiceConnector = Future<FlutterVmService> Function(Uri httpUri, { ReloadSources? reloadSources, Restart? restart, CompileExpression? compileExpression, GetSkSLMethod? getSkSLMethod, 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, GetSkSLMethod? skSLMethod, 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 List<Future<vm_service.Success?>> 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!, ); 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 List<String> definitions = List<String>.from(params['definitions']! as List<Object?>); final List<String> definitionTypes = List<String>.from(params['definitionTypes']! as List<Object?>); final List<String> typeDefinitions = List<String>.from(params['typeDefinitions']! as List<Object?>); final List<String> typeBounds = List<String>.from(params['typeBounds']! as List<Object?>); final List<String> typeDefaults = List<String>.from(params['typeDefaults']! as List<Object?>); final String libraryUri = params['libraryUri']! as String; final String? klass = params['klass'] as String?; final String? method = params['method'] as String?; final bool isStatic = _validateRpcBoolParam('compileExpression', params, 'isStatic'); 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}, }; }); 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 (skSLMethod != null) { vmService.registerServiceCallback(kFlutterGetSkSLServiceName, (Map<String, Object?> params) async { final String? filename = await skSLMethod(); if (filename == null) { return <String, Object>{ 'result': <String, Object>{ kResultType: kResultTypeSuccess, }, }; } return <String, Object>{ 'result': <String, Object>{ kResultType: kResultTypeSuccess, 'filename': filename, }, }; }); registrationRequests.add(vmService.registerService(kFlutterGetSkSLServiceName, 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) { 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, GetSkSLMethod? getSkSLMethod, 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, getSkSLMethod: getSkSLMethod, 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, GetSkSLMethod? getSkSLMethod, 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, skSLMethod: getSkSLMethod, 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, RPCErrorCodes.kInvalidParams, "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, RPCErrorCodes.kInvalidParams, "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 Map<String, Object?>? 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; 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.code == RPCErrorCodes.kServiceDisappeared) { 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), }); } /// Retrieve the cached SkSL shaders from an attached Flutter view. /// /// This method will only return data if `--cache-sksl` was provided as a /// flutter run argument, and only then on physical devices. Future<Map<String, Object?>?> getSkSLs({ required String viewId, }) async { final vm_service.Response? response = await callMethodWrapper( kGetSkSLsMethod, args: <String, String>{ 'viewId': viewId, }, ); if (response == null) { return null; } return response.json?['SkSLs'] as Map<String, Object?>?; } /// 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 { // Do nothing, since the tool is already subscribed. } 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; } /// Renders the last frame with additional raster tracing enabled. /// /// When a frame is rendered using this method it will incur additional cost /// for rasterization which is not reflective of how long the frame takes in /// production. This is primarily intended to be used to identify the layers /// that result in the most raster perf degradation. Future<Map<String, Object?>?> renderFrameWithRasterStats({ required String? viewId, required String? uiIsolateId, }) async { final vm_service.Response? response = await callMethodWrapper( kRenderFrameWithRasterStatsMethod, isolateId: uiIsolateId, args: <String, String?>{ 'viewId': viewId, }, ); return response?.json; } 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, }, ); } Future<Map<String, Object?>?> flutterEvictScene(String assetPath, { required String isolateId, }) { return invokeFlutterExtensionRpcRaw( 'ext.ui.window.reinitializeScene', 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 != null && result['value'] is String) { return result['value']! as String; } 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.code == RPCErrorCodes.kMethodNotFound) || (err.code == RPCErrorCodes.kServiceDisappeared)) { 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 List<Object?>? rawViews = response.json?['views'] as List<Object?>?; final List<FlutterView> 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 { await callMethodWrapper( kReloadAssetFonts, isolateId: isolateId, args: <String, Object?>{ 'viewId': viewId, }, ); } /// 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. /// /// If [webIsolate] is true, this uses the VM Service isolate list instead of /// the `_flutter.listViews` method, which is not implemented by DWDS. /// /// 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 Completer<vm_service.IsolateRef> 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 vm_service.IsolateRef 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; } finally { await isolateEvents.cancel(); try { await service.streamCancel(vm_service.EventStreams.kIsolate); } on vm_service.RPCError { // It's ok for cleanup to fail, such as when the service disappears. } } } Future<List<vm_service.IsolateRef>> _getIsolateRefs() async { final List<FlutterView> flutterViews = await getFlutterViews(); if (flutterViews.isEmpty) { throw VmServiceDisappearedException(); } final List<vm_service.IsolateRef> refs = <vm_service.IsolateRef>[]; for (final FlutterView flutterView in flutterViews) { final vm_service.IsolateRef? uiIsolate = flutterView.uiIsolate; if (uiIsolate != null) { refs.add(uiIsolate); } } return refs; } /// 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 == RPCErrorCodes.kServiceDisappeared)) { 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 == RPCErrorCodes.kServiceDisappeared)) { 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 { } /// Whether the event attached to an [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 https://github.com/flutter/engine/blob/3a96741247528133c0201ab88500c0c3c036e64e/lib/ui/window.dart#L1328 /// 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. String processVmServiceMessage(vm_service.Event event) { final String message = utf8.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; }