| // 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 'globals.dart' as globals; |
| import 'ios/application_package.dart'; |
| import 'ios/devices.dart'; |
| 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, |
| frontendServerStarterPath: buildInfo.frontendServerStarterPath, |
| 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, |
| frontendServerStarterPath: buildInfo.frontendServerStarterPath, |
| 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 { |
| List<String> extraFrontEndOptions = buildInfo.extraFrontEndOptions; |
| extraFrontEndOptions = <String>[ |
| '--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, |
| frontendServerStarterPath: buildInfo.frontendServerStarterPath, |
| 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?>? vmServiceUris; |
| FlutterVmService? vmService; |
| DevFS? devFS; |
| ApplicationPackage? package; |
| // ignore: cancel_subscriptions |
| StreamSubscription<String>? _loggingSubscription; |
| bool? _isListeningForVmServiceUri; |
| |
| /// Whether the stream [vmServiceUris] is still open. |
| bool get isWaitingForVmService => _isListeningForVmServiceUri ?? 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. VmService) 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 = vmServiceUris!.listen((Uri? vmServiceUri) async { |
| // FYI, this message is used as a sentinel in tests. |
| globals.printTrace('Connecting to service protocol: $vmServiceUri'); |
| isWaitingForVm = true; |
| bool existingDds = false; |
| FlutterVmService? service; |
| if (enableDds) { |
| void handleError(Exception e, StackTrace st) { |
| globals.printTrace('Fail to connect to service protocol: $vmServiceUri: $e'); |
| if (!completer.isCompleted) { |
| completer.completeError('failed to connect to $vmServiceUri', st); |
| } |
| } |
| // First check if the VM service is actually listening on vmServiceUri 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(vmServiceUri!, logger: globals.logger); |
| await service.dispose(); |
| } on Exception catch (exception) { |
| globals.printTrace('Fail to connect to service protocol: $vmServiceUri: $exception'); |
| if (!completer.isCompleted && !_isListeningForVmServiceUri!) { |
| completer.completeError('failed to connect to $vmServiceUri'); |
| } |
| 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( |
| vmServiceUri, |
| 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 ?? vmServiceUri!): vmServiceUri!, |
| reloadSources: reloadSources, |
| restart: restart, |
| compileExpression: compileExpression, |
| getSkSLMethod: getSkSLMethod, |
| flutterProject: FlutterProject.current(), |
| 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: $vmServiceUri: $exception'); |
| if (!completer.isCompleted && !_isListeningForVmServiceUri!) { |
| completer.completeError('failed to connect to $vmServiceUri'); |
| } |
| return; |
| } |
| if (completer.isCompleted) { |
| return; |
| } |
| globals.printTrace('Successfully connected to service protocol: $vmServiceUri'); |
| |
| vmService = service; |
| (await device!.getLogReader(app: package)).connectedVMService = vmService; |
| completer.complete(); |
| await subscription.cancel(); |
| }, onError: (dynamic error) { |
| globals.printTrace('Fail to handle VM Service URI: $error'); |
| }, onDone: () { |
| _isListeningForVmServiceUri = false; |
| if (!completer.isCompleted && !isWaitingForVm) { |
| completer.completeError(Exception('connection to device ended too early')); |
| } |
| }); |
| _isListeningForVmServiceUri = 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(DebuggingOptions debuggingOptions) async { |
| if (_loggingSubscription != null) { |
| return; |
| } |
| final Stream<String> logStream; |
| if (device is IOSDevice) { |
| logStream = (device! as IOSDevice).getLogReader( |
| app: package as IOSApp?, |
| usingCISystem: debuggingOptions.usingCISystem, |
| ).logLines; |
| } else { |
| 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(hotRunner.debuggingOptions); |
| |
| // 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.hasVmService) { |
| vmServiceUris = Stream<Uri?> |
| .value(result.vmServiceUri) |
| .asBroadcastStream(); |
| } else { |
| vmServiceUris = 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(coldRunner.debuggingOptions); |
| |
| 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.hasVmService) { |
| vmServiceUris = Stream<Uri?> |
| .value(result.vmServiceUri) |
| .asBroadcastStream(); |
| } else { |
| vmServiceUris = 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; |
| } |
| |
| Future<bool> debugDumpFocusTree() 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!.flutterDebugDumpFocusTree( |
| 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 { |
| if (device.targetPlatform != TargetPlatform.web_javascript) { |
| return false; |
| } |
| assert(supportsServiceProtocol); |
| |
| return _toggleDebugBanner(device, () async { |
| final vm_service.Response? response = await device.vmService!.callMethodWrapper('ext.dwds.screenshot'); |
| if (response == null) { |
| throw Exception('Failed to take screenshot'); |
| } |
| final String data = response.json!['data'] 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 vmService URIs. |
| bool get isWaitingForVmService { |
| return flutterDevices.every((FlutterDevice? device) { |
| return device!.isWaitingForVmService; |
| }); |
| } |
| |
| 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 performs 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 includeVmService = 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 (includeVmService) { |
| // Caution: This log line is parsed by device lab tests. |
| globals.printStatus( |
| 'A Dart VM Service 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.f.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 'f': |
| return residentRunner.debugDumpFocusTree(); |
| 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) { |
| const List<String> platforms = <String>[ |
| 'android', |
| 'iOS', |
| 'windows', |
| 'macOS', |
| 'linux', |
| 'fuchsia', |
| ]; |
| final int index = platforms.indexOf(currentPlatform); |
| assert(index >= 0, 'unknown platform "$currentPlatform"'); |
| return platforms[(index + 1) % platforms.length]; |
| } |
| |
| /// 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); |
| } |
| } |