// 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:dds/dds.dart' as dds; import 'package:meta/meta.dart'; import 'package:package_config/package_config.dart'; import 'package:vm_service/vm_service.dart' as vm_service; import 'application_package.dart'; import 'artifacts.dart'; import 'asset.dart'; import 'base/command_help.dart'; import 'base/common.dart'; import 'base/context.dart'; import 'base/file_system.dart'; import 'base/io.dart' as io; import 'base/logger.dart'; import 'base/platform.dart'; import 'base/signals.dart'; import 'base/terminal.dart'; import 'base/utils.dart'; import 'build_info.dart'; import 'build_system/build_system.dart'; import 'build_system/targets/dart_plugin_registrant.dart'; import 'build_system/targets/localizations.dart'; import 'build_system/targets/scene_importer.dart'; import 'build_system/targets/shader_compiler.dart'; import 'bundle.dart'; import 'cache.dart'; import 'compile.dart'; import 'convert.dart'; import 'devfs.dart'; import 'device.dart'; import 'features.dart'; import 'globals.dart' as globals; import 'project.dart'; import 'resident_devtools_handler.dart'; import 'run_cold.dart'; import 'run_hot.dart'; import 'sksl_writer.dart'; import 'vmservice.dart'; class FlutterDevice { FlutterDevice( this.device, { required this.buildInfo, TargetModel targetModel = TargetModel.flutter, this.targetPlatform, ResidentCompiler? generator, this.userIdentifier, required this.developmentShaderCompiler, this.developmentSceneImporter, }) : generator = generator ?? ResidentCompiler( globals.artifacts!.getArtifactPath( Artifact.flutterPatchedSdkPath, platform: targetPlatform, mode: buildInfo.mode, ), buildMode: buildInfo.mode, trackWidgetCreation: buildInfo.trackWidgetCreation, fileSystemRoots: buildInfo.fileSystemRoots, fileSystemScheme: buildInfo.fileSystemScheme, targetModel: targetModel, dartDefines: buildInfo.dartDefines, packagesPath: buildInfo.packagesPath, extraFrontEndOptions: buildInfo.extraFrontEndOptions, artifacts: globals.artifacts!, processManager: globals.processManager, logger: globals.logger, platform: globals.platform, fileSystem: globals.fs, ); /// Create a [FlutterDevice] with optional code generation enabled. static Future<FlutterDevice> create( Device device, { required String? target, required BuildInfo buildInfo, required Platform platform, TargetModel targetModel = TargetModel.flutter, List<String>? experimentalFlags, String? userIdentifier, }) async { final TargetPlatform targetPlatform = await device.targetPlatform; if (device.platformType == PlatformType.fuchsia) { targetModel = TargetModel.flutterRunner; } final DevelopmentShaderCompiler shaderCompiler = DevelopmentShaderCompiler( shaderCompiler: ShaderCompiler( artifacts: globals.artifacts!, logger: globals.logger, processManager: globals.processManager, fileSystem: globals.fs, ), fileSystem: globals.fs, ); final DevelopmentSceneImporter sceneImporter = DevelopmentSceneImporter( sceneImporter: SceneImporter( artifacts: globals.artifacts!, logger: globals.logger, processManager: globals.processManager, fileSystem: globals.fs, ), fileSystem: globals.fs, ); final ResidentCompiler generator; // For both web and non-web platforms we initialize dill to/from // a shared location for faster bootstrapping. If the compiler fails // due to a kernel target or version mismatch, no error is reported // and the compiler starts up as normal. Unexpected errors will print // a warning message and dump some debug information which can be // used to file a bug, but the compiler will still start up correctly. if (targetPlatform == TargetPlatform.web_javascript) { // TODO(zanderso): consistently provide these flags across platforms. final String platformDillName; final List<String> extraFrontEndOptions = List<String>.of(buildInfo.extraFrontEndOptions); if (buildInfo.nullSafetyMode == NullSafetyMode.unsound) { platformDillName = 'ddc_outline.dill'; if (!extraFrontEndOptions.contains('--no-sound-null-safety')) { extraFrontEndOptions.add('--no-sound-null-safety'); } } else if (buildInfo.nullSafetyMode == NullSafetyMode.sound) { platformDillName = 'ddc_outline_sound.dill'; if (!extraFrontEndOptions.contains('--sound-null-safety')) { extraFrontEndOptions.add('--sound-null-safety'); } } else { throw StateError('Expected buildInfo.nullSafetyMode to be one of unsound or sound, got ${buildInfo.nullSafetyMode}'); } final String platformDillPath = globals.fs.path.join( getWebPlatformBinariesDirectory(globals.artifacts!, buildInfo.webRenderer).path, platformDillName, ); generator = ResidentCompiler( globals.artifacts!.getHostArtifact(HostArtifact.flutterWebSdk).path, buildMode: buildInfo.mode, trackWidgetCreation: buildInfo.trackWidgetCreation, fileSystemRoots: buildInfo.fileSystemRoots, // Override the filesystem scheme so that the frontend_server can find // the generated entrypoint code. fileSystemScheme: 'org-dartlang-app', initializeFromDill: buildInfo.initializeFromDill ?? getDefaultCachedKernelPath( trackWidgetCreation: buildInfo.trackWidgetCreation, dartDefines: buildInfo.dartDefines, extraFrontEndOptions: extraFrontEndOptions, ), assumeInitializeFromDillUpToDate: buildInfo.assumeInitializeFromDillUpToDate, targetModel: TargetModel.dartdevc, extraFrontEndOptions: extraFrontEndOptions, platformDill: globals.fs.file(platformDillPath).absolute.uri.toString(), dartDefines: buildInfo.dartDefines, librariesSpec: globals.fs.file(globals.artifacts! .getHostArtifact(HostArtifact.flutterWebLibrariesJson)).uri.toString(), packagesPath: buildInfo.packagesPath, artifacts: globals.artifacts!, processManager: globals.processManager, logger: globals.logger, fileSystem: globals.fs, platform: platform, ); } else { // The flutter-widget-cache feature only applies to run mode. List<String> extraFrontEndOptions = buildInfo.extraFrontEndOptions; extraFrontEndOptions = <String>[ if (featureFlags.isSingleWidgetReloadEnabled) '--flutter-widget-cache', '--enable-experiment=alternative-invalidation-strategy', ...extraFrontEndOptions, ]; generator = ResidentCompiler( globals.artifacts!.getArtifactPath( Artifact.flutterPatchedSdkPath, platform: targetPlatform, mode: buildInfo.mode, ), buildMode: buildInfo.mode, trackWidgetCreation: buildInfo.trackWidgetCreation, fileSystemRoots: buildInfo.fileSystemRoots, fileSystemScheme: buildInfo.fileSystemScheme, targetModel: targetModel, dartDefines: buildInfo.dartDefines, extraFrontEndOptions: extraFrontEndOptions, initializeFromDill: buildInfo.initializeFromDill ?? getDefaultCachedKernelPath( trackWidgetCreation: buildInfo.trackWidgetCreation, dartDefines: buildInfo.dartDefines, extraFrontEndOptions: extraFrontEndOptions, ), assumeInitializeFromDillUpToDate: buildInfo.assumeInitializeFromDillUpToDate, packagesPath: buildInfo.packagesPath, artifacts: globals.artifacts!, processManager: globals.processManager, logger: globals.logger, platform: platform, fileSystem: globals.fs, ); } return FlutterDevice( device, targetModel: targetModel, targetPlatform: targetPlatform, generator: generator, buildInfo: buildInfo, userIdentifier: userIdentifier, developmentShaderCompiler: shaderCompiler, developmentSceneImporter: sceneImporter, ); } final TargetPlatform? targetPlatform; final Device? device; final ResidentCompiler? generator; final BuildInfo buildInfo; final String? userIdentifier; final DevelopmentShaderCompiler developmentShaderCompiler; final DevelopmentSceneImporter? developmentSceneImporter; DevFSWriter? devFSWriter; Stream<Uri?>? observatoryUris; FlutterVmService? vmService; DevFS? devFS; ApplicationPackage? package; // ignore: cancel_subscriptions StreamSubscription<String>? _loggingSubscription; bool? _isListeningForObservatoryUri; /// Whether the stream [observatoryUris] is still open. bool get isWaitingForObservatory => _isListeningForObservatoryUri ?? false; /// If the [reloadSources] parameter is not null the 'reloadSources' service /// will be registered. /// The 'reloadSources' service can be used by other Service Protocol clients /// connected to the VM (e.g. Observatory) to request a reload of the source /// code of the running application (a.k.a. HotReload). /// The 'compileExpression' service can be used to compile user-provided /// expressions requested during debugging of the application. /// This ensures that the reload process follows the normal orchestration of /// the Flutter Tools and not just the VM internal service. Future<void> connect({ ReloadSources? reloadSources, Restart? restart, CompileExpression? compileExpression, GetSkSLMethod? getSkSLMethod, PrintStructuredErrorLogMethod? printStructuredErrorLogMethod, int? hostVmServicePort, int? ddsPort, bool disableServiceAuthCodes = false, bool cacheStartupProfile = false, bool enableDds = true, required bool allowExistingDdsInstance, bool ipv6 = false, }) { final Completer<void> completer = Completer<void>(); late StreamSubscription<void> subscription; bool isWaitingForVm = false; subscription = observatoryUris!.listen((Uri? observatoryUri) async { // FYI, this message is used as a sentinel in tests. globals.printTrace('Connecting to service protocol: $observatoryUri'); isWaitingForVm = true; bool existingDds = false; FlutterVmService? service; if (enableDds) { void handleError(Exception e, StackTrace st) { globals.printTrace('Fail to connect to service protocol: $observatoryUri: $e'); if (!completer.isCompleted) { completer.completeError('failed to connect to $observatoryUri', st); } } // First check if the VM service is actually listening on observatoryUri as // this may not be the case when scraping logcat for URIs. If this URI is // from an old application instance, we shouldn't try and start DDS. try { service = await connectToVmService(observatoryUri!, logger: globals.logger); await service.dispose(); } on Exception catch (exception) { globals.printTrace('Fail to connect to service protocol: $observatoryUri: $exception'); if (!completer.isCompleted && !_isListeningForObservatoryUri!) { completer.completeError('failed to connect to $observatoryUri'); } return; } // This first try block is meant to catch errors that occur during DDS startup // (e.g., failure to bind to a port, failure to connect to the VM service, // attaching to a VM service with existing clients, etc.). try { await device!.dds.startDartDevelopmentService( observatoryUri, hostPort: ddsPort, ipv6: ipv6, disableServiceAuthCodes: disableServiceAuthCodes, logger: globals.logger, cacheStartupProfile: cacheStartupProfile, ); } on dds.DartDevelopmentServiceException catch (e, st) { if (!allowExistingDdsInstance || (e.errorCode != dds.DartDevelopmentServiceException.existingDdsInstanceError)) { handleError(e, st); return; } else { existingDds = true; } } on ToolExit { rethrow; } on Exception catch (e, st) { handleError(e, st); return; } } // This second try block handles cases where the VM service connection goes down // before flutter_tools connects to DDS. The DDS `done` future completes when DDS // shuts down, including after an error. If `done` completes before `connectToVmService`, // something went wrong that caused DDS to shutdown early. try { service = await Future.any<dynamic>( <Future<dynamic>>[ connectToVmService( enableDds ? (device!.dds.uri ?? observatoryUri!): observatoryUri!, reloadSources: reloadSources, restart: restart, compileExpression: compileExpression, getSkSLMethod: getSkSLMethod, printStructuredErrorLogMethod: printStructuredErrorLogMethod, device: device, logger: globals.logger, ), if (!existingDds) device!.dds.done.whenComplete(() => throw Exception('DDS shut down too early')), ] ) as FlutterVmService?; } on Exception catch (exception) { globals.printTrace('Fail to connect to service protocol: $observatoryUri: $exception'); if (!completer.isCompleted && !_isListeningForObservatoryUri!) { completer.completeError('failed to connect to $observatoryUri'); } return; } if (completer.isCompleted) { return; } globals.printTrace('Successfully connected to service protocol: $observatoryUri'); vmService = service; (await device!.getLogReader(app: package)).connectedVMService = vmService; completer.complete(); await subscription.cancel(); }, onError: (dynamic error) { globals.printTrace('Fail to handle observatory URI: $error'); }, onDone: () { _isListeningForObservatoryUri = false; if (!completer.isCompleted && !isWaitingForVm) { completer.completeError(Exception('connection to device ended too early')); } }); _isListeningForObservatoryUri = true; return completer.future; } Future<void> exitApps({ @visibleForTesting Duration timeoutDelay = const Duration(seconds: 10), }) async { // TODO(zanderso): https://github.com/flutter/flutter/issues/83127 // When updating `flutter attach` to support running without a device, // this will need to be changed to fall back to io exit. await device!.stopApp(package, userIdentifier: userIdentifier); } Future<Uri?> setupDevFS( String fsName, Directory rootDirectory, ) { // One devFS per device. Shared by all running instances. devFS = DevFS( vmService!, fsName, rootDirectory, osUtils: globals.os, fileSystem: globals.fs, logger: globals.logger, ); return devFS!.create(); } Future<void> startEchoingDeviceLog() async { if (_loggingSubscription != null) { return; } final Stream<String> logStream = (await device!.getLogReader(app: package)).logLines; _loggingSubscription = logStream.listen((String line) { if (!line.contains(globals.kVMServiceMessageRegExp)) { globals.printStatus(line, wrap: false); } }); } Future<void> stopEchoingDeviceLog() async { if (_loggingSubscription == null) { return; } await _loggingSubscription!.cancel(); _loggingSubscription = null; } Future<void> initLogReader() async { final vm_service.VM vm = await vmService!.service.getVM(); final DeviceLogReader logReader = await device!.getLogReader(app: package); logReader.appPid = vm.pid; } Future<int> runHot({ required HotRunner hotRunner, String? route, }) async { final bool prebuiltMode = hotRunner.applicationBinary != null; final String modeName = hotRunner.debuggingOptions.buildInfo.friendlyModeName; globals.printStatus( 'Launching ${getDisplayPath(hotRunner.mainPath, globals.fs)} ' 'on ${device!.name} in $modeName mode...', ); final TargetPlatform targetPlatform = await device!.targetPlatform; package = await ApplicationPackageFactory.instance!.getPackageForPlatform( targetPlatform, buildInfo: hotRunner.debuggingOptions.buildInfo, applicationBinary: hotRunner.applicationBinary, ); final ApplicationPackage? applicationPackage = package; if (applicationPackage == null) { String message = 'No application found for $targetPlatform.'; final String? hint = await getMissingPackageHintForPlatform(targetPlatform); if (hint != null) { message += '\n$hint'; } globals.printError(message); return 1; } devFSWriter = device!.createDevFSWriter(applicationPackage, userIdentifier); final Map<String, dynamic> platformArgs = <String, dynamic>{ 'multidex': hotRunner.multidexEnabled, }; await startEchoingDeviceLog(); // Start the application. final Future<LaunchResult> futureResult = device!.startApp( applicationPackage, mainPath: hotRunner.mainPath, debuggingOptions: hotRunner.debuggingOptions, platformArgs: platformArgs, route: route, prebuiltApplication: prebuiltMode, ipv6: hotRunner.ipv6!, userIdentifier: userIdentifier, ); final LaunchResult result = await futureResult; if (!result.started) { globals.printError('Error launching application on ${device!.name}.'); await stopEchoingDeviceLog(); return 2; } if (result.hasObservatory) { observatoryUris = Stream<Uri?> .value(result.observatoryUri) .asBroadcastStream(); } else { observatoryUris = const Stream<Uri> .empty() .asBroadcastStream(); } return 0; } Future<int> runCold({ required ColdRunner coldRunner, String? route, }) async { final TargetPlatform targetPlatform = await device!.targetPlatform; package = await ApplicationPackageFactory.instance!.getPackageForPlatform( targetPlatform, buildInfo: coldRunner.debuggingOptions.buildInfo, applicationBinary: coldRunner.applicationBinary, ); final ApplicationPackage? applicationPackage = package; if (applicationPackage == null) { String message = 'No application found for $targetPlatform.'; final String? hint = await getMissingPackageHintForPlatform(targetPlatform); if (hint != null) { message += '\n$hint'; } globals.printError(message); return 1; } devFSWriter = device!.createDevFSWriter(applicationPackage, userIdentifier); final String modeName = coldRunner.debuggingOptions.buildInfo.friendlyModeName; final bool prebuiltMode = coldRunner.applicationBinary != null; globals.printStatus( 'Launching ${getDisplayPath(coldRunner.mainPath, globals.fs)} ' 'on ${device!.name} in $modeName mode...', ); final Map<String, dynamic> platformArgs = <String, dynamic>{}; platformArgs['trace-startup'] = coldRunner.traceStartup; platformArgs['multidex'] = coldRunner.multidexEnabled; await startEchoingDeviceLog(); final LaunchResult result = await device!.startApp( applicationPackage, mainPath: coldRunner.mainPath, debuggingOptions: coldRunner.debuggingOptions, platformArgs: platformArgs, route: route, prebuiltApplication: prebuiltMode, ipv6: coldRunner.ipv6!, userIdentifier: userIdentifier, ); if (!result.started) { globals.printError('Error running application on ${device!.name}.'); await stopEchoingDeviceLog(); return 2; } if (result.hasObservatory) { observatoryUris = Stream<Uri?> .value(result.observatoryUri) .asBroadcastStream(); } else { observatoryUris = const Stream<Uri> .empty() .asBroadcastStream(); } return 0; } Future<UpdateFSReport> updateDevFS({ required Uri mainUri, String? target, AssetBundle? bundle, DateTime? firstBuildTime, bool bundleFirstUpload = false, bool bundleDirty = false, bool fullRestart = false, String? projectRootPath, required String pathToReload, required String dillOutputPath, required List<Uri> invalidatedFiles, required PackageConfig packageConfig, }) async { final Status devFSStatus = globals.logger.startProgress( 'Syncing files to device ${device!.name}...', ); UpdateFSReport report; try { report = await devFS!.update( mainUri: mainUri, target: target, bundle: bundle, firstBuildTime: firstBuildTime, bundleFirstUpload: bundleFirstUpload, generator: generator!, fullRestart: fullRestart, dillOutputPath: dillOutputPath, trackWidgetCreation: buildInfo.trackWidgetCreation, projectRootPath: projectRootPath, pathToReload: pathToReload, invalidatedFiles: invalidatedFiles, packageConfig: packageConfig, devFSWriter: devFSWriter, shaderCompiler: developmentShaderCompiler, sceneImporter: developmentSceneImporter, dartPluginRegistrant: FlutterProject.current().dartPluginRegistrant, ); } on DevFSException { devFSStatus.cancel(); return UpdateFSReport(); } devFSStatus.stop(); globals.printTrace('Synced ${getSizeAsMB(report.syncedBytes)}.'); return report; } Future<void> updateReloadStatus(bool wasReloadSuccessful) async { if (wasReloadSuccessful) { generator?.accept(); } else { await generator?.reject(); } } } /// A subset of the [ResidentRunner] for delegating to attached flutter devices. abstract class ResidentHandlers { List<FlutterDevice?> get flutterDevices; /// Whether the resident runner has hot reload and restart enabled. bool get hotMode; /// Whether the resident runner is connect to the device's VM Service. bool get supportsServiceProtocol; /// The application is running in debug mode. bool get isRunningDebug; /// The application is running in profile mode. bool get isRunningProfile; /// The application is running in release mode. bool get isRunningRelease; /// The resident runner should stay resident after establishing a connection with the /// application. bool get stayResident; /// Whether all of the connected devices support hot restart. /// /// To prevent scenarios where only a subset of devices are hot restarted, /// the runner requires that all attached devices can support hot restart /// before enabling it. bool get supportsRestart; /// Whether all of the connected devices support gathering SkSL. bool get supportsWriteSkSL; /// Whether all of the connected devices support hot reload. bool get canHotReload; ResidentDevtoolsHandler? get residentDevtoolsHandler; @protected Logger get logger; @protected FileSystem? get fileSystem; /// Called to print help to the terminal. void printHelp({ required bool details }); /// Perform a hot reload or hot restart of all attached applications. /// /// If [fullRestart] is true, a hot restart is performed. Otherwise a hot reload /// is run instead. On web devices, this only performs a hot restart regardless of /// the value of [fullRestart]. Future<OperationResult> restart({ bool fullRestart = false, bool pause = false, String? reason }) { final String mode = isRunningProfile ? 'profile' :isRunningRelease ? 'release' : 'this'; throw Exception('${fullRestart ? 'Restart' : 'Reload'} is not supported in $mode mode'); } /// Dump the application's current widget tree to the terminal. Future<bool> debugDumpApp() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice? device in flutterDevices) { final List<FlutterView> views = await device!.vmService!.getFlutterViews(); for (final FlutterView view in views) { final String data = await device.vmService!.flutterDebugDumpApp( isolateId: view.uiIsolate!.id!, ); logger.printStatus(data); } } return true; } /// Dump the application's current render tree to the terminal. Future<bool> debugDumpRenderTree() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice? device in flutterDevices) { final List<FlutterView> views = await device!.vmService!.getFlutterViews(); for (final FlutterView view in views) { final String data = await device.vmService!.flutterDebugDumpRenderTree( isolateId: view.uiIsolate!.id!, ); logger.printStatus(data); } } return true; } /// Dump frame rasterization metrics for the last rendered frame. /// /// The last frames gets re-painted while recording additional tracing info /// pertaining to the various draw calls issued by the frame. The timings /// recorded here are not indicative of production performance. The intended /// use case is to look at the various layers in proportion to see what /// contributes the most towards raster performance. Future<bool> debugFrameJankMetrics() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice? device in flutterDevices) { if (device?.targetPlatform == TargetPlatform.web_javascript) { logger.printWarning('Unable to get jank metrics for web'); continue; } final List<FlutterView> views = await device!.vmService!.getFlutterViews(); for (final FlutterView view in views) { final Map<String, Object?>? rasterData = await device.vmService!.renderFrameWithRasterStats( viewId: view.id, uiIsolateId: view.uiIsolate!.id, ); if (rasterData != null) { final File tempFile = globals.fsUtils.getUniqueFile( globals.fs.currentDirectory, 'flutter_jank_metrics', 'json', ); tempFile.writeAsStringSync(jsonEncode(rasterData), flush: true); logger.printStatus('Wrote jank metrics to ${tempFile.absolute.path}'); } else { logger.printWarning('Unable to get jank metrics.'); } } } return true; } /// Dump the application's current layer tree to the terminal. Future<bool> debugDumpLayerTree() async { if (!supportsServiceProtocol || !isRunningDebug) { return false; } for (final FlutterDevice? device in flutterDevices) { final List<FlutterView> views = await device!.vmService!.getFlutterViews(); for (final FlutterView view in views) { final String data = await device.vmService!.flutterDebugDumpLayerTree( isolateId: view.uiIsolate!.id!, ); logger.printStatus(data); } } return true; } /// Dump the application's current semantics tree to the terminal. /// /// If semantics are not enabled, nothing is returned. Future<bool> debugDumpSemanticsTreeInTraversalOrder() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice? device in flutterDevices) { final List<FlutterView> views = await device!.vmService!.getFlutterViews(); for (final FlutterView view in views) { final String data = await device.vmService!.flutterDebugDumpSemanticsTreeInTraversalOrder( isolateId: view.uiIsolate!.id!, ); logger.printStatus(data); } } return true; } /// Dump the application's current semantics tree to the terminal. /// /// If semantics are not enabled, nothing is returned. Future<bool> debugDumpSemanticsTreeInInverseHitTestOrder() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice? device in flutterDevices) { final List<FlutterView> views = await device!.vmService!.getFlutterViews(); for (final FlutterView view in views) { final String data = await device.vmService!.flutterDebugDumpSemanticsTreeInInverseHitTestOrder( isolateId: view.uiIsolate!.id!, ); logger.printStatus(data); } } return true; } /// Toggle the "paint size" debugging feature. Future<bool> debugToggleDebugPaintSizeEnabled() async { if (!supportsServiceProtocol || !isRunningDebug) { return false; } for (final FlutterDevice? device in flutterDevices) { final List<FlutterView> views = await device!.vmService!.getFlutterViews(); for (final FlutterView view in views) { await device.vmService!.flutterToggleDebugPaintSizeEnabled( isolateId: view.uiIsolate!.id!, ); } } return true; } /// Toggle the performance overlay. /// /// This is not supported in web mode. Future<bool> debugTogglePerformanceOverlayOverride() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice? device in flutterDevices) { if (device!.targetPlatform == TargetPlatform.web_javascript) { continue; } final List<FlutterView> views = await device.vmService!.getFlutterViews(); for (final FlutterView view in views) { await device.vmService!.flutterTogglePerformanceOverlayOverride( isolateId: view.uiIsolate!.id!, ); } } return true; } /// Toggle the widget inspector. Future<bool> debugToggleWidgetInspector() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice? device in flutterDevices) { final List<FlutterView> views = await device!.vmService!.getFlutterViews(); for (final FlutterView view in views) { await device.vmService!.flutterToggleWidgetInspector( isolateId: view.uiIsolate!.id!, ); } } return true; } /// Toggle the "invert images" debugging feature. Future<bool> debugToggleInvertOversizedImages() async { if (!supportsServiceProtocol || !isRunningDebug) { return false; } for (final FlutterDevice? device in flutterDevices) { final List<FlutterView> views = await device!.vmService!.getFlutterViews(); for (final FlutterView view in views) { await device.vmService!.flutterToggleInvertOversizedImages( isolateId: view.uiIsolate!.id!, ); } } return true; } /// Toggle the "profile widget builds" debugging feature. Future<bool> debugToggleProfileWidgetBuilds() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice? device in flutterDevices) { final List<FlutterView> views = await device!.vmService!.getFlutterViews(); for (final FlutterView view in views) { await device.vmService!.flutterToggleProfileWidgetBuilds( isolateId: view.uiIsolate!.id!, ); } } return true; } /// Toggle the operating system brightness (light or dark). Future<bool> debugToggleBrightness() async { if (!supportsServiceProtocol) { return false; } final List<FlutterView> views = await flutterDevices.first!.vmService!.getFlutterViews(); final Brightness? current = await flutterDevices.first!.vmService!.flutterBrightnessOverride( isolateId: views.first.uiIsolate!.id!, ); Brightness next; if (current == Brightness.light) { next = Brightness.dark; } else { next = Brightness.light; } for (final FlutterDevice? device in flutterDevices) { final List<FlutterView> views = await device!.vmService!.getFlutterViews(); for (final FlutterView view in views) { await device.vmService!.flutterBrightnessOverride( isolateId: view.uiIsolate!.id!, brightness: next, ); } logger.printStatus('Changed brightness to $next.'); } return true; } /// Rotate the application through different `defaultTargetPlatform` values. Future<bool> debugTogglePlatform() async { if (!supportsServiceProtocol || !isRunningDebug) { return false; } final List<FlutterView> views = await flutterDevices.first!.vmService!.getFlutterViews(); final String from = await flutterDevices .first!.vmService!.flutterPlatformOverride( isolateId: views.first.uiIsolate!.id!, ); final String to = nextPlatform(from); for (final FlutterDevice? device in flutterDevices) { final List<FlutterView> views = await device!.vmService!.getFlutterViews(); for (final FlutterView view in views) { await device.vmService!.flutterPlatformOverride( platform: to, isolateId: view.uiIsolate!.id!, ); } } logger.printStatus('Switched operating system to $to'); return true; } /// Write the SkSL shaders to a zip file in build directory. /// /// Returns the name of the file, or `null` on failures. Future<String?> writeSkSL() async { if (!supportsWriteSkSL) { throw Exception('writeSkSL is not supported by this runner.'); } final FlutterDevice flutterDevice = flutterDevices.first!; final FlutterVmService vmService = flutterDevice.vmService!; final List<FlutterView> views = await vmService.getFlutterViews(); final Map<String, Object?>? data = await vmService.getSkSLs( viewId: views.first.id, ); final Device device = flutterDevice.device!; return sharedSkSlWriter(device, data); } /// Take a screenshot on the provided [device]. /// /// If the device has a connected vmservice, this method will attempt to hide /// and restore the debug banner before taking the screenshot. /// /// If the device type does not support a "native" screenshot, then this /// will fallback to a rasterizer screenshot from the engine. This has the /// downside of being unable to display the contents of platform views. /// /// This method will return without writing the screenshot file if any /// RPC errors are encountered, printing them to stderr. This is true even /// if an error occurs after the data has already been received, such as /// from restoring the debug banner. Future<void> screenshot(FlutterDevice device) async { if (!device.device!.supportsScreenshot && !supportsServiceProtocol) { return; } final Status status = logger.startProgress( 'Taking screenshot for ${device.device!.name}...', ); final File outputFile = getUniqueFile( fileSystem!.currentDirectory, 'flutter', 'png', ); try { bool result; if (device.device!.supportsScreenshot) { result = await _toggleDebugBanner(device, () => device.device!.takeScreenshot(outputFile)); } else { result = await _takeVmServiceScreenshot(device, outputFile); } if (!result) { return; } final int sizeKB = outputFile.lengthSync() ~/ 1024; status.stop(); logger.printStatus( 'Screenshot written to ${fileSystem!.path.relative(outputFile.path)} (${sizeKB}kB).', ); } on Exception catch (error) { status.cancel(); logger.printError('Error taking screenshot: $error'); } } Future<bool> _takeVmServiceScreenshot(FlutterDevice device, File outputFile) async { final bool isWebDevice = device.targetPlatform == TargetPlatform.web_javascript; assert(supportsServiceProtocol); return _toggleDebugBanner(device, () async { final vm_service.Response? response = isWebDevice ? await device.vmService!.callMethodWrapper('ext.dwds.screenshot') : await device.vmService!.screenshot(); if (response == null) { throw Exception('Failed to take screenshot'); } final String data = response.json![isWebDevice ? 'data' : 'screenshot'] as String; outputFile.writeAsBytesSync(base64.decode(data)); }); } Future<bool> _toggleDebugBanner(FlutterDevice device, Future<void> Function() cb) async { List<FlutterView> views = <FlutterView>[]; if (supportsServiceProtocol) { views = await device.vmService!.getFlutterViews(); } Future<bool> setDebugBanner(bool value) async { try { for (final FlutterView view in views) { await device.vmService!.flutterDebugAllowBanner( value, isolateId: view.uiIsolate!.id!, ); } return true; } on vm_service.RPCError catch (error) { logger.printError('Error communicating with Flutter on the device: $error'); return false; } } if (!await setDebugBanner(false)) { return false; } bool succeeded = true; try { await cb(); } finally { if (!await setDebugBanner(true)) { succeeded = false; } } return succeeded; } /// Remove sigusr signal handlers. Future<void> cleanupAfterSignal(); /// Tear down the runner and leave the application running. /// /// This is not supported on web devices where the runner is running /// the application server as well. Future<void> detach(); /// Tear down the runner and exit the application. Future<void> exit(); /// Run any source generators, such as localizations. /// /// These are automatically run during hot restart, but can be /// triggered manually to see the updated generated code. Future<void> runSourceGenerators(); } // Shared code between different resident application runners. abstract class ResidentRunner extends ResidentHandlers { ResidentRunner( this.flutterDevices, { required this.target, required this.debuggingOptions, String? projectRootPath, this.ipv6, this.stayResident = true, this.hotMode = true, String? dillOutputPath, this.machine = false, ResidentDevtoolsHandlerFactory devtoolsHandler = createDefaultHandler, }) : mainPath = globals.fs.file(target).absolute.path, packagesFilePath = debuggingOptions.buildInfo.packagesPath, projectRootPath = projectRootPath ?? globals.fs.currentDirectory.path, _dillOutputPath = dillOutputPath, artifactDirectory = dillOutputPath == null ? globals.fs.systemTempDirectory.createTempSync('flutter_tool.') : globals.fs.file(dillOutputPath).parent, assetBundle = AssetBundleFactory.instance.createBundle(), commandHelp = CommandHelp( logger: globals.logger, terminal: globals.terminal, platform: globals.platform, outputPreferences: globals.outputPreferences, ) { if (!artifactDirectory.existsSync()) { artifactDirectory.createSync(recursive: true); } _residentDevtoolsHandler = devtoolsHandler(DevtoolsLauncher.instance, this, globals.logger); } @override Logger get logger => globals.logger; @override FileSystem get fileSystem => globals.fs; @override final List<FlutterDevice> flutterDevices; final String target; final DebuggingOptions debuggingOptions; @override final bool stayResident; final bool? ipv6; final String? _dillOutputPath; /// The parent location of the incremental artifacts. final Directory artifactDirectory; final String packagesFilePath; final String projectRootPath; final String mainPath; final AssetBundle assetBundle; final CommandHelp commandHelp; final bool machine; @override ResidentDevtoolsHandler? get residentDevtoolsHandler => _residentDevtoolsHandler; ResidentDevtoolsHandler? _residentDevtoolsHandler; bool _exited = false; Completer<int> _finished = Completer<int>(); BuildResult? _lastBuild; Environment? _environment; @override bool hotMode; /// Returns true if every device is streaming observatory URIs. bool get isWaitingForObservatory { return flutterDevices.every((FlutterDevice? device) { return device!.isWaitingForObservatory; }); } String get dillOutputPath => _dillOutputPath ?? globals.fs.path.join(artifactDirectory.path, 'app.dill'); String getReloadPath({ bool fullRestart = false, required bool swap, }) { if (!fullRestart) { return 'main.dart.incremental.dill'; } return 'main.dart${swap ? '.swap' : ''}.dill'; } bool get debuggingEnabled => debuggingOptions.debuggingEnabled; @override bool get isRunningDebug => debuggingOptions.buildInfo.isDebug; @override bool get isRunningProfile => debuggingOptions.buildInfo.isProfile; @override bool get isRunningRelease => debuggingOptions.buildInfo.isRelease; @override bool get supportsServiceProtocol => isRunningDebug || isRunningProfile; @override bool get supportsWriteSkSL => supportsServiceProtocol; bool get trackWidgetCreation => debuggingOptions.buildInfo.trackWidgetCreation; /// True if the shared Dart plugin registry (which is different than the one /// used for web) should be generated during source generation. bool get generateDartPluginRegistry => true; // Returns the Uri of the first connected device for mobile, // and only connected device for web. // // Would be null if there is no device connected or // there is no devFS associated with the first device. Uri? get uri => flutterDevices.first.devFS?.baseUri; /// Returns [true] if the resident runner exited after invoking [exit()]. bool get exited => _exited; @override bool get supportsRestart { return isRunningDebug && flutterDevices.every((FlutterDevice? device) { return device!.device!.supportsHotRestart; }); } @override bool get canHotReload => hotMode; /// Start the app and keep the process running during its lifetime. /// /// Returns the exit code that we should use for the flutter tool process; 0 /// for success, 1 for user error (e.g. bad arguments), 2 for other failures. Future<int?> run({ Completer<DebugConnectionInfo>? connectionInfoCompleter, Completer<void>? appStartedCompleter, bool enableDevTools = false, String? route, }); /// Connect to a flutter application. /// /// [needsFullRestart] defaults to `true`, and controls if the frontend server should /// compile a full dill. This should be set to `false` if this is called in [ResidentRunner.run], since that method already perfoms an initial compilation. Future<int?> attach({ Completer<DebugConnectionInfo>? connectionInfoCompleter, Completer<void>? appStartedCompleter, bool allowExistingDdsInstance = false, bool enableDevTools = false, bool needsFullRestart = true, }); @override Future<void> runSourceGenerators() async { _environment ??= Environment( artifacts: globals.artifacts!, logger: globals.logger, cacheDir: globals.cache.getRoot(), engineVersion: globals.flutterVersion.engineRevision, fileSystem: globals.fs, flutterRootDir: globals.fs.directory(Cache.flutterRoot), outputDir: globals.fs.directory(getBuildDirectory()), processManager: globals.processManager, platform: globals.platform, usage: globals.flutterUsage, projectDir: globals.fs.currentDirectory, generateDartPluginRegistry: generateDartPluginRegistry, defines: <String, String>{ // Needed for Dart plugin registry generation. kTargetFile: mainPath, }, ); final CompositeTarget compositeTarget = CompositeTarget(<Target>[ const GenerateLocalizationsTarget(), const DartPluginRegistrantTarget(), ]); _lastBuild = await globals.buildSystem.buildIncremental( compositeTarget, _environment!, _lastBuild, ); if (!_lastBuild!.success) { for (final ExceptionMeasurement exceptionMeasurement in _lastBuild!.exceptions.values) { globals.printError( exceptionMeasurement.exception.toString(), stackTrace: globals.logger.isVerbose ? exceptionMeasurement.stackTrace : null, ); } } globals.printTrace('complete'); } @protected void writeVmServiceFile() { if (debuggingOptions.vmserviceOutFile != null) { try { final String address = flutterDevices.first.vmService!.wsAddress.toString(); final File vmserviceOutFile = globals.fs.file(debuggingOptions.vmserviceOutFile); vmserviceOutFile.createSync(recursive: true); vmserviceOutFile.writeAsStringSync(address); } on FileSystemException { globals.printError('Failed to write vmservice-out-file at ${debuggingOptions.vmserviceOutFile}'); } } } @override Future<void> exit() async { _exited = true; await residentDevtoolsHandler!.shutdown(); await stopEchoingDeviceLog(); await preExit(); await exitApp(); // calls appFinished await shutdownDartDevelopmentService(); } @override Future<void> detach() async { await residentDevtoolsHandler!.shutdown(); await stopEchoingDeviceLog(); await preExit(); await shutdownDartDevelopmentService(); appFinished(); } Future<void> stopEchoingDeviceLog() async { await Future.wait<void>( flutterDevices.map<Future<void>>((FlutterDevice? device) => device!.stopEchoingDeviceLog()) ); } Future<void> shutdownDartDevelopmentService() async { await Future.wait<void>( flutterDevices.map<Future<void>>( (FlutterDevice? device) => device?.device?.dds.shutdown() ?? Future<void>.value() ) ); } @protected void cacheInitialDillCompilation() { if (_dillOutputPath != null) { return; } globals.printTrace('Caching compiled dill'); final File outputDill = globals.fs.file(dillOutputPath); if (outputDill.existsSync()) { final String copyPath = getDefaultCachedKernelPath( trackWidgetCreation: trackWidgetCreation, dartDefines: debuggingOptions.buildInfo.dartDefines, extraFrontEndOptions: debuggingOptions.buildInfo.extraFrontEndOptions, ); globals.fs .file(copyPath) .parent .createSync(recursive: true); outputDill.copySync(copyPath); } } void printStructuredErrorLog(vm_service.Event event) { if (event.extensionKind == 'Flutter.Error' && !machine) { final Map<String, Object?>? json = event.extensionData?.data; if (json != null && json.containsKey('renderedErrorText')) { final int errorsSinceReload; if (json.containsKey('errorsSinceReload') && json['errorsSinceReload'] is int) { errorsSinceReload = json['errorsSinceReload']! as int; } else { errorsSinceReload = 0; } if (errorsSinceReload == 0) { // We print a blank line around the first error, to more clearly emphasize it // in the output. (Other errors don't get this.) globals.printStatus(''); } globals.printStatus('${json['renderedErrorText']}'); if (errorsSinceReload == 0) { globals.printStatus(''); } } else { globals.printError('Received an invalid ${globals.logger.terminal.bolden("Flutter.Error")} message from app: $json'); } } } /// If the [reloadSources] parameter is not null the 'reloadSources' service /// will be registered. // // Failures should be indicated by completing the future with an error, using // a string as the error object, which will be used by the caller (attach()) // to display an error message. Future<void> connectToServiceProtocol({ ReloadSources? reloadSources, Restart? restart, CompileExpression? compileExpression, GetSkSLMethod? getSkSLMethod, required bool allowExistingDdsInstance, }) async { if (!debuggingOptions.debuggingEnabled) { throw Exception('The service protocol is not enabled.'); } _finished = Completer<int>(); // Listen for service protocol connection to close. for (final FlutterDevice? device in flutterDevices) { await device!.connect( reloadSources: reloadSources, restart: restart, compileExpression: compileExpression, enableDds: debuggingOptions.enableDds, ddsPort: debuggingOptions.ddsPort, allowExistingDdsInstance: allowExistingDdsInstance, hostVmServicePort: debuggingOptions.hostVmServicePort, getSkSLMethod: getSkSLMethod, printStructuredErrorLogMethod: printStructuredErrorLog, ipv6: ipv6 ?? false, disableServiceAuthCodes: debuggingOptions.disableServiceAuthCodes, cacheStartupProfile: debuggingOptions.cacheStartupProfile, ); await device.vmService!.getFlutterViews(); // This hooks up callbacks for when the connection stops in the future. // We don't want to wait for them. We don't handle errors in those callbacks' // futures either because they just print to logger and is not critical. unawaited(device.vmService!.service.onDone.then<void>( _serviceProtocolDone, onError: _serviceProtocolError, ).whenComplete(_serviceDisconnected)); } } Future<void> _serviceProtocolDone(dynamic object) async { globals.printTrace('Service protocol connection closed.'); } Future<void> _serviceProtocolError(Object error, StackTrace stack) { globals.printTrace('Service protocol connection closed with an error: $error\n$stack'); return Future<void>.error(error, stack); } void _serviceDisconnected() { if (_exited) { // User requested the application exit. return; } if (_finished.isCompleted) { return; } globals.printStatus('Lost connection to device.'); _finished.complete(0); } Future<void> enableObservatory() async { assert(debuggingOptions.serveObservatory); final List<Future<vm_service.Response?>> serveObservatoryRequests = <Future<vm_service.Response?>>[]; for (final FlutterDevice? device in flutterDevices) { if (device == null) { continue; } // Notify the VM service if the user wants Observatory to be served. serveObservatoryRequests.add( device.vmService?.callMethodWrapper('_serveObservatory') ?? Future<vm_service.Response?>.value(), ); } try { await Future.wait(serveObservatoryRequests); } on vm_service.RPCError catch(e) { globals.printWarning('Unable to enable Observatory: $e'); } } void appFinished() { if (_finished.isCompleted) { return; } globals.printStatus('Application finished.'); _finished.complete(0); } void appFailedToStart() { if (!_finished.isCompleted) { _finished.complete(1); } } Future<int> waitForAppToFinish() async { final int exitCode = await _finished.future; await cleanupAtFinish(); return exitCode; } @mustCallSuper Future<void> preExit() async { // If _dillOutputPath is null, the tool created a temporary directory for // the dill. if (_dillOutputPath == null && artifactDirectory.existsSync()) { artifactDirectory.deleteSync(recursive: true); } } Future<void> exitApp() async { final List<Future<void>> futures = <Future<void>>[ for (final FlutterDevice? device in flutterDevices) device!.exitApps(), ]; await Future.wait(futures); appFinished(); } bool get reportedDebuggers => _reportedDebuggers; bool _reportedDebuggers = false; void printDebuggerList({ bool includeObservatory = true, bool includeDevtools = true }) { final DevToolsServerAddress? devToolsServerAddress = residentDevtoolsHandler!.activeDevToolsServer; if (!residentDevtoolsHandler!.readyToAnnounce) { includeDevtools = false; } assert(!includeDevtools || devToolsServerAddress != null); for (final FlutterDevice? device in flutterDevices) { if (device!.vmService == null) { continue; } if (includeObservatory) { // Caution: This log line is parsed by device lab tests. globals.printStatus( 'An Observatory debugger and profiler on ${device.device!.name} is available at: ' '${device.vmService!.httpAddress}', ); } if (includeDevtools) { final Uri? uri = devToolsServerAddress!.uri?.replace( queryParameters: <String, dynamic>{'uri': '${device.vmService!.httpAddress}'}, ); if (uri != null) { globals.printStatus( 'The Flutter DevTools debugger and profiler ' 'on ${device.device!.name} is available at: ${urlToDisplayString(uri)}', ); } } } _reportedDebuggers = true; } void printHelpDetails() { commandHelp.v.print(); if (flutterDevices.any((FlutterDevice? d) => d!.device!.supportsScreenshot)) { commandHelp.s.print(); } if (supportsServiceProtocol) { commandHelp.w.print(); commandHelp.t.print(); if (isRunningDebug) { commandHelp.L.print(); commandHelp.S.print(); commandHelp.U.print(); commandHelp.i.print(); commandHelp.p.print(); commandHelp.I.print(); commandHelp.o.print(); commandHelp.b.print(); } else { commandHelp.S.print(); commandHelp.U.print(); } // Performance related features: `P` should precede `a`, which should precede `M`. commandHelp.P.print(); commandHelp.a.print(); if (supportsWriteSkSL) { commandHelp.M.print(); } if (isRunningDebug) { commandHelp.g.print(); } commandHelp.j.print(); } } @override Future<void> cleanupAfterSignal(); /// Called right before we exit. Future<void> cleanupAtFinish(); } class OperationResult { OperationResult(this.code, this.message, { this.fatal = false, this.updateFSReport, this.extraTimings = const <OperationResultExtraTiming>[] }); /// The result of the operation; a non-zero code indicates a failure. final int code; /// A user facing message about the results of the operation. final String message; /// User facing extra timing information about the operation. final List<OperationResultExtraTiming> extraTimings; /// Whether this error should cause the runner to exit. final bool fatal; final UpdateFSReport? updateFSReport; bool get isOk => code == 0; static final OperationResult ok = OperationResult(0, ''); } class OperationResultExtraTiming { const OperationResultExtraTiming(this.description, this.timeInMs); /// A user facing short description of this timing. final String description; /// The time this operation took in milliseconds. final int timeInMs; } Future<String?> getMissingPackageHintForPlatform(TargetPlatform platform) async { switch (platform) { case TargetPlatform.android_arm: case TargetPlatform.android_arm64: case TargetPlatform.android_x64: case TargetPlatform.android_x86: final FlutterProject project = FlutterProject.current(); final String manifestPath = globals.fs.path.relative(project.android.appManifestFile.path); return 'Is your project missing an $manifestPath?\nConsider running "flutter create ." to create one.'; case TargetPlatform.ios: return 'Is your project missing an ios/Runner/Info.plist?\nConsider running "flutter create ." to create one.'; case TargetPlatform.android: case TargetPlatform.darwin: case TargetPlatform.fuchsia_arm64: case TargetPlatform.fuchsia_x64: case TargetPlatform.linux_arm64: case TargetPlatform.linux_x64: case TargetPlatform.tester: case TargetPlatform.web_javascript: case TargetPlatform.windows_x64: return null; } } /// Redirects terminal commands to the correct resident runner methods. class TerminalHandler { TerminalHandler(this.residentRunner, { required Logger logger, required Terminal terminal, required Signals signals, required io.ProcessInfo processInfo, required bool reportReady, String? pidFile, }) : _logger = logger, _terminal = terminal, _signals = signals, _processInfo = processInfo, _reportReady = reportReady, _pidFile = pidFile; final Logger _logger; final Terminal _terminal; final Signals _signals; final io.ProcessInfo _processInfo; final bool _reportReady; final String? _pidFile; final ResidentHandlers residentRunner; bool _processingUserRequest = false; StreamSubscription<void>? subscription; File? _actualPidFile; @visibleForTesting String? lastReceivedCommand; /// This is only a buffer logger in unit tests @visibleForTesting BufferLogger get logger => _logger as BufferLogger; void setupTerminal() { if (!_logger.quiet) { _logger.printStatus(''); residentRunner.printHelp(details: false); } _terminal.singleCharMode = true; subscription = _terminal.keystrokes.listen(processTerminalInput); } final Map<io.ProcessSignal, Object> _signalTokens = <io.ProcessSignal, Object>{}; void _addSignalHandler(io.ProcessSignal signal, SignalHandler handler) { _signalTokens[signal] = _signals.addHandler(signal, handler); } void registerSignalHandlers() { assert(residentRunner.stayResident); _addSignalHandler(io.ProcessSignal.sigint, _cleanUp); _addSignalHandler(io.ProcessSignal.sigterm, _cleanUp); if (residentRunner.supportsServiceProtocol && residentRunner.supportsRestart) { _addSignalHandler(io.ProcessSignal.sigusr1, _handleSignal); _addSignalHandler(io.ProcessSignal.sigusr2, _handleSignal); if (_pidFile != null) { _logger.printTrace('Writing pid to: $_pidFile'); _actualPidFile = _processInfo.writePidFile(_pidFile!); } } } /// Unregisters terminal signal and keystroke handlers. void stop() { assert(residentRunner.stayResident); if (_actualPidFile != null) { try { _logger.printTrace('Deleting pid file (${_actualPidFile!.path}).'); _actualPidFile!.deleteSync(); } on FileSystemException catch (error) { _logger.printWarning('Failed to delete pid file (${_actualPidFile!.path}): ${error.message}'); } _actualPidFile = null; } for (final MapEntry<io.ProcessSignal, Object> entry in _signalTokens.entries) { _signals.removeHandler(entry.key, entry.value); } _signalTokens.clear(); subscription?.cancel(); } /// Returns [true] if the input has been handled by this function. Future<bool> _commonTerminalInputHandler(String character) async { _logger.printStatus(''); // the key the user tapped might be on this line switch (character) { case 'a': return residentRunner.debugToggleProfileWidgetBuilds(); case 'b': return residentRunner.debugToggleBrightness(); case 'c': _logger.clear(); return true; case 'd': case 'D': await residentRunner.detach(); return true; case 'g': await residentRunner.runSourceGenerators(); return true; case 'h': case 'H': case '?': // help residentRunner.printHelp(details: true); return true; case 'i': return residentRunner.debugToggleWidgetInspector(); case 'I': return residentRunner.debugToggleInvertOversizedImages(); case 'j': case 'J': return residentRunner.debugFrameJankMetrics(); case 'L': return residentRunner.debugDumpLayerTree(); case 'o': case 'O': return residentRunner.debugTogglePlatform(); case 'M': if (residentRunner.supportsWriteSkSL) { await residentRunner.writeSkSL(); return true; } return false; case 'p': return residentRunner.debugToggleDebugPaintSizeEnabled(); case 'P': return residentRunner.debugTogglePerformanceOverlayOverride(); case 'q': case 'Q': // exit await residentRunner.exit(); return true; case 'r': if (!residentRunner.canHotReload) { return false; } final OperationResult result = await residentRunner.restart(); if (result.fatal) { throwToolExit(result.message); } if (!result.isOk) { _logger.printStatus('Try again after fixing the above error(s).', emphasis: true); } return true; case 'R': // If hot restart is not supported for all devices, ignore the command. if (!residentRunner.supportsRestart || !residentRunner.hotMode) { return false; } final OperationResult result = await residentRunner.restart(fullRestart: true); if (result.fatal) { throwToolExit(result.message); } if (!result.isOk) { _logger.printStatus('Try again after fixing the above error(s).', emphasis: true); } return true; case 's': for (final FlutterDevice? device in residentRunner.flutterDevices) { await residentRunner.screenshot(device!); } return true; case 'S': return residentRunner.debugDumpSemanticsTreeInTraversalOrder(); case 't': case 'T': return residentRunner.debugDumpRenderTree(); case 'U': return residentRunner.debugDumpSemanticsTreeInInverseHitTestOrder(); case 'v': case 'V': return residentRunner.residentDevtoolsHandler!.launchDevToolsInBrowser(flutterDevices: residentRunner.flutterDevices); case 'w': case 'W': return residentRunner.debugDumpApp(); } return false; } Future<void> processTerminalInput(String command) async { // When terminal doesn't support line mode, '\n' can sneak into the input. command = command.trim(); if (_processingUserRequest) { _logger.printTrace('Ignoring terminal input: "$command" because we are busy.'); return; } _processingUserRequest = true; try { lastReceivedCommand = command; await _commonTerminalInputHandler(command); // Catch all exception since this is doing cleanup and rethrowing. } catch (error, st) { // ignore: avoid_catches_without_on_clauses // Don't print stack traces for known error types. if (error is! ToolExit) { _logger.printError('$error\n$st'); } await _cleanUp(null); rethrow; } finally { _processingUserRequest = false; if (_reportReady) { _logger.printStatus('ready'); } } } Future<void> _handleSignal(io.ProcessSignal signal) async { if (_processingUserRequest) { _logger.printTrace('Ignoring signal: "$signal" because we are busy.'); return; } _processingUserRequest = true; final bool fullRestart = signal == io.ProcessSignal.sigusr2; try { await residentRunner.restart(fullRestart: fullRestart); } finally { _processingUserRequest = false; } } Future<void> _cleanUp(io.ProcessSignal? signal) async { _terminal.singleCharMode = false; await subscription?.cancel(); await residentRunner.cleanupAfterSignal(); } } class DebugConnectionInfo { DebugConnectionInfo({ this.httpUri, this.wsUri, this.baseUri }); final Uri? httpUri; final Uri? wsUri; final String? baseUri; } /// Returns the next platform value for the switcher. /// /// These values must match what is available in /// `packages/flutter/lib/src/foundation/binding.dart`. String nextPlatform(String currentPlatform) { switch (currentPlatform) { case 'android': return 'iOS'; case 'iOS': return 'fuchsia'; case 'fuchsia': return 'macOS'; case 'macOS': return 'android'; default: assert(false); // Invalid current platform. return 'android'; } } /// A launcher for the devtools debugger and analysis tool. abstract class DevtoolsLauncher { static DevtoolsLauncher? get instance => context.get<DevtoolsLauncher>(); /// Serve Dart DevTools and return the host and port they are available on. /// /// This method must return a future that is guaranteed not to fail, because it /// will be used in unawaited contexts. It may, however, return null. Future<DevToolsServerAddress?> serve(); /// Launch a Dart DevTools process, optionally targeting a specific VM Service /// URI if [vmServiceUri] is non-null. /// /// [additionalArguments] may be optionally specified and are passed directly /// to the devtools run command. /// /// This method must return a future that is guaranteed not to fail, because it /// will be used in unawaited contexts. Future<void> launch(Uri vmServiceUri, {List<String>? additionalArguments}); Future<void> close(); /// When measuring devtools memory via additional arguments, the launch process /// will technically never complete. /// /// Us this as an indicator that the process has started. Future<void>? processStart; /// Returns a future that completes when the DevTools server is ready. /// /// Completes when [devToolsUrl] is set. That can be set either directly, or /// by calling [serve]. Future<void> get ready => _readyCompleter.future; Completer<void> _readyCompleter = Completer<void>(); Uri? get devToolsUrl => _devToolsUrl; Uri? _devToolsUrl; set devToolsUrl(Uri? value) { assert((_devToolsUrl == null) != (value == null)); _devToolsUrl = value; if (_devToolsUrl != null) { _readyCompleter.complete(); } else { _readyCompleter = Completer<void>(); } } /// The URL of the current DevTools server. /// /// Returns null if [ready] is not complete. DevToolsServerAddress? get activeDevToolsServer { if (_devToolsUrl == null) { return null; } return DevToolsServerAddress(devToolsUrl!.host, devToolsUrl!.port); } } class DevToolsServerAddress { DevToolsServerAddress(this.host, this.port); final String host; final int port; Uri? get uri { return Uri(scheme: 'http', host: host, port: port); } }