// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'package:meta/meta.dart'; 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/localizations.dart'; import 'bundle.dart'; import 'cache.dart'; import 'compile.dart'; import 'devfs.dart'; import 'device.dart'; import 'features.dart'; import 'globals.dart' as globals; import 'project.dart'; import 'run_cold.dart'; import 'run_hot.dart'; import 'vmservice.dart'; class FlutterDevice { FlutterDevice( this.device, { @required this.buildInfo, this.fileSystemRoots, this.fileSystemScheme, TargetModel targetModel = TargetModel.flutter, TargetPlatform targetPlatform, ResidentCompiler generator, this.userIdentifier, }) : assert(buildInfo.trackWidgetCreation != null), generator = generator ?? ResidentCompiler( globals.artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, platform: targetPlatform, mode: buildInfo.mode, ), buildMode: buildInfo.mode, trackWidgetCreation: buildInfo.trackWidgetCreation, fileSystemRoots: fileSystemRoots ?? <String>[], fileSystemScheme: fileSystemScheme, targetModel: targetModel, dartDefines: buildInfo.dartDefines, packagesPath: buildInfo.packagesPath, extraFrontEndOptions: buildInfo.extraFrontEndOptions, artifacts: globals.artifacts, processManager: globals.processManager, logger: globals.logger, platform: globals.platform, ); /// Create a [FlutterDevice] with optional code generation enabled. static Future<FlutterDevice> create( Device device, { @required String target, @required BuildInfo buildInfo, @required Platform platform, List<String> fileSystemRoots, String fileSystemScheme, TargetModel targetModel = TargetModel.flutter, List<String> experimentalFlags, ResidentCompiler generator, String userIdentifier, }) async { ResidentCompiler generator; final TargetPlatform targetPlatform = await device.targetPlatform; if (device.platformType == PlatformType.fuchsia) { targetModel = TargetModel.flutterRunner; } // 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(jonahwilliams): consistently provide these flags across platforms. Artifact platformDillArtifact; final List<String> extraFrontEndOptions = List<String>.of(buildInfo.extraFrontEndOptions ?? <String>[]); if (buildInfo.nullSafetyMode == NullSafetyMode.unsound) { platformDillArtifact = Artifact.webPlatformKernelDill; if (!extraFrontEndOptions.contains('--no-sound-null-safety')) { extraFrontEndOptions.add('--no-sound-null-safety'); } } else if (buildInfo.nullSafetyMode == NullSafetyMode.sound) { platformDillArtifact = Artifact.webPlatformSoundKernelDill; if (!extraFrontEndOptions.contains('--sound-null-safety')) { extraFrontEndOptions.add('--sound-null-safety'); } } else { assert(false); } generator = ResidentCompiler( globals.artifacts.getArtifactPath(Artifact.flutterWebSdk, mode: buildInfo.mode), buildMode: buildInfo.mode, trackWidgetCreation: buildInfo.trackWidgetCreation, fileSystemRoots: fileSystemRoots ?? <String>[], // Override the filesystem scheme so that the frontend_server can find // the generated entrypoint code. fileSystemScheme: 'org-dartlang-app', initializeFromDill: getDefaultCachedKernelPath( trackWidgetCreation: buildInfo.trackWidgetCreation, dartDefines: buildInfo.dartDefines, extraFrontEndOptions: extraFrontEndOptions, ), targetModel: TargetModel.dartdevc, extraFrontEndOptions: extraFrontEndOptions, platformDill: globals.fs.file(globals.artifacts .getArtifactPath(platformDillArtifact, mode: buildInfo.mode)) .absolute.uri.toString(), dartDefines: buildInfo.dartDefines, librariesSpec: globals.fs.file(globals.artifacts .getArtifactPath(Artifact.flutterWebLibrariesJson)).uri.toString(), packagesPath: buildInfo.packagesPath, artifacts: globals.artifacts, processManager: globals.processManager, logger: globals.logger, platform: platform, ); } else { // The flutter-widget-cache feature only applies to run mode. List<String> extraFrontEndOptions = buildInfo.extraFrontEndOptions; if (featureFlags.isSingleWidgetReloadEnabled) { extraFrontEndOptions = <String>[ '--flutter-widget-cache', ...?extraFrontEndOptions, ]; } generator = ResidentCompiler( globals.artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, platform: targetPlatform, mode: buildInfo.mode, ), buildMode: buildInfo.mode, trackWidgetCreation: buildInfo.trackWidgetCreation, fileSystemRoots: fileSystemRoots, fileSystemScheme: fileSystemScheme, targetModel: targetModel, dartDefines: buildInfo.dartDefines, extraFrontEndOptions: extraFrontEndOptions, initializeFromDill: getDefaultCachedKernelPath( trackWidgetCreation: buildInfo.trackWidgetCreation, dartDefines: buildInfo.dartDefines, extraFrontEndOptions: extraFrontEndOptions, ), packagesPath: buildInfo.packagesPath, artifacts: globals.artifacts, processManager: globals.processManager, logger: globals.logger, platform: platform, ); } return FlutterDevice( device, fileSystemRoots: fileSystemRoots, fileSystemScheme:fileSystemScheme, targetModel: targetModel, targetPlatform: targetPlatform, generator: generator, buildInfo: buildInfo, userIdentifier: userIdentifier, ); } final Device device; final ResidentCompiler generator; final BuildInfo buildInfo; final String userIdentifier; DevFSWriter devFSWriter; Stream<Uri> observatoryUris; vm_service.VmService vmService; DevFS devFS; ApplicationPackage package; List<String> fileSystemRoots; String fileSystemScheme; 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 disableDds = false, bool ipv6 = false, }) { final Completer<void> completer = Completer<void>(); 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; vm_service.VmService service; if (!disableDds) { // 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, ddsPort, ipv6, disableServiceAuthCodes, ); } on Exception catch (e) { globals.printTrace('Fail to connect to service protocol: $observatoryUri: $e'); if (!completer.isCompleted && !_isListeningForObservatoryUri) { completer.completeError('failed to connect to $observatoryUri'); } 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( disableDds ? observatoryUri : device.dds.uri, reloadSources: reloadSources, restart: restart, compileExpression: compileExpression, getSkSLMethod: getSkSLMethod, printStructuredErrorLogMethod: printStructuredErrorLogMethod, device: device, ), device.dds.done.whenComplete(() => throw Exception('DDS shut down too early')), ] ) as vm_service.VmService; } 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 { if (!device.supportsFlutterExit || vmService == null) { return device.stopApp(package, userIdentifier: userIdentifier); } final List<FlutterView> views = await vmService.getFlutterViews(); if (views == null || views.isEmpty) { return device.stopApp(package, userIdentifier: userIdentifier); } // If any of the flutter views are paused, we might not be able to // cleanly exit since the service extension may not have been registered. for (final FlutterView flutterView in views) { final vm_service.Isolate isolate = await vmService .getIsolateOrNull(flutterView.uiIsolate.id); if (isolate == null) { continue; } if (isPauseEvent(isolate.pauseEvent.kind)) { return device.stopApp(package, userIdentifier: userIdentifier); } } for (final FlutterView view in views) { if (view != null && view.uiIsolate != null) { // If successful, there will be no response from flutterExit. unawaited(vmService.flutterExit( isolateId: view.uiIsolate.id, )); } } return vmService.onDone .catchError((dynamic error, StackTrace stackTrace) { globals.logger.printError( 'unhandled error waiting for vm service exit:\n $error', stackTrace: stackTrace, ); }) .timeout(timeoutDelay, onTimeout: () { // TODO(jonahwilliams): this only seems to fail on CI in the // flutter_attach_android_test. This log should help verify this // is where the tool is getting stuck. globals.logger.printTrace('error: vm service shutdown failed'); return 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> debugDumpApp() async { final List<FlutterView> views = await vmService.getFlutterViews(); for (final FlutterView view in views) { await vmService.flutterDebugDumpApp( isolateId: view.uiIsolate.id, ); } } Future<void> debugDumpRenderTree() async { final List<FlutterView> views = await vmService.getFlutterViews(); for (final FlutterView view in views) { await vmService.flutterDebugDumpRenderTree( isolateId: view.uiIsolate.id, ); } } Future<void> debugDumpLayerTree() async { final List<FlutterView> views = await vmService.getFlutterViews(); for (final FlutterView view in views) { await vmService.flutterDebugDumpLayerTree( isolateId: view.uiIsolate.id, ); } } Future<void> debugDumpSemanticsTreeInTraversalOrder() async { final List<FlutterView> views = await vmService.getFlutterViews(); for (final FlutterView view in views) { await vmService.flutterDebugDumpSemanticsTreeInTraversalOrder( isolateId: view.uiIsolate.id, ); } } Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async { final List<FlutterView> views = await vmService.getFlutterViews(); for (final FlutterView view in views) { await vmService.flutterDebugDumpSemanticsTreeInInverseHitTestOrder( isolateId: view.uiIsolate.id, ); } } Future<void> toggleDebugPaintSizeEnabled() async { final List<FlutterView> views = await vmService.getFlutterViews(); for (final FlutterView view in views) { await vmService.flutterToggleDebugPaintSizeEnabled( isolateId: view.uiIsolate.id, ); } } Future<void> toggleDebugCheckElevationsEnabled() async { final List<FlutterView> views = await vmService.getFlutterViews(); for (final FlutterView view in views) { await vmService.flutterToggleDebugCheckElevationsEnabled( isolateId: view.uiIsolate.id, ); } } Future<void> debugTogglePerformanceOverlayOverride() async { final List<FlutterView> views = await vmService.getFlutterViews(); for (final FlutterView view in views) { await vmService.flutterTogglePerformanceOverlayOverride( isolateId: view.uiIsolate.id, ); } } Future<void> toggleWidgetInspector() async { final List<FlutterView> views = await vmService.getFlutterViews(); for (final FlutterView view in views) { await vmService.flutterToggleWidgetInspector( isolateId: view.uiIsolate.id, ); } } Future<void> toggleInvertOversizedImages() async { final List<FlutterView> views = await vmService.getFlutterViews(); for (final FlutterView view in views) { await vmService.flutterToggleInvertOversizedImages( isolateId: view.uiIsolate.id, ); } } Future<void> toggleProfileWidgetBuilds() async { final List<FlutterView> views = await vmService.getFlutterViews(); for (final FlutterView view in views) { await vmService.flutterToggleProfileWidgetBuilds( isolateId: view.uiIsolate.id, ); } } Future<Brightness> toggleBrightness({ Brightness current }) async { final List<FlutterView> views = await vmService.getFlutterViews(); Brightness next; if (current == Brightness.light) { next = Brightness.dark; } else if (current == Brightness.dark) { next = Brightness.light; } for (final FlutterView view in views) { next = await vmService.flutterBrightnessOverride( isolateId: view.uiIsolate.id, brightness: next, ); } return next; } Future<String> togglePlatform({ String from }) async { final List<FlutterView> views = await vmService.getFlutterViews(); final String to = nextPlatform(from, featureFlags); for (final FlutterView view in views) { await vmService.flutterPlatformOverride( platform: to, isolateId: view.uiIsolate.id, ); } return to; } Future<void> startEchoingDeviceLog() async { if (_loggingSubscription != null) { return; } final Stream<String> logStream = (await device.getLogReader(app: package)).logLines; if (logStream == null) { globals.printError('Failed to read device log stream'); return; } _loggingSubscription = logStream.listen((String line) { if (!line.contains('Observatory listening on http')) { 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.getVM(); final DeviceLogReader logReader = await device.getLogReader(app: package); logReader.appPid = vm.pid; } Future<int> runHot({ HotRunner hotRunner, String route, }) async { final bool prebuiltMode = hotRunner.applicationBinary != null; final String modeName = hotRunner.debuggingOptions.buildInfo.friendlyModeName; globals.printStatus( 'Launching ${globals.fsUtils.getDisplayPath(hotRunner.mainPath)} ' '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, ); if (package == 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(package, userIdentifier); final Map<String, dynamic> platformArgs = <String, dynamic>{}; await startEchoingDeviceLog(); // Start the application. final Future<LaunchResult> futureResult = device.startApp( package, 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({ ColdRunner coldRunner, String route, }) async { final TargetPlatform targetPlatform = await device.targetPlatform; package = await ApplicationPackageFactory.instance.getPackageForPlatform( targetPlatform, buildInfo: coldRunner.debuggingOptions.buildInfo, applicationBinary: coldRunner.applicationBinary, ); devFSWriter = device.createDevFSWriter(package, userIdentifier); final String modeName = coldRunner.debuggingOptions.buildInfo.friendlyModeName; final bool prebuiltMode = coldRunner.applicationBinary != null; if (coldRunner.mainPath == null) { assert(prebuiltMode); globals.printStatus( 'Launching ${package.displayName} ' 'on ${device.name} in $modeName mode...', ); } else { globals.printStatus( 'Launching ${globals.fsUtils.getDisplayPath(coldRunner.mainPath)} ' 'on ${device.name} in $modeName mode...', ); } if (package == 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; } final Map<String, dynamic> platformArgs = <String, dynamic>{}; if (coldRunner.traceStartup != null) { platformArgs['trace-startup'] = coldRunner.traceStartup; } await startEchoingDeviceLog(); final LaunchResult result = await device.startApp( package, 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({ Uri mainUri, String target, AssetBundle bundle, DateTime firstBuildTime, bool bundleFirstUpload = false, bool bundleDirty = false, bool fullRestart = false, String projectRootPath, 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, ); } on DevFSException { devFSStatus.cancel(); return UpdateFSReport(success: false); } devFSStatus.stop(); globals.printTrace('Synced ${getSizeAsMB(report.syncedBytes)}.'); return report; } Future<void> updateReloadStatus(bool wasReloadSuccessful) async { if (wasReloadSuccessful) { generator?.accept(); } else { await generator?.reject(); } } } // Shared code between different resident application runners. abstract class ResidentRunner { ResidentRunner( this.flutterDevices, { this.target, @required this.debuggingOptions, String projectRootPath, this.ipv6, this.stayResident = true, this.hotMode = true, String dillOutputPath, this.machine = false, }) : mainPath = findMainDartFile(target), 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); } } @protected @visibleForTesting final List<FlutterDevice> flutterDevices; final String target; final DebuggingOptions debuggingOptions; 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; DevtoolsLauncher _devtoolsLauncher; bool _exited = false; Completer<int> _finished = Completer<int>(); 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; bool get isRunningDebug => debuggingOptions.buildInfo.isDebug; bool get isRunningProfile => debuggingOptions.buildInfo.isProfile; bool get isRunningRelease => debuggingOptions.buildInfo.isRelease; bool get supportsServiceProtocol => isRunningDebug || isRunningProfile; bool get supportsWriteSkSL => supportsServiceProtocol; bool get trackWidgetCreation => debuggingOptions.buildInfo.trackWidgetCreation; // 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; /// Whether this runner can 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 canHotRestart { return flutterDevices.every((FlutterDevice device) { return device.device.supportsHotRestart; }); } /// Invoke an RPC extension method on the first attached ui isolate of the first device. // TODO(jonahwilliams): Update/Remove this method when refactoring the resident // runner to support a single flutter device. Future<Map<String, dynamic>> invokeFlutterExtensionRpcRawOnFirstIsolate( String method, { Map<String, dynamic> params, }) async { final List<FlutterView> views = await flutterDevices .first .vmService.getFlutterViews(); return flutterDevices .first .vmService .invokeFlutterExtensionRpcRaw( method, args: params, isolateId: views .first.uiIsolate.id ); } /// Whether this runner can hot reload. 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, String route, }); Future<int> attach({ Completer<DebugConnectionInfo> connectionInfoCompleter, Completer<void> appStartedCompleter, }); bool get supportsRestart => false; Future<OperationResult> restart({ bool fullRestart = false, bool pause = false, String reason }) { final String mode = isRunningProfile ? 'profile' : isRunningRelease ? 'release' : 'this'; throw '${fullRestart ? 'Restart' : 'Reload'} is not supported in $mode mode'; } BuildResult _lastBuild; Environment _environment; 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, projectDir: globals.fs.currentDirectory, ); _lastBuild = await globals.buildSystem.buildIncremental( const GenerateLocalizationsTarget(), _environment, _lastBuild, ); if (!_lastBuild.success) { for (final ExceptionMeasurement exceptionMeasurement in _lastBuild.exceptions.values) { globals.logger.printError( exceptionMeasurement.exception.toString(), stackTrace: globals.logger.isVerbose ? exceptionMeasurement.stackTrace : null, ); } } globals.logger.printTrace('complete'); } /// 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 List<FlutterView> views = await flutterDevices .first .vmService.getFlutterViews(); final Map<String, Object> data = await flutterDevices.first.vmService.getSkSLs( viewId: views.first.id, ); final Device device = flutterDevices.first.device; return sharedSkSlWriter(device, data); } @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}'); } } } Future<void> exit() async { _exited = true; await shutdownDevtools(); await stopEchoingDeviceLog(); await preExit(); await exitApp(); await shutdownDartDevelopmentService(); } Future<void> detach() async { await shutdownDevtools(); await stopEchoingDeviceLog(); await preExit(); await shutdownDartDevelopmentService(); appFinished(); } Future<bool> debugDumpApp() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice device in flutterDevices) { await device.debugDumpApp(); } return true; } Future<bool> debugDumpRenderTree() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice device in flutterDevices) { await device.debugDumpRenderTree(); } return true; } Future<bool> debugDumpLayerTree() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice device in flutterDevices) { await device.debugDumpLayerTree(); } return true; } Future<bool> debugDumpSemanticsTreeInTraversalOrder() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice device in flutterDevices) { await device.debugDumpSemanticsTreeInTraversalOrder(); } return true; } Future<bool> debugDumpSemanticsTreeInInverseHitTestOrder() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice device in flutterDevices) { await device.debugDumpSemanticsTreeInInverseHitTestOrder(); } return true; } Future<bool> debugToggleDebugPaintSizeEnabled() async { if (!supportsServiceProtocol || !isRunningDebug) { return false; } for (final FlutterDevice device in flutterDevices) { await device.toggleDebugPaintSizeEnabled(); } return true; } Future<bool> debugToggleDebugCheckElevationsEnabled() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice device in flutterDevices) { await device.toggleDebugCheckElevationsEnabled(); } return true; } Future<bool> debugTogglePerformanceOverlayOverride() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice device in flutterDevices) { await device.debugTogglePerformanceOverlayOverride(); } return true; } Future<bool> debugToggleWidgetInspector() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice device in flutterDevices) { await device.toggleWidgetInspector(); } return true; } Future<bool> debugToggleInvertOversizedImages() async { if (!supportsServiceProtocol || !isRunningDebug) { return false; } for (final FlutterDevice device in flutterDevices) { await device.toggleInvertOversizedImages(); } return true; } Future<bool> debugToggleProfileWidgetBuilds() async { if (!supportsServiceProtocol) { return false; } for (final FlutterDevice device in flutterDevices) { await device.toggleProfileWidgetBuilds(); } return true; } Future<bool> debugToggleBrightness() async { if (!supportsServiceProtocol) { return false; } final Brightness brightness = await flutterDevices.first.toggleBrightness(); Brightness next; for (final FlutterDevice device in flutterDevices) { next = await device.toggleBrightness( current: brightness, ); globals.logger.printStatus('Changed brightness to $next.'); } return true; } /// 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. /// /// Throws an [AssertionError] if [Device.supportsScreenshot] is not true. Future<void> screenshot(FlutterDevice device) async { assert(device.device.supportsScreenshot); final Status status = globals.logger.startProgress( 'Taking screenshot for ${device.device.name}...', ); final File outputFile = globals.fsUtils.getUniqueFile( globals.fs.currentDirectory, 'flutter', 'png', ); List<FlutterView> views = <FlutterView>[]; 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 Exception catch (error) { status.cancel(); globals.printError('Error communicating with Flutter on the device: $error'); return false; } } try { if (supportsServiceProtocol && isRunningDebug) { // Ensure that the vmService access is guarded by supportsServiceProtocol, it // will be null in release mode. views = await device.vmService.getFlutterViews(); if (!await setDebugBanner(false)) { return; } } try { await device.device.takeScreenshot(outputFile); } finally { if (supportsServiceProtocol && isRunningDebug) { await setDebugBanner(true); } } final int sizeKB = outputFile.lengthSync() ~/ 1024; status.stop(); globals.printStatus( 'Screenshot written to ${globals.fs.path.relative(outputFile.path)} (${sizeKB}kB).', ); } on Exception catch (error) { status.cancel(); globals.printError('Error taking screenshot: $error'); } } Future<bool> debugTogglePlatform() async { if (!supportsServiceProtocol || !isRunningDebug) { return false; } final List<FlutterView> views = await flutterDevices .first .vmService.getFlutterViews(); final String isolateId = views.first.uiIsolate.id; final String from = await flutterDevices .first.vmService.flutterPlatformOverride( isolateId: isolateId, ); String to; for (final FlutterDevice device in flutterDevices) { to = await device.togglePlatform(from: from); } globals.printStatus('Switched operating system to $to'); return true; } 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() ).where((Future<void> element) => element != null) ); } @protected void cacheInitialDillCompilation() { if (_dillOutputPath != null) { return; } globals.logger.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<dynamic, dynamic> json = event.extensionData?.data; if (json != null && json.containsKey('renderedErrorText')) { globals.printStatus('\n${json['renderedErrorText']}'); } } } /// 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, }) async { if (!debuggingOptions.debuggingEnabled) { throw '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, disableDds: debuggingOptions.disableDds, ddsPort: debuggingOptions.ddsPort, hostVmServicePort: debuggingOptions.hostVmServicePort, getSkSLMethod: getSkSLMethod, printStructuredErrorLogMethod: printStructuredErrorLog, ipv6: ipv6, disableServiceAuthCodes: debuggingOptions.disableServiceAuthCodes ); // This will wait for at least one flutter view before returning. final Status status = globals.logger.startProgress( 'Waiting for ${device.device.name} to report its views...', ); try { await device.vmService.getFlutterViews(); } finally { status.stop(); } // 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.onDone.then<void>( _serviceProtocolDone, onError: _serviceProtocolError, ).whenComplete(_serviceDisconnected)); } } Future<bool> launchDevTools() async { if (!supportsServiceProtocol) { return false; } assert(supportsServiceProtocol); _devtoolsLauncher ??= DevtoolsLauncher.instance; await _devtoolsLauncher.launch(flutterDevices.first.vmService.httpAddress); return true; } Future<void> shutdownDevtools() async { await _devtoolsLauncher?.close(); _devtoolsLauncher = null; } Future<void> _serviceProtocolDone(dynamic object) async { globals.printTrace('Service protocol connection closed.'); } Future<void> _serviceProtocolError(dynamic 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); } 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; assert(exitCode != null); 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(); } /// Called to print help to the terminal. void printHelp({ @required bool details }); void printHelpDetails() { if (flutterDevices.any((FlutterDevice d) => d.device.supportsScreenshot)) { commandHelp.s.print(); } if (supportsServiceProtocol) { commandHelp.b.print(); commandHelp.w.print(); commandHelp.t.print(); if (isRunningDebug) { commandHelp.L.print(); commandHelp.S.print(); commandHelp.U.print(); commandHelp.i.print(); commandHelp.I.print(); commandHelp.p.print(); commandHelp.o.print(); commandHelp.z.print(); commandHelp.g.print(); } else { commandHelp.S.print(); commandHelp.U.print(); } if (supportsWriteSkSL) { commandHelp.M.print(); } commandHelp.v.print(); // `P` should precede `a` commandHelp.P.print(); commandHelp.a.print(); } } /// Called when a signal has requested we exit. Future<void> cleanupAfterSignal(); /// Called right before we exit. Future<void> cleanupAtFinish(); // Clears the screen. void clearScreen() => globals.logger.clear(); } class OperationResult { OperationResult(this.code, this.message, { this.fatal = false }); /// 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; /// Whether this error should cause the runner to exit. final bool fatal; bool get isOk => code == 0; static final OperationResult ok = OperationResult(0, ''); } /// Given the value of the --target option, return the path of the Dart file /// where the app's main function should be. String findMainDartFile([ String target ]) { target ??= ''; final String targetPath = globals.fs.path.absolute(target); if (globals.fs.isDirectorySync(targetPath)) { return globals.fs.path.join(targetPath, 'lib', 'main.dart'); } return targetPath; } 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.'; default: 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, }) : _logger = logger, _terminal = terminal, _signals = signals; final Logger _logger; final Terminal _terminal; final Signals _signals; final ResidentRunner residentRunner; bool _processingUserRequest = false; StreamSubscription<void> subscription; @visibleForTesting String lastReceivedCommand; 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) { return; } _addSignalHandler(io.ProcessSignal.SIGUSR1, _handleSignal); _addSignalHandler(io.ProcessSignal.SIGUSR2, _handleSignal); } /// Unregisters terminal signal and keystroke handlers. void stop() { assert(residentRunner.stayResident); 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': residentRunner.clearScreen(); 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 '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(fullRestart: false); 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.canHotRestart || !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) { if (device.device.supportsScreenshot) { 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': return residentRunner.launchDevTools(); case 'w': case 'W': return residentRunner.debugDumpApp(); case 'z': case 'Z': return residentRunner.debugToggleDebugCheckElevationsEnabled(); } 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; } } 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 }); // TODO(danrubel): the httpUri field should be removed as part of // https://github.com/flutter/flutter/issues/7050 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, FeatureFlags featureFlags) { switch (currentPlatform) { case 'android': return 'iOS'; case 'iOS': return 'fuchsia'; case 'fuchsia': if (featureFlags.isMacOSEnabled) { return 'macOS'; } return 'android'; case 'macOS': return 'android'; default: assert(false); // Invalid current platform. return 'android'; } } /// A launcher for the devtools debugger and analysis tool. abstract class DevtoolsLauncher { Future<void> launch(Uri observatoryAddress); Future<DevToolsServerAddress> serve(); Future<void> close(); static DevtoolsLauncher get instance => context.get<DevtoolsLauncher>(); } class DevToolsServerAddress { DevToolsServerAddress(this.host, this.port); final String host; final int port; }