// Copyright 2016 The Chromium 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 'application_package.dart'; import 'artifacts.dart'; import 'asset.dart'; import 'base/common.dart'; import 'base/file_system.dart'; import 'base/io.dart' as io; import 'base/logger.dart'; import 'base/signals.dart'; import 'base/terminal.dart'; import 'base/utils.dart'; import 'build_info.dart'; import 'codegen.dart'; import 'compile.dart'; import 'dart/package_map.dart'; import 'devfs.dart'; import 'device.dart'; import 'features.dart'; import 'globals.dart'; import 'project.dart'; import 'run_cold.dart'; import 'run_hot.dart'; import 'vmservice.dart'; class FlutterDevice { FlutterDevice( this.device, { @required this.trackWidgetCreation, this.fileSystemRoots, this.fileSystemScheme, this.viewFilter, TargetModel targetModel = TargetModel.flutter, TargetPlatform targetPlatform, List<String> experimentalFlags, ResidentCompiler generator, @required BuildMode buildMode, List<String> dartDefines, }) : assert(trackWidgetCreation != null), generator = generator ?? ResidentCompiler( artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, platform: targetPlatform, mode: buildMode, ), buildMode: buildMode, trackWidgetCreation: trackWidgetCreation, fileSystemRoots: fileSystemRoots, fileSystemScheme: fileSystemScheme, targetModel: targetModel, experimentalFlags: experimentalFlags, dartDefines: dartDefines, ); /// Create a [FlutterDevice] with optional code generation enabled. static Future<FlutterDevice> create( Device device, { @required FlutterProject flutterProject, @required bool trackWidgetCreation, @required String target, @required BuildMode buildMode, List<String> fileSystemRoots, String fileSystemScheme, String viewFilter, TargetModel targetModel = TargetModel.flutter, List<String> experimentalFlags, ResidentCompiler generator, List<String> dartDefines, }) async { ResidentCompiler generator; final TargetPlatform targetPlatform = await device.targetPlatform; if (device.platformType == PlatformType.fuchsia) { targetModel = TargetModel.flutterRunner; } if (featureFlags.isWebIncrementalCompilerEnabled && targetPlatform == TargetPlatform.web_javascript) { generator = ResidentCompiler( artifacts.getArtifactPath(Artifact.flutterWebSdk, mode: buildMode), buildMode: buildMode, trackWidgetCreation: trackWidgetCreation, fileSystemRoots: fileSystemRoots, fileSystemScheme: fileSystemScheme, targetModel: TargetModel.dartdevc, experimentalFlags: experimentalFlags, platformDill: artifacts.getArtifactPath(Artifact.webPlatformKernelDill, mode: buildMode), dartDefines: dartDefines, ); } else if (flutterProject.hasBuilders) { generator = await CodeGeneratingResidentCompiler.create( targetPlatform: targetPlatform, buildMode: buildMode, flutterProject: flutterProject, dartDefines: dartDefines, ); } else { generator = ResidentCompiler( artifacts.getArtifactPath( Artifact.flutterPatchedSdkPath, platform: targetPlatform, mode: buildMode, ), buildMode: buildMode, trackWidgetCreation: trackWidgetCreation, fileSystemRoots: fileSystemRoots, fileSystemScheme: fileSystemScheme, targetModel: targetModel, experimentalFlags: experimentalFlags, dartDefines: dartDefines, ); } return FlutterDevice( device, trackWidgetCreation: trackWidgetCreation, fileSystemRoots: fileSystemRoots, fileSystemScheme:fileSystemScheme, viewFilter: viewFilter, experimentalFlags: experimentalFlags, targetModel: targetModel, targetPlatform: targetPlatform, generator: generator, buildMode: buildMode, dartDefines: dartDefines, ); } final Device device; final ResidentCompiler generator; List<Uri> observatoryUris; List<VMService> vmServices; DevFS devFS; ApplicationPackage package; List<String> fileSystemRoots; String fileSystemScheme; StreamSubscription<String> _loggingSubscription; final String viewFilter; final bool trackWidgetCreation; /// 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, }) async { if (vmServices != null) { return; } final List<VMService> localVmServices = List<VMService>(observatoryUris.length); for (int i = 0; i < observatoryUris.length; i += 1) { printTrace('Connecting to service protocol: ${observatoryUris[i]}'); localVmServices[i] = await VMService.connect( observatoryUris[i], reloadSources: reloadSources, restart: restart, compileExpression: compileExpression, ); printTrace('Successfully connected to service protocol: ${observatoryUris[i]}'); } vmServices = localVmServices; device.getLogReader(app: package).connectedVMServices = vmServices; } Future<void> refreshViews() async { if (vmServices == null || vmServices.isEmpty) { return Future<void>.value(null); } final List<Future<void>> futures = <Future<void>>[ for (VMService service in vmServices) service.vm.refreshViews(waitForViews: true), ]; await Future.wait(futures); } List<FlutterView> get views { if (vmServices == null) { return <FlutterView>[]; } return vmServices .where((VMService service) => !service.isClosed) .expand<FlutterView>( (VMService service) { return viewFilter != null ? service.vm.allViewsWithName(viewFilter) : service.vm.views; }, ) .toList(); } Future<void> getVMs() async { for (VMService service in vmServices) { await service.getVM(); } } Future<void> exitApps() async { if (!device.supportsFlutterExit) { await device.stopApp(package); return; } final List<FlutterView> flutterViews = views; if (flutterViews == null || flutterViews.isEmpty) { return; } final List<Future<void>> futures = <Future<void>>[]; // 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. if (flutterViews.any((FlutterView view) { return view != null && view.uiIsolate != null && view.uiIsolate.pauseEvent.isPauseEvent; } )) { await device.stopApp(package); return; } for (FlutterView view in flutterViews) { if (view != null && view.uiIsolate != null) { assert(!view.uiIsolate.pauseEvent.isPauseEvent); futures.add(view.uiIsolate.flutterExit()); } } // The flutterExit message only returns if it fails, so just wait a few // seconds then assume it worked. // TODO(ianh): We should make this return once the VM service disconnects. await Future.wait(futures).timeout(const Duration(seconds: 2), onTimeout: () => <void>[]); } Future<Uri> setupDevFS( String fsName, Directory rootDirectory, { String packagesFilePath, }) { // One devFS per device. Shared by all running instances. devFS = DevFS( vmServices[0], fsName, rootDirectory, packagesFilePath: packagesFilePath, ); return devFS.create(); } List<Future<Map<String, dynamic>>> reloadSources( String entryPath, { bool pause = false, }) { final Uri deviceEntryUri = devFS.baseUri.resolveUri(fs.path.toUri(entryPath)); final Uri devicePackagesUri = devFS.baseUri.resolve('.packages'); return <Future<Map<String, dynamic>>>[ for (FlutterView view in views) view.uiIsolate.reloadSources( pause: pause, rootLibUri: deviceEntryUri, packagesUri: devicePackagesUri, ), ]; } Future<void> resetAssetDirectory() async { final Uri deviceAssetsDirectoryUri = devFS.baseUri.resolveUri( fs.path.toUri(getAssetBuildDirectory())); assert(deviceAssetsDirectoryUri != null); await Future.wait<void>(views.map<Future<void>>( (FlutterView view) => view.setAssetDirectory(deviceAssetsDirectoryUri) )); } Future<void> debugDumpApp() async { for (FlutterView view in views) { await view.uiIsolate.flutterDebugDumpApp(); } } Future<void> debugDumpRenderTree() async { for (FlutterView view in views) { await view.uiIsolate.flutterDebugDumpRenderTree(); } } Future<void> debugDumpLayerTree() async { for (FlutterView view in views) { await view.uiIsolate.flutterDebugDumpLayerTree(); } } Future<void> debugDumpSemanticsTreeInTraversalOrder() async { for (FlutterView view in views) { await view.uiIsolate.flutterDebugDumpSemanticsTreeInTraversalOrder(); } } Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async { for (FlutterView view in views) { await view.uiIsolate.flutterDebugDumpSemanticsTreeInInverseHitTestOrder(); } } Future<void> toggleDebugPaintSizeEnabled() async { for (FlutterView view in views) { await view.uiIsolate.flutterToggleDebugPaintSizeEnabled(); } } Future<void> toggleDebugCheckElevationsEnabled() async { for (FlutterView view in views) { await view.uiIsolate.flutterToggleDebugCheckElevationsEnabled(); } } Future<void> debugTogglePerformanceOverlayOverride() async { for (FlutterView view in views) { await view.uiIsolate.flutterTogglePerformanceOverlayOverride(); } } Future<void> toggleWidgetInspector() async { for (FlutterView view in views) { await view.uiIsolate.flutterToggleWidgetInspector(); } } Future<void> toggleProfileWidgetBuilds() async { for (FlutterView view in views) { await view.uiIsolate.flutterToggleProfileWidgetBuilds(); } } Future<String> togglePlatform({ String from }) async { String to; switch (from) { case 'iOS': to = 'android'; break; case 'android': default: to = 'iOS'; break; } for (FlutterView view in views) { await view.uiIsolate.flutterPlatformOverride(to); } return to; } void startEchoingDeviceLog() { if (_loggingSubscription != null) { return; } final Stream<String> logStream = device.getLogReader(app: package).logLines; if (logStream == null) { printError('Failed to read device log stream'); return; } _loggingSubscription = logStream.listen((String line) { if (!line.contains('Observatory listening on http')) { printStatus(line, wrap: false); } }); } Future<void> stopEchoingDeviceLog() async { if (_loggingSubscription == null) { return; } await _loggingSubscription.cancel(); _loggingSubscription = null; } void initLogReader() { device.getLogReader(app: package).appPid = vmServices.first.vm.pid; } Future<int> runHot({ HotRunner hotRunner, String route, }) async { final bool prebuiltMode = hotRunner.applicationBinary != null; final String modeName = hotRunner.debuggingOptions.buildInfo.friendlyModeName; printStatus('Launching ${getDisplayPath(hotRunner.mainPath)} on ${device.name} in $modeName mode...'); final TargetPlatform targetPlatform = await device.targetPlatform; package = await ApplicationPackageFactory.instance.getPackageForPlatform( targetPlatform, 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'; } printError(message); return 1; } final Map<String, dynamic> platformArgs = <String, dynamic>{}; 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, ); final LaunchResult result = await futureResult; if (!result.started) { printError('Error launching application on ${device.name}.'); await stopEchoingDeviceLog(); return 2; } if (result.hasObservatory) { observatoryUris = <Uri>[result.observatoryUri]; } else { observatoryUris = <Uri>[]; } return 0; } Future<int> runCold({ ColdRunner coldRunner, String route, }) async { final TargetPlatform targetPlatform = await device.targetPlatform; package = await ApplicationPackageFactory.instance.getPackageForPlatform( targetPlatform, applicationBinary: coldRunner.applicationBinary, ); final String modeName = coldRunner.debuggingOptions.buildInfo.friendlyModeName; final bool prebuiltMode = coldRunner.applicationBinary != null; if (coldRunner.mainPath == null) { assert(prebuiltMode); printStatus('Launching ${package.displayName} on ${device.name} in $modeName mode...'); } else { printStatus('Launching ${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'; } printError(message); return 1; } final Map<String, dynamic> platformArgs = <String, dynamic>{}; if (coldRunner.traceStartup != null) { platformArgs['trace-startup'] = coldRunner.traceStartup; } startEchoingDeviceLog(); final LaunchResult result = await device.startApp( package, mainPath: coldRunner.mainPath, debuggingOptions: coldRunner.debuggingOptions, platformArgs: platformArgs, route: route, prebuiltApplication: prebuiltMode, ipv6: coldRunner.ipv6, ); if (!result.started) { printError('Error running application on ${device.name}.'); await stopEchoingDeviceLog(); return 2; } if (result.hasObservatory) { observatoryUris = <Uri>[result.observatoryUri]; } else { observatoryUris = <Uri>[]; } return 0; } Future<UpdateFSReport> updateDevFS({ String mainPath, 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, }) async { final Status devFSStatus = logger.startProgress( 'Syncing files to device ${device.name}...', timeout: timeoutConfiguration.fastOperation, ); UpdateFSReport report; try { report = await devFS.update( mainPath: mainPath, target: target, bundle: bundle, firstBuildTime: firstBuildTime, bundleFirstUpload: bundleFirstUpload, generator: generator, fullRestart: fullRestart, dillOutputPath: dillOutputPath, trackWidgetCreation: trackWidgetCreation, projectRootPath: projectRootPath, pathToReload: pathToReload, invalidatedFiles: invalidatedFiles, ); } on DevFSException { devFSStatus.cancel(); return UpdateFSReport(success: false); } devFSStatus.stop(); printTrace('Synced ${getSizeAsMB(report.syncedBytes)}.'); return report; } Future<void> updateReloadStatus(bool wasReloadSuccessful) async { if (wasReloadSuccessful) { generator?.accept(); } else { await generator?.reject(); } } } // Issue: https://github.com/flutter/flutter/issues/33050 // Matches the following patterns: // HttpException: Connection closed before full header was received, uri = * // HttpException: , uri = * final RegExp kAndroidQHttpConnectionClosedExp = RegExp(r'^HttpException\:.+\, uri \=.+$'); /// Returns `true` if any of the devices is running Android Q. Future<bool> hasDeviceRunningAndroidQ(List<FlutterDevice> flutterDevices) async { for (FlutterDevice flutterDevice in flutterDevices) { final String sdkNameAndVersion = await flutterDevice.device.sdkNameAndVersion; if (sdkNameAndVersion != null && sdkNameAndVersion.startsWith('Android 10')) { return true; } } return false; } // Shared code between different resident application runners. abstract class ResidentRunner { ResidentRunner( this.flutterDevices, { this.target, this.debuggingOptions, String projectRootPath, String packagesFilePath, this.ipv6, this.stayResident = true, this.hotMode = true, String dillOutputPath, }) : mainPath = findMainDartFile(target), projectRootPath = projectRootPath ?? fs.currentDirectory.path, packagesFilePath = packagesFilePath ?? fs.path.absolute(PackageMap.globalPackagesPath), _dillOutputPath = dillOutputPath, artifactDirectory = dillOutputPath == null ? fs.systemTempDirectory.createTempSync('flutter_tool.') : fs.file(dillOutputPath).parent, assetBundle = AssetBundleFactory.instance.createBundle() { if (!artifactDirectory.existsSync()) { artifactDirectory.createSync(recursive: true); } // TODO(jonahwilliams): this is a temporary work around to regain some of // the initialize from dill performance. Longer term, we should have a // better way to determine where the appropriate dill file is, as this // doesn't work for Android or macOS builds.} if (dillOutputPath == null) { final File existingDill = fs.file(fs.path.join('build', 'app.dill')); if (existingDill.existsSync()) { existingDill.copySync(fs.path.join(artifactDirectory.path, 'app.dill')); } } } @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. @visibleForTesting final Directory artifactDirectory; final Completer<int> _finished = Completer<int>(); final String packagesFilePath; final String projectRootPath; final String mainPath; final AssetBundle assetBundle; bool _exited = false; bool hotMode ; String get dillOutputPath => _dillOutputPath ?? fs.path.join(artifactDirectory.path, 'app.dill'); String getReloadPath({ bool fullRestart }) => mainPath + (fullRestart ? '' : '.incremental') + '.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; /// 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, }) { return flutterDevices.first.views.first.uiIsolate .invokeFlutterExtensionRpcRaw(method, params: params); } /// 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 pauseAfterRestart = false, String reason }) { final String mode = isRunningProfile ? 'profile' : isRunningRelease ? 'release' : 'this'; throw '${fullRestart ? 'Restart' : 'Reload'} is not supported in $mode mode'; } @protected void writeVmserviceFile() { if (debuggingOptions.vmserviceOutFile != null) { try { final String address = flutterDevices.first.vmServices.first.wsAddress.toString(); final File vmserviceOutFile = fs.file(debuggingOptions.vmserviceOutFile); vmserviceOutFile.createSync(recursive: true); vmserviceOutFile.writeAsStringSync(address); } on FileSystemException { printError('Failed to write vmservice-out-file at ${debuggingOptions.vmserviceOutFile}'); } } } Future<void> exit() async { _exited = true; await stopEchoingDeviceLog(); await preExit(); await exitApp(); } Future<void> detach() async { await stopEchoingDeviceLog(); await preExit(); appFinished(); } Future<void> refreshViews() async { final List<Future<void>> futures = <Future<void>>[ for (FlutterDevice device in flutterDevices) device.refreshViews(), ]; await Future.wait(futures); } Future<void> debugDumpApp() async { await refreshViews(); for (FlutterDevice device in flutterDevices) { await device.debugDumpApp(); } } Future<void> debugDumpRenderTree() async { await refreshViews(); for (FlutterDevice device in flutterDevices) { await device.debugDumpRenderTree(); } } Future<void> debugDumpLayerTree() async { await refreshViews(); for (FlutterDevice device in flutterDevices) { await device.debugDumpLayerTree(); } } Future<void> debugDumpSemanticsTreeInTraversalOrder() async { await refreshViews(); for (FlutterDevice device in flutterDevices) { await device.debugDumpSemanticsTreeInTraversalOrder(); } } Future<void> debugDumpSemanticsTreeInInverseHitTestOrder() async { await refreshViews(); for (FlutterDevice device in flutterDevices) { await device.debugDumpSemanticsTreeInInverseHitTestOrder(); } } Future<void> debugToggleDebugPaintSizeEnabled() async { await refreshViews(); for (FlutterDevice device in flutterDevices) { await device.toggleDebugPaintSizeEnabled(); } } Future<void> debugToggleDebugCheckElevationsEnabled() async { await refreshViews(); for (FlutterDevice device in flutterDevices) { await device.toggleDebugCheckElevationsEnabled(); } } Future<void> debugTogglePerformanceOverlayOverride() async { await refreshViews(); for (FlutterDevice device in flutterDevices) { await device.debugTogglePerformanceOverlayOverride(); } } Future<void> debugToggleWidgetInspector() async { await refreshViews(); for (FlutterDevice device in flutterDevices) { await device.toggleWidgetInspector(); } } Future<void> debugToggleProfileWidgetBuilds() async { await refreshViews(); for (FlutterDevice device in flutterDevices) { await device.toggleProfileWidgetBuilds(); } } /// 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 [Devce.supportsScreenshot] is not true. Future<void> screenshot(FlutterDevice device) async { assert(device.device.supportsScreenshot); final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...', timeout: timeoutConfiguration.fastOperation); final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png'); try { if (supportsServiceProtocol && isRunningDebug) { await device.refreshViews(); try { for (FlutterView view in device.views) { await view.uiIsolate.flutterDebugAllowBanner(false); } } catch (error) { status.cancel(); printError('Error communicating with Flutter on the device: $error'); return; } } try { await device.device.takeScreenshot(outputFile); } finally { if (supportsServiceProtocol && isRunningDebug) { try { for (FlutterView view in device.views) { await view.uiIsolate.flutterDebugAllowBanner(true); } } catch (error) { status.cancel(); printError('Error communicating with Flutter on the device: $error'); return; } } } final int sizeKB = outputFile.lengthSync() ~/ 1024; status.stop(); printStatus('Screenshot written to ${fs.path.relative(outputFile.path)} (${sizeKB}kB).'); } catch (error) { status.cancel(); printError('Error taking screenshot: $error'); } } Future<void> debugTogglePlatform() async { await refreshViews(); final String from = await flutterDevices[0].views[0].uiIsolate.flutterPlatformOverride(); String to; for (FlutterDevice device in flutterDevices) { to = await device.togglePlatform(from: from); } printStatus('Switched operating system to $to'); } Future<void> stopEchoingDeviceLog() async { await Future.wait<void>( flutterDevices.map<Future<void>>((FlutterDevice device) => device.stopEchoingDeviceLog()) ); } /// 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, }) async { if (!debuggingOptions.debuggingEnabled) { throw 'The service protocol is not enabled.'; } bool viewFound = false; for (FlutterDevice device in flutterDevices) { await device.connect( reloadSources: reloadSources, restart: restart, compileExpression: compileExpression, ); await device.getVMs(); await device.refreshViews(); if (device.views.isNotEmpty) { viewFound = true; } } if (!viewFound) { if (flutterDevices.length == 1) { throw 'No Flutter view is available on ${flutterDevices.first.device.name}.'; } throw 'No Flutter view is available on any device ' '(${flutterDevices.map<String>((FlutterDevice device) => device.device.name).join(', ')}).'; } // Listen for service protocol connection to close. for (FlutterDevice device in flutterDevices) { for (VMService service in device.vmServices) { // 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(service.done.then<void>( _serviceProtocolDone, onError: _serviceProtocolError, ).whenComplete(_serviceDisconnected)); } } } Future<void> _serviceProtocolDone(dynamic object) { printTrace('Service protocol connection closed.'); return Future<void>.value(object); } Future<void> _serviceProtocolError(dynamic error, StackTrace stack) { 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; } printStatus('Lost connection to device.'); _finished.complete(0); } void appFinished() { if (_finished.isCompleted) { return; } printStatus('Application finished.'); _finished.complete(0); } 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, we 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 (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 (supportsServiceProtocol) { printStatus('You can dump the widget hierarchy of the app (debugDumpApp) by pressing "w".'); printStatus('To dump the rendering tree of the app (debugDumpRenderTree), press "t".'); if (isRunningDebug) { printStatus('For layers (debugDumpLayerTree), use "L"; for accessibility (debugDumpSemantics), use "S" (for traversal order) or "U" (for inverse hit test order).'); printStatus('To toggle the widget inspector (WidgetsApp.showWidgetInspectorOverride), press "i".'); printStatus('To toggle the display of construction lines (debugPaintSizeEnabled), press "p".'); printStatus('To simulate different operating systems, (defaultTargetPlatform), press "o".'); printStatus('To toggle the elevation checker, press "z".'); } else { printStatus('To dump the accessibility tree (debugDumpSemantics), press "S" (for traversal order) or "U" (for inverse hit test order).'); } printStatus('To display the performance overlay (WidgetsApp.showPerformanceOverlay), press "P".'); printStatus('To enable timeline events for all widget build methods, (debugProfileWidgetBuilds), press "a"'); } if (flutterDevices.any((FlutterDevice d) => d.device.supportsScreenshot)) { printStatus('To save a screenshot to flutter.png, press "s".'); } } /// Called when a signal has requested we exit. Future<void> cleanupAfterSignal(); /// Called right before we exit. Future<void> cleanupAtFinish(); } 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 = fs.path.absolute(target); if (fs.isDirectorySync(targetPath)) { return 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 = 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); final ResidentRunner residentRunner; bool _processingUserRequest = false; StreamSubscription<void> subscription; @visibleForTesting String lastReceivedCommand; void setupTerminal() { if (!logger.quiet) { printStatus(''); residentRunner.printHelp(details: false); } terminal.singleCharMode = true; subscription = terminal.keystrokes.listen(processTerminalInput); } void registerSignalHandlers() { assert(residentRunner.stayResident); signals.addHandler(io.ProcessSignal.SIGINT, _cleanUp); signals.addHandler(io.ProcessSignal.SIGTERM, _cleanUp); if (!residentRunner.supportsServiceProtocol || !residentRunner.supportsRestart) { return; } signals.addHandler(io.ProcessSignal.SIGUSR1, _handleSignal); signals.addHandler(io.ProcessSignal.SIGUSR2, _handleSignal); } /// Returns [true] if the input has been handled by this function. Future<bool> _commonTerminalInputHandler(String character) async { printStatus(''); // the key the user tapped might be on this line switch(character) { case 'a': if (residentRunner.supportsServiceProtocol) { await residentRunner.debugToggleProfileWidgetBuilds(); return true; } return false; case 'd': case 'D': await residentRunner.detach(); return true; case 'h': case 'H': case '?': // help residentRunner.printHelp(details: true); return true; case 'i': case 'I': if (residentRunner.supportsServiceProtocol) { await residentRunner.debugToggleWidgetInspector(); return true; } return false; case 'l': final List<FlutterView> views = residentRunner.flutterDevices .expand((FlutterDevice d) => d.views).toList(); printStatus('Connected ${pluralize('view', views.length)}:'); for (FlutterView v in views) { printStatus('${v.uiIsolate.name} (${v.uiIsolate.id})', indent: 2); } return true; case 'L': if (residentRunner.supportsServiceProtocol) { await residentRunner.debugDumpLayerTree(); return true; } return false; case 'o': case 'O': if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) { await residentRunner.debugTogglePlatform(); return true; } return false; case 'p': if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) { await residentRunner.debugToggleDebugPaintSizeEnabled(); return true; } return false; case 'P': if (residentRunner.supportsServiceProtocol) { await residentRunner.debugTogglePerformanceOverlayOverride(); return true; } return false; case 'q': case 'Q': // exit await residentRunner.exit(); return true; case 's': for (FlutterDevice device in residentRunner.flutterDevices) { if (device.device.supportsScreenshot) { await residentRunner.screenshot(device); } } 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) { 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) { printStatus('Try again after fixing the above error(s).', emphasis: true); } return true; case 'S': if (residentRunner.supportsServiceProtocol) { await residentRunner.debugDumpSemanticsTreeInTraversalOrder(); return true; } return false; case 't': case 'T': if (residentRunner.supportsServiceProtocol) { await residentRunner.debugDumpRenderTree(); return true; } return false; case 'U': if (residentRunner.supportsServiceProtocol) { await residentRunner.debugDumpSemanticsTreeInInverseHitTestOrder(); return true; } return false; case 'w': case 'W': if (residentRunner.supportsServiceProtocol) { await residentRunner.debugDumpApp(); return true; } return false; case 'z': case 'Z': await residentRunner.debugToggleDebugCheckElevationsEnabled(); return true; } 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) { printTrace('Ignoring terminal input: "$command" because we are busy.'); return; } _processingUserRequest = true; try { lastReceivedCommand = command; await _commonTerminalInputHandler(command); } catch (error, st) { // Don't print stack traces for known error types. if (error is! ToolExit) { printError('$error\n$st'); } await _cleanUp(null); rethrow; } finally { _processingUserRequest = false; } } Future<void> _handleSignal(io.ProcessSignal signal) async { if (_processingUserRequest) { 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; }