// 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; /// Override `WebSocketConnector` in [context] to use a different constructor /// for [io.WebSocket]s (used by tests). typedef WebSocketConnector = Future 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 Function(String isolateId, {bool force, bool pause}); typedef Restart = Future Function({bool pause}); typedef CompileExpression = Future Function( String isolateId, String expression, List definitions, List definitionTypes, List typeDefinitions, List typeBounds, List typeDefaults, String libraryUri, String? klass, String? method, bool isStatic, ); Future _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 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.delayed(delay); // Back off exponentially, up to 1600ms per attempt. if (delay < const Duration(seconds: 1)) { delay *= 2; } } final WebSocketConnector constructor = context.get() ?? ( 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 /// [vm_service.VmService] from [connectToVmService] (used by tests). typedef VMServiceConnector = Future 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 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 = >[]; if (reloadSources != null) { vmService.registerServiceCallback(kReloadSourcesServiceName, ( Map 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 { 'result': {kResultType: kResultTypeSuccess}, }; }); registrationRequests.add( vmService.registerService(kReloadSourcesServiceName, kFlutterToolAlias), ); } if (restart != null) { vmService.registerServiceCallback(kHotRestartServiceName, (Map params) async { final bool pause = _validateRpcBoolParam('compileExpression', params, 'pause'); await restart(pause: pause); return { 'result': {kResultType: kResultTypeSuccess}, }; }); registrationRequests.add(vmService.registerService(kHotRestartServiceName, kFlutterToolAlias)); } vmService.registerServiceCallback(kFlutterVersionServiceName, ( Map params, ) async { final FlutterVersion version = context.get() ?? FlutterVersion(fs: globals.fs, flutterRoot: Cache.flutterRoot!, git: globals.git); final Map versionJson = version.toJson(); versionJson['frameworkRevisionShort'] = version.frameworkRevisionShort; versionJson['engineRevisionShort'] = version.engineRevisionShort; return { 'result': {kResultType: kResultTypeSuccess, ...versionJson}, }; }); registrationRequests.add( vmService.registerService(kFlutterVersionServiceName, kFlutterToolAlias), ); if (compileExpression != null) { vmService.registerServiceCallback(kCompileExpressionServiceName, ( Map params, ) async { final String isolateId = _validateRpcStringParam('compileExpression', params, 'isolateId'); final String expression = _validateRpcStringParam('compileExpression', params, 'expression'); final definitions = List.from(params['definitions']! as List); final definitionTypes = List.from(params['definitionTypes']! as List); final typeDefinitions = List.from(params['typeDefinitions']! as List); final typeBounds = List.from(params['typeBounds']! as List); final typeDefaults = List.from(params['typeDefaults']! as List); 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 { kResultType: kResultTypeSuccess, 'result': {'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 { 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 params, ) async { final MemoryInfo result = await device.queryMemoryInfo(); return { 'result': {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 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.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 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() ?? _connect; return connector( httpUri, reloadSources: reloadSources, restart: restart, compileExpression: compileExpression, compression: compression, device: device, flutterProject: flutterProject, printStructuredErrorLogMethod: printStructuredErrorLogMethod, logger: logger, ); } Future 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 _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 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 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 json) { final rawIsolate = json['isolate'] as Map?; 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 toJson() { return {'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 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 callMethodWrapper( String method, { String? isolateId, Map? 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 setAssetDirectory({ required Uri assetsDirectory, required String? viewId, required String? uiIsolateId, required bool windows, }) async { await callMethodWrapper( kSetAssetBundlePathMethod, isolateId: uiIsolateId, args: { '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 flushUIThreadTasks({required String uiIsolateId}) async { await callMethodWrapper( kFlushUIThreadTasksMethod, args: {'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 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 onRunnable = service.onIsolateEvent.firstWhere((vm_service.Event event) { return event.kind == vm_service.EventKind.kIsolateRunnable; }); await callMethodWrapper( kRunInViewMethod, args: { 'viewId': viewId, 'mainScript': main.toString(), 'assetDirectory': assetsDirectory.toString(), }, ); await onRunnable; } Future flutterDebugDumpApp({required String isolateId}) async { final Map? response = await invokeFlutterExtensionRpcRaw( 'ext.flutter.debugDumpApp', isolateId: isolateId, ); return response?['data']?.toString() ?? ''; } Future flutterDebugDumpRenderTree({required String isolateId}) async { final Map? response = await invokeFlutterExtensionRpcRaw( 'ext.flutter.debugDumpRenderTree', isolateId: isolateId, args: {}, ); return response?['data']?.toString() ?? ''; } Future flutterDebugDumpLayerTree({required String isolateId}) async { final Map? response = await invokeFlutterExtensionRpcRaw( 'ext.flutter.debugDumpLayerTree', isolateId: isolateId, ); return response?['data']?.toString() ?? ''; } Future flutterDebugDumpFocusTree({required String isolateId}) async { final Map? response = await invokeFlutterExtensionRpcRaw( 'ext.flutter.debugDumpFocusTree', isolateId: isolateId, ); return response?['data']?.toString() ?? ''; } Future flutterDebugDumpSemanticsTreeInTraversalOrder({required String isolateId}) async { final Map? response = await invokeFlutterExtensionRpcRaw( 'ext.flutter.debugDumpSemanticsTreeInTraversalOrder', isolateId: isolateId, ); return response?['data']?.toString() ?? ''; } Future flutterDebugDumpSemanticsTreeInInverseHitTestOrder({ required String isolateId, }) async { final Map? response = await invokeFlutterExtensionRpcRaw( 'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder', isolateId: isolateId, ); if (response != null) { return response['data']?.toString() ?? ''; } return ''; } Future?> _flutterToggle(String name, {required String isolateId}) async { Map? 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: {'enabled': state['enabled'] == 'true' ? 'false' : 'true'}, ); } return state; } Future?> flutterToggleDebugPaintSizeEnabled({required String isolateId}) => _flutterToggle('debugPaint', isolateId: isolateId); Future?> flutterTogglePerformanceOverlayOverride({ required String isolateId, }) => _flutterToggle('showPerformanceOverlay', isolateId: isolateId); Future?> flutterToggleWidgetInspector({required String isolateId}) => _flutterToggle('inspector.show', isolateId: isolateId); Future?> flutterToggleInvertOversizedImages({required String isolateId}) => _flutterToggle('invertOversizedImages', isolateId: isolateId); Future?> flutterToggleProfileWidgetBuilds({required String isolateId}) => _flutterToggle('profileWidgetBuilds', isolateId: isolateId); Future?> flutterDebugAllowBanner(bool show, {required String isolateId}) { return invokeFlutterExtensionRpcRaw( 'ext.flutter.debugAllowBanner', isolateId: isolateId, args: {'enabled': show ? 'true' : 'false'}, ); } Future?> flutterReassemble({required String? isolateId}) { return invokeFlutterExtensionRpcRaw('ext.flutter.reassemble', isolateId: isolateId); } Future flutterAlreadyPaintedFirstUsefulFrame({required String isolateId}) async { final Map? result = await invokeFlutterExtensionRpcRaw( 'ext.flutter.didSendFirstFrameRasterizedEvent', isolateId: isolateId, ); // result might be null when the service extension is not initialized return result?['enabled'] == 'true'; } Future?> uiWindowScheduleFrame({required String isolateId}) { return invokeFlutterExtensionRpcRaw('ext.ui.window.scheduleFrame', isolateId: isolateId); } Future?> flutterEvictAsset(String assetPath, {required String isolateId}) { return invokeFlutterExtensionRpcRaw( 'ext.flutter.evict', isolateId: isolateId, args: {'value': assetPath}, ); } Future?> flutterEvictShader(String assetPath, {required String isolateId}) { return invokeFlutterExtensionRpcRaw( 'ext.ui.window.reinitializeShader', isolateId: isolateId, args: {'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 flutterExit({required String isolateId}) async { try { final Map? 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 flutterPlatformOverride({String? platform, required String isolateId}) async { final Map? result = await invokeFlutterExtensionRpcRaw( 'ext.flutter.platformOverride', isolateId: isolateId, args: platform != null ? {'value': platform} : {}, ); 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 flutterBrightnessOverride({ Brightness? brightness, required String isolateId, }) async { final Map? result = await invokeFlutterExtensionRpcRaw( 'ext.flutter.brightnessOverride', isolateId: isolateId, args: brightness != null ? {'value': brightness.toString()} : {}, ); if (result != null && result['value'] is String) { return result['value'] == 'Brightness.light' ? Brightness.light : Brightness.dark; } return null; } Future _checkedCallServiceExtension( String method, { Map? 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 == vm_service.RPCErrorKind.kMethodNotFound.code || err.isConnectionDisposedException) { return null; } rethrow; } } /// Invoke a flutter extension method, if the flutter extension is not /// available, returns null. Future?> invokeFlutterExtensionRpcRaw( String method, { required String? isolateId, Map? args, }) async { final vm_service.Response? response = await _checkedCallServiceExtension( method, args: {'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> 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 []; } final rawViews = response.json?['views'] as List?; final views = [ if (rawViews != null) for (final Map rawView in rawViews.whereType>()) FlutterView.parse(rawView), ]; if (views.isNotEmpty || returnEarly) { return views; } await Future.delayed(delay); } } /// Tell the provided flutter view that the font manifest has been updated /// and asset fonts should be reloaded. Future reloadAssetFonts({required String isolateId, required String viewId}) async { await callMethodWrapper( kReloadAssetFonts, isolateId: isolateId, args: {'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. /// /// Throws a [VmServiceDisappearedException] should the VM Service disappear /// while making calls to it. Future 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(); late final StreamSubscription 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 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> _getIsolateRefs() async { final List flutterViews = await getFlutterViews(); if (flutterViews.isEmpty) { throw VmServiceDisappearedException(); } return [ 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 getIsolateOrNull(String isolateId) async { return service .getIsolate(isolateId) .then( (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.error(error, stackTrace); }, ); } /// Attempt to retrieve the isolate pause event with id [isolateId], or `null` if it has /// been collected. Future getIsolatePauseEventOrNull(String isolateId) async { return service .getIsolatePauseEvent(isolateId) .then( (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.error(error, stackTrace); }, ); } /// Create a new development file system on the device. Future createDevFS(String fsName) { // Call the unchecked version of `callServiceExtension` because the caller // has custom handling of certain RPCErrors. return service.callServiceExtension('_createDevFS', args: {'fsName': fsName}); } /// Delete an existing file system. Future deleteDevFS(String fsName) async { await _checkedCallServiceExtension('_deleteDevFS', args: {'fsName': fsName}); } Future screenshotSkp() { return _checkedCallServiceExtension(kScreenshotSkpMethod); } /// Set the VM timeline flags. Future setTimelineFlags(List recordedStreams) async { await _checkedCallServiceExtension( 'setVMTimelineFlags', args: {'recordedStreams': recordedStreams}, ); } Future getTimeline() { return _checkedCallServiceExtension('getVMTimeline'); } Future 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 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; } 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'); }