Split hot run mode into a separate file (#5233)
diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart index 822f69d..af15760 100644 --- a/packages/flutter_tools/lib/src/asset.dart +++ b/packages/flutter_tools/lib/src/asset.dart
@@ -52,10 +52,27 @@ static const String _kFontSetMaterial = 'material'; static const String _kFontSetRoboto = 'roboto'; + DateTime _lastBuildTimestamp; + + bool needsBuild({String manifestPath: defaultManifestPath}) { + if (_lastBuildTimestamp == null) + return true; + + FileStat stat = new File(manifestPath).statSync(); + if (stat.type == FileSystemEntityType.NOT_FOUND) + return true; + + return stat.modified.isAfter(_lastBuildTimestamp); + } + Future<int> build({String manifestPath: defaultManifestPath, String workingDirPath: defaultWorkingDirPath, bool includeRobotoFonts: true}) async { Object manifest = _loadFlutterYamlManifest(manifestPath); + if (manifest == null) { + // No manifest file found for this application. + return 0; + } if (manifest != null) { int result = await _validateFlutterYamlManifest(manifest); if (result != 0) @@ -65,6 +82,8 @@ assert(manifestDescriptor != null); String assetBasePath = path.dirname(path.absolute(manifestPath)); + _lastBuildTimestamp = new DateTime.now(); + final PackageMap packageMap = new PackageMap(path.join(assetBasePath, '.packages'));
diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart index ce3656a..db20a97 100644 --- a/packages/flutter_tools/lib/src/commands/build_aot.dart +++ b/packages/flutter_tools/lib/src/commands/build_aot.dart
@@ -13,7 +13,7 @@ import '../build_info.dart'; import '../dart/package_map.dart'; import '../globals.dart'; -import '../run.dart'; +import '../resident_runner.dart'; import 'build.dart'; const String _kDefaultAotOutputDir = 'build/aot';
diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart index df7ea5c..539d5a4 100644 --- a/packages/flutter_tools/lib/src/commands/build_apk.dart +++ b/packages/flutter_tools/lib/src/commands/build_apk.dart
@@ -18,7 +18,7 @@ import '../build_info.dart'; import '../flx.dart' as flx; import '../globals.dart'; -import '../run.dart'; +import '../resident_runner.dart'; import '../services.dart'; import 'build_aot.dart'; import 'build.dart';
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 0486ec3..cb49fc9 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -291,7 +291,7 @@ String route = _getStringArg(args, 'route'); String mode = _getStringArg(args, 'mode'); String target = _getStringArg(args, 'target'); - bool hotMode = _getBoolArg(args, 'hot'); + // TODO(johnmccutchan): Wire up support for hot mode. Device device = daemon.deviceDomain._getDevice(deviceId); if (device == null) @@ -323,8 +323,7 @@ device, target: target, debuggingOptions: options, - usesTerminalUI: false, - hotMode: hotMode + usesTerminalUI: false ); AppInstance app = new AppInstance(_getNextAppId(), runner);
diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index abe4c07..213e9f1 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart
@@ -21,7 +21,7 @@ import '../device.dart'; import '../globals.dart'; import '../ios/simulators.dart' show SimControl, IOSSimulatorUtils; -import '../run.dart'; +import '../resident_runner.dart'; import 'build_apk.dart' as build_apk; import 'run.dart';
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index cd14ae2..0e38310 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -12,7 +12,9 @@ import '../cache.dart'; import '../device.dart'; import '../globals.dart'; +import '../hot.dart' as hot; import '../observatory.dart'; +import '../resident_runner.dart'; import '../run.dart'; import '../runner/flutter_command.dart'; import 'build_apk.dart'; @@ -132,18 +134,25 @@ } if (argResults['resident']) { - RunAndStayResident runner = new RunAndStayResident( - deviceForCommand, - target: targetFile, - debuggingOptions: options, - hotMode: argResults['hot'] - ); - - return runner.run( - traceStartup: traceStartup, - benchmark: argResults['benchmark'], - route: route - ); + if (argResults['hot']) { + hot.HotRunner runner = new hot.HotRunner( + deviceForCommand, + target: targetFile, + debuggingOptions: options + ); + return runner.run(route: route); + } else { + RunAndStayResident runner = new RunAndStayResident( + deviceForCommand, + target: targetFile, + debuggingOptions: options + ); + return runner.run( + traceStartup: traceStartup, + benchmark: argResults['benchmark'], + route: route + ); + } } else { // TODO(devoncarew): Remove this path and support the `--no-resident` option // using the `RunAndStayResident` class.
diff --git a/packages/flutter_tools/lib/src/commands/run_mojo.dart b/packages/flutter_tools/lib/src/commands/run_mojo.dart index eb7f6a2..5e5bdad 100644 --- a/packages/flutter_tools/lib/src/commands/run_mojo.dart +++ b/packages/flutter_tools/lib/src/commands/run_mojo.dart
@@ -12,7 +12,7 @@ import '../cache.dart'; import '../flx.dart' as flx; import '../globals.dart'; -import '../run.dart'; +import '../resident_runner.dart'; import '../runner/flutter_command.dart'; const String _kDefaultBundlePath = 'build/app.flx';
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart index 2d4e4f0..cdae4d0 100644 --- a/packages/flutter_tools/lib/src/devfs.dart +++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -180,7 +180,9 @@ return await _operations.destroy(fsName); } - Future<dynamic> update({ DevFSProgressReporter progressReporter, AssetBundle bundle }) async { + Future<dynamic> update({ DevFSProgressReporter progressReporter, + AssetBundle bundle, + bool bundleDirty: false }) async { _bytes = 0; // Mark all entries as not seen. _entries.forEach((String path, DevFSEntry entry) { @@ -189,7 +191,8 @@ printTrace('DevFS: Starting sync from $rootDirectory'); // Send the root and lib directories. Directory directory = rootDirectory; - _syncDirectory(directory, recursive: true); + await _syncDirectory(directory, recursive: true); + printTrace('DevFS: Syncing of $rootDirectory finished'); String packagesFilePath = path.join(rootDirectory.path, kPackagesFileName); StringBuffer sb; // Send the packages. @@ -202,12 +205,16 @@ if (uri.toString() == 'lib/') continue; Directory directory = new Directory.fromUri(uri); - if (_syncDirectory(directory, - directoryName: 'packages/$packageName', - recursive: true)) { + printTrace('DevFS: Syncing package $packageName started'); + bool packageWritten = + await _syncDirectory(directory, + directoryName: 'packages/$packageName', + recursive: true); + if (packageWritten) { sb ??= new StringBuffer(); sb.writeln('$packageName:packages/$packageName'); } + printTrace('DevFS: Syncing package $packageName finished'); } } if (bundle != null) { @@ -216,6 +223,11 @@ // We write the assets into 'build/flx' so that they are in the // same location in DevFS and the iOS simulator. final String devicePath = path.join('build/flx', entry.archivePath); + if (!bundleDirty && entry.isStringEntry) { + // When the bundle isn't dirty, we do not need to sync string + // entries. + continue; + } _syncBundleEntry(devicePath, entry); } } @@ -309,10 +321,10 @@ return false; } - bool _syncDirectory(Directory directory, - {String directoryName, - bool recursive: false, - bool ignoreDotFiles: true}) { + Future<bool> _syncDirectory(Directory directory, + {String directoryName, + bool recursive: false, + bool ignoreDotFiles: true}) async { String prefix = directoryName; if (prefix == null) { prefix = path.relative(directory.path, from: rootDirectory.path); @@ -320,9 +332,9 @@ prefix = ''; } try { - List<FileSystemEntity> files = - directory.listSync(recursive: recursive, followLinks: false); - for (FileSystemEntity file in files) { + Stream<FileSystemEntity> files = + directory.list(recursive: recursive, followLinks: false); + await for (FileSystemEntity file in files) { if (file is! File) { // Skip non-files. continue;
diff --git a/packages/flutter_tools/lib/src/hot.dart b/packages/flutter_tools/lib/src/hot.dart new file mode 100644 index 0000000..ba7f288 --- /dev/null +++ b/packages/flutter_tools/lib/src/hot.dart
@@ -0,0 +1,351 @@ +// 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 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'application_package.dart'; +import 'asset.dart'; +import 'base/logger.dart'; +import 'base/utils.dart'; +import 'cache.dart'; +import 'commands/build_apk.dart'; +import 'commands/install.dart'; +import 'device.dart'; +import 'globals.dart'; +import 'devfs.dart'; +import 'observatory.dart'; +import 'resident_runner.dart'; + +String getDevFSLoaderScript() { + return path.absolute(path.join(Cache.flutterRoot, + 'packages', + 'flutter', + 'bin', + 'loader', + 'loader_app.dart')); +} + +class HotRunner extends ResidentRunner { + HotRunner( + Device device, { + String target, + DebuggingOptions debuggingOptions, + bool usesTerminalUI: true + }) : super(device, + target: target, + debuggingOptions: debuggingOptions, + usesTerminalUI: usesTerminalUI); + + ApplicationPackage _package; + String _mainPath; + final AssetBundle bundle = new AssetBundle(); + + /// Start the app and keep the process running during its lifetime. + Future<int> run({ + Completer<int> observatoryPortCompleter, + String route + }) { + // Don't let uncaught errors kill the process. + return runZoned(() { + return _run( + observatoryPortCompleter: observatoryPortCompleter, + route: route + ); + }, onError: (dynamic error, StackTrace stackTrace) { + printError('Exception from flutter run: $error', stackTrace); + }); + } + + Future<int> _run({ + Completer<int> observatoryPortCompleter, + String route + }) async { + _mainPath = findMainDartFile(target); + if (!FileSystemEntity.isFileSync(_mainPath)) { + String message = 'Tried to run $_mainPath, but that file does not exist.'; + if (target == null) + message += '\nConsider using the -t option to specify the Dart file to start.'; + printError(message); + return 1; + } + + _package = getApplicationPackageForPlatform(device.platform); + + if (_package == null) { + String message = 'No application found for ${device.platform}.'; + String hint = getMissingPackageHintForPlatform(device.platform); + if (hint != null) + message += '\n$hint'; + printError(message); + return 1; + } + + // TODO(devoncarew): We shouldn't have to do type checks here. + if (device is AndroidDevice) { + printTrace('Running build command.'); + + int result = await buildApk( + device.platform, + target: target, + buildMode: debuggingOptions.buildMode + ); + + if (result != 0) + return result; + } + + // TODO(devoncarew): Move this into the device.startApp() impls. + if (_package != null) { + printTrace("Stopping app '${_package.name}' on ${device.name}."); + // We don't wait for the stop command to complete. + device.stopApp(_package); + } + + // Allow any stop commands from above to start work. + await new Future<Duration>.delayed(Duration.ZERO); + + // TODO(devoncarew): This fails for ios devices - we haven't built yet. + if (device is AndroidDevice) { + printTrace('Running install command.'); + if (!(installApp(device, _package, uninstall: false))) + return 1; + } + + Map<String, dynamic> platformArgs = new Map<String, dynamic>(); + + await startEchoingDeviceLog(); + + if (device.needsDevFS) { + printStatus('Launching loader on ${device.name}...'); + } else { + printStatus('Launching ${getDisplayPath(_mainPath)} on ${device.name}...'); + } + + LaunchResult result = await device.startApp( + _package, + debuggingOptions.buildMode, + mainPath: device.needsDevFS ? getDevFSLoaderScript() : _mainPath, + debuggingOptions: debuggingOptions, + platformArgs: platformArgs, + route: route + ); + + if (!result.started) { + if (device.needsDevFS) { + printError('Error launching DevFS loader on ${device.name}.'); + } else { + printError('Error launching ${getDisplayPath(_mainPath)} on ${device.name}.'); + } + await stopEchoingDeviceLog(); + return 2; + } + + if (observatoryPortCompleter != null && result.hasObservatory) + observatoryPortCompleter.complete(result.observatoryPort); + + await connectToServiceProtocol(result.observatoryPort); + + if (device.needsDevFS) { + _loaderShowMessage('Connecting...', progress: 0); + bool result = await _updateDevFS( + progressReporter: (int progress, int max) { + _loaderShowMessage('Syncing files to device...', progress: progress, max: max); + } + ); + if (!result) { + _loaderShowMessage('Failed.'); + printError('Could not perform initial file synchronization.'); + return 3; + } + printStatus('Running ${getDisplayPath(_mainPath)} on ${device.name}...'); + _loaderShowMessage('Launching...'); + await _launchFromDevFS(_package, _mainPath); + } + + printStatus('Application running.'); + + setupTerminal(); + + registerSignalHandlers(); + + return await waitForAppToFinish(); + } + + @override + void handleTerminalCommand(String code) { + final String lower = code.toLowerCase(); + if (lower == 'r' || code == AnsiTerminal.KEY_F5) { + // F5, restart + if (code == 'r') { + // lower-case 'r' + _reloadSources(); + } else { + // upper-case 'R'. + _restartFromSources(); + } + } + } + + void _loaderShowMessage(String message, { int progress, int max }) { + serviceProtocol.flutterLoaderShowMessage(serviceProtocol.firstIsolateId, message); + if (progress != null) { + serviceProtocol.flutterLoaderSetProgress(serviceProtocol.firstIsolateId, progress.toDouble()); + serviceProtocol.flutterLoaderSetProgressMax(serviceProtocol.firstIsolateId, max?.toDouble() ?? 0.0); + } else { + serviceProtocol.flutterLoaderSetProgress(serviceProtocol.firstIsolateId, 0.0); + serviceProtocol.flutterLoaderSetProgressMax(serviceProtocol.firstIsolateId, -1.0); + } + } + + DevFS _devFS; + String _devFSProjectRootPath; + Future<bool> _updateDevFS({ DevFSProgressReporter progressReporter }) async { + if (_devFS == null) { + Directory directory = Directory.current; + _devFSProjectRootPath = directory.path; + String fsName = path.basename(directory.path); + _devFS = new DevFS(serviceProtocol, fsName, directory); + + try { + await _devFS.create(); + } catch (error) { + _devFS = null; + printError('Error initializing DevFS: $error'); + return false; + } + } + + final bool rebuildBundle = bundle.needsBuild(); + if (rebuildBundle) { + Status bundleStatus = logger.startProgress('Updating assets...'); + await bundle.build(); + bundleStatus.stop(showElapsedTime: true); + } + Status devFSStatus = logger.startProgress('Syncing files on device...'); + await _devFS.update(progressReporter: progressReporter, + bundle: bundle, + bundleDirty: rebuildBundle); + devFSStatus.stop(showElapsedTime: true); + printStatus('Synced ${getSizeAsMB(_devFS.bytes)} MB'); + return true; + } + + Future<Null> _cleanupDevFS() async { + if (_devFS != null) { + // Cleanup the devFS. + await _devFS.destroy(); + } + _devFS = null; + } + + Future<Null> _launchFromDevFS(ApplicationPackage package, + String mainScript) async { + String entryPath = path.relative(mainScript, from: _devFSProjectRootPath); + String deviceEntryPath = + _devFS.baseUri.resolve(entryPath).toFilePath(); + String devicePackagesPath = + _devFS.baseUri.resolve('.packages').toFilePath(); + String deviceAssetsDirectoryPath = + _devFS.baseUri.resolve('build/flx').toFilePath(); + String viewId = await serviceProtocol.getFirstViewId(); + + // When this completer completes the isolate is running. + // TODO(johnmccutchan): Have the framework send an event after the first + // frame is rendered and use that instead of 'runnable'. + Completer<Null> completer = new Completer<Null>(); + StreamSubscription<Event> subscription = + serviceProtocol.onIsolateEvent.listen((Event event) { + if (event.kind == 'IsolateStart') { + printTrace('Isolate is spawned.'); + } else if (event.kind == 'IsolateRunnable') { + printTrace('Isolate is runnable.'); + completer.complete(null); + } + }); + await serviceProtocol.runInView(viewId, + deviceEntryPath, + devicePackagesPath, + deviceAssetsDirectoryPath); + await completer.future; + await subscription.cancel(); + } + + Future<Null> _restartFromSources() async { + if (_devFS != null) + await _updateDevFS(); + Status restartStatus = logger.startProgress('Restarting application...'); + await _launchFromDevFS(_package, _mainPath); + restartStatus.stop(showElapsedTime: true); + } + + /// Returns [true] if the reload was successful. + bool _printReloadReport(Map<String, dynamic> reloadReport) { + if (!reloadReport['success']) { + printError('Hot reload was rejected:'); + for (Map<String, dynamic> notice in reloadReport['details']['notices']) { + printError('${notice['message']}'); + } + return false; + } + int loadedLibraryCount = reloadReport['details']['loadedLibraryCount']; + int finalLibraryCount = reloadReport['details']['finalLibraryCount']; + printStatus('Reloaded $loadedLibraryCount out of $finalLibraryCount libraries.'); + return true; + } + + Future<bool> _reloadSources() async { + if (serviceProtocol.firstIsolateId == null) + throw 'Application isolate not found'; + if (_devFS != null) + await _updateDevFS(); + Status reloadStatus = logger.startProgress('Performing hot reload'); + try { + Map<String, dynamic> reloadReport = + await serviceProtocol.reloadSources(serviceProtocol.firstIsolateId); + reloadStatus.stop(showElapsedTime: true); + if (!_printReloadReport(reloadReport)) { + // Reload failed. + return false; + } + } catch (errorMessage) { + reloadStatus.stop(showElapsedTime: true); + printError('Hot reload failed:\n$errorMessage'); + return false; + } + Status reassembleStatus = + logger.startProgress('Reassembling application'); + try { + await serviceProtocol.flutterReassemble(serviceProtocol.firstIsolateId); + } catch (_) { + reassembleStatus.stop(showElapsedTime: true); + printError('Reassembling application failed.'); + return false; + } + reassembleStatus.stop(showElapsedTime: true); + return true; + } + + @override + void printHelp() { + printStatus('Type "h" or F1 for this help message. Type "q", F10, or ctrl-c to quit.', emphasis: true); + printStatus('Type "r" or F5 to perform a hot reload of the app.', emphasis: true); + printStatus('Type "R" to restart the app', emphasis: true); + printStatus('Type "w" to print the widget hierarchy of the app, and "t" for the render tree.', emphasis: true); + } + + @override + Future<Null> cleanupAfterSignal() async { + await stopEchoingDeviceLog(); + await stopApp(); + } + + @override + Future<Null> cleanupAtFinish() async { + await _cleanupDevFS(); + await stopEchoingDeviceLog(); + } +}
diff --git a/packages/flutter_tools/lib/src/observatory.dart b/packages/flutter_tools/lib/src/observatory.dart index b7d3337..f13d096 100644 --- a/packages/flutter_tools/lib/src/observatory.dart +++ b/packages/flutter_tools/lib/src/observatory.dart
@@ -116,16 +116,37 @@ }); } - Future<Null> reloadSources(String isolateId) async { + Future<Map<String, dynamic>> reloadSources(String isolateId) async { try { - await sendRequest('_reloadSources', - <String, dynamic>{ 'isolateId': isolateId }); - return null; + Response response = + await sendRequest('_reloadSources', + <String, dynamic>{ 'isolateId': isolateId }); + return response.response; } catch (e) { - return new Future<Null>.error(e.data['details']); + return new Future<Map<String, dynamic>>.error(e.data['details']); } } + Future<String> getFirstViewId() async { + Map<String, dynamic> response = await peer.sendRequest('_flutter.listViews'); + List<Map<String, String>> views = response['views']; + return views[0]['id']; + } + + Future<Null> runInView(String viewId, + String main, + String packages, + String assetsDirectory) async { + await peer.sendRequest('_flutter.runInView', + <String, dynamic> { + 'viewId': viewId, + 'mainScript': main, + 'packagesFile': packages, + 'assetDirectory': assetsDirectory + }); + return null; + } + Future<Response> clearVMTimeline() => sendRequest('_clearVMTimeline'); Future<Response> setVMTimelineFlags(List<String> recordedStreams) {
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart new file mode 100644 index 0000000..d8c2fde --- /dev/null +++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -0,0 +1,197 @@ +// 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 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'base/logger.dart'; +import 'build_info.dart'; +import 'device.dart'; +import 'globals.dart'; +import 'observatory.dart'; + +// Shared code between different resident application runners. +abstract class ResidentRunner { + ResidentRunner(this.device, { + this.target, + this.debuggingOptions, + this.usesTerminalUI: true + }); + + final Device device; + final String target; + final DebuggingOptions debuggingOptions; + final bool usesTerminalUI; + final Completer<int> _finished = new Completer<int>(); + + Observatory serviceProtocol; + StreamSubscription<String> _loggingSubscription; + + Future<Null> stop() async { + await stopEchoingDeviceLog(); + return stopApp(); + } + + void _debugDumpApp() { + serviceProtocol.flutterDebugDumpApp(serviceProtocol.firstIsolateId); + } + + void _debugDumpRenderTree() { + serviceProtocol.flutterDebugDumpRenderTree(serviceProtocol.firstIsolateId); + } + + void registerSignalHandlers() { + ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) async { + _resetTerminal(); + await cleanupAfterSignal(); + exit(0); + }); + ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) async { + _resetTerminal(); + await cleanupAfterSignal(); + exit(0); + }); + } + + Future<Null> startEchoingDeviceLog() async { + if (_loggingSubscription != null) { + return; + } + _loggingSubscription = device.logReader.logLines.listen((String line) { + if (!line.contains('Observatory listening on http') && + !line.contains('Diagnostic server listening on http')) + printStatus(line); + }); + } + + Future<Null> stopEchoingDeviceLog() async { + if (_loggingSubscription != null) { + await _loggingSubscription.cancel(); + } + _loggingSubscription = null; + } + + Future<Null> connectToServiceProtocol(int port) async { + if (!debuggingOptions.debuggingEnabled) { + return new Future<Null>.error('Error the service protocol is not enabled.'); + } + serviceProtocol = await Observatory.connect(port); + printTrace('Connected to service protocol on port $port'); + serviceProtocol.populateIsolateInfo(); + serviceProtocol.onExtensionEvent.listen((Event event) { + printTrace(event.toString()); + }); + serviceProtocol.onIsolateEvent.listen((Event event) { + printTrace(event.toString()); + }); + // Listen for service protocol connection to close. + serviceProtocol.done.whenComplete(() { + appFinished(); + }); + } + + /// Returns [true] if the input has been handled by this function. + bool _commonTerminalInputHandler(String character) { + final String lower = character.toLowerCase(); + + printStatus(''); // the key the user tapped might be on this line + + if (lower == 'h' || lower == '?' || character == AnsiTerminal.KEY_F1) { + // F1, help + printHelp(); + return true; + } else if (lower == 'w') { + _debugDumpApp(); + return true; + } else if (lower == 't') { + _debugDumpRenderTree(); + return true; + } else if (lower == 'q' || character == AnsiTerminal.KEY_F10) { + // F10, exit + stopApp(); + return true; + } + + return false; + } + + void appFinished() { + if (_finished.isCompleted) + return; + printStatus('Application finished.'); + _resetTerminal(); + _finished.complete(0); + } + + void _resetTerminal() { + if (usesTerminalUI) + terminal.singleCharMode = false; + } + + void setupTerminal() { + if (usesTerminalUI) { + if (!logger.quiet) + printHelp(); + + terminal.singleCharMode = true; + terminal.onCharInput.listen((String code) { + if (!_commonTerminalInputHandler(code)) { + handleTerminalCommand(code); + } + }); + } + } + + Future<int> waitForAppToFinish() async { + int exitCode = await _finished.future; + await cleanupAtFinish(); + return exitCode; + } + + Future<Null> stopApp() { + if (serviceProtocol != null && !serviceProtocol.isClosed) { + if (serviceProtocol.isolates.isNotEmpty) { + serviceProtocol.flutterExit(serviceProtocol.firstIsolateId); + return new Future<Null>.delayed(new Duration(milliseconds: 100)); + } + } + appFinished(); + return new Future<Null>.value(); + } + + /// Called when a signal has requested we exit. + Future<Null> cleanupAfterSignal(); + /// Called right before we exit. + Future<Null> cleanupAtFinish(); + /// Called to print help to the terminal. + void printHelp(); + /// Called when the runner should handle a terminal command. + void handleTerminalCommand(String code); +} + +/// 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]) { + if (target == null) + target = ''; + String targetPath = path.absolute(target); + if (FileSystemEntity.isDirectorySync(targetPath)) + return path.join(targetPath, 'lib', 'main.dart'); + else + return targetPath; +} + +String getMissingPackageHintForPlatform(TargetPlatform platform) { + switch (platform) { + case TargetPlatform.android_arm: + case TargetPlatform.android_x64: + return 'Is your project missing an android/AndroidManifest.xml?\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; + } +}
diff --git a/packages/flutter_tools/lib/src/run.dart b/packages/flutter_tools/lib/src/run.dart index 1ee4eab..098f4b5 100644 --- a/packages/flutter_tools/lib/src/run.dart +++ b/packages/flutter_tools/lib/src/run.dart
@@ -5,66 +5,32 @@ import 'dart:async'; import 'dart:io'; -import 'package:path/path.dart' as path; - import 'application_package.dart'; import 'base/logger.dart'; import 'base/utils.dart'; -import 'build_info.dart'; -import 'cache.dart'; import 'commands/build_apk.dart'; import 'commands/install.dart'; import 'commands/trace.dart'; import 'device.dart'; import 'globals.dart'; import 'observatory.dart'; -import 'devfs.dart'; +import 'resident_runner.dart'; -/// 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]) { - if (target == null) - target = ''; - String targetPath = path.absolute(target); - if (FileSystemEntity.isDirectorySync(targetPath)) - return path.join(targetPath, 'lib', 'main.dart'); - else - return targetPath; -} - -String getDevFSLoaderScript() { - return path.absolute(path.join(Cache.flutterRoot, - 'packages', - 'flutter', - 'bin', - 'loader', - 'loader_app.dart')); -} - -class RunAndStayResident { +class RunAndStayResident extends ResidentRunner { RunAndStayResident( - this.device, { - this.target, - this.debuggingOptions, - this.usesTerminalUI: true, - this.hotMode: false - }); - - final Device device; - final String target; - final DebuggingOptions debuggingOptions; - final bool usesTerminalUI; - final bool hotMode; + Device device, { + String target, + DebuggingOptions debuggingOptions, + bool usesTerminalUI: true + }) : super(device, + target: target, + debuggingOptions: debuggingOptions, + usesTerminalUI: usesTerminalUI); ApplicationPackage _package; String _mainPath; LaunchResult _result; - final Completer<int> _exitCompleter = new Completer<int>(); - StreamSubscription<String> _loggingSubscription; - - Observatory observatory; - /// Start the app and keep the process running during its lifetime. Future<int> run({ bool traceStartup: false, @@ -86,7 +52,7 @@ } Future<bool> restart() async { - if (observatory == null) { + if (serviceProtocol == null) { printError('Debugging is not enabled.'); return false; } else { @@ -95,7 +61,7 @@ Future<Event> extensionAddedEvent; if (device.restartSendsFrameworkInitEvent) { - extensionAddedEvent = observatory.onExtensionEvent + extensionAddedEvent = serviceProtocol.onExtensionEvent .where((Event event) => event.extensionKind == 'Flutter.FrameworkInitialization') .first; } @@ -104,7 +70,7 @@ _package, _result, mainPath: _mainPath, - observatory: observatory + observatory: serviceProtocol ); status.stop(showElapsedTime: true); @@ -118,11 +84,6 @@ } } - Future<Null> stop() { - _stopLogger(); - return _stopApp(); - } - Future<int> _run({ bool traceStartup: false, bool benchmark: false, @@ -186,19 +147,13 @@ if (traceStartup != null) platformArgs = <String, dynamic>{ 'trace-startup': traceStartup }; - if (!hotMode || (hotMode && !device.needsDevFS)) - printStatus('Running ${getDisplayPath(_mainPath)} on ${device.name}...'); - - - _loggingSubscription = device.logReader.logLines.listen((String line) { - if (!line.contains('Observatory listening on http') && !line.contains('Diagnostic server listening on http')) - printStatus(line); - }); + await startEchoingDeviceLog(); + printStatus('Running ${getDisplayPath(_mainPath)} on ${device.name}...'); _result = await device.startApp( _package, debuggingOptions.buildMode, - mainPath: (hotMode && device.needsDevFS) ? getDevFSLoaderScript() : _mainPath, + mainPath: _mainPath, debuggingOptions: debuggingOptions, platformArgs: platformArgs, route: route @@ -206,7 +161,7 @@ if (!_result.started) { printError('Error running application on ${device.name}.'); - await _loggingSubscription.cancel(); + await stopEchoingDeviceLog(); return 2; } @@ -217,101 +172,21 @@ // Connect to observatory. if (debuggingOptions.debuggingEnabled) { - observatory = await Observatory.connect(_result.observatoryPort); - printTrace('Connected to observatory port: ${_result.observatoryPort}.'); - observatory.populateIsolateInfo(); - observatory.onExtensionEvent.listen((Event event) { - printTrace(event.toString()); - }); - observatory.onIsolateEvent.listen((Event event) { - printTrace(event.toString()); - }); - - if (hotMode && device.needsDevFS) { - _loaderShowMessage('Connecting...', progress: 0); - bool result = await _updateDevFS( - progressReporter: (int progress, int max) { - _loaderShowMessage('Syncing files to device...', progress: progress, max: max); - } - ); - if (!result) { - _loaderShowMessage('Failed.'); - printError('Could not perform initial file synchronization.'); - return 3; - } - printStatus('Running ${getDisplayPath(_mainPath)} on ${device.name}...'); - _loaderShowMessage('Launching...'); - await _launchFromDevFS(_package, _mainPath); - } + await connectToServiceProtocol(_result.observatoryPort); if (benchmark) - await observatory.waitFirstIsolate; - - // Listen for observatory connection close. - observatory.done.whenComplete(() { - if (!_exitCompleter.isCompleted) { - printStatus('Application finished.'); - _exitCompleter.complete(0); - } - }); + await serviceProtocol.waitFirstIsolate; } printStatus('Application running.'); - if (observatory != null && traceStartup) { + if (serviceProtocol != null && traceStartup) { printStatus('Downloading startup trace info...'); - - await downloadStartupTrace(observatory); - - if (!_exitCompleter.isCompleted) - _exitCompleter.complete(0); + await downloadStartupTrace(serviceProtocol); + appFinished(); } else { - if (usesTerminalUI) { - if (!logger.quiet) - _printHelp(); - - terminal.singleCharMode = true; - terminal.onCharInput.listen((String code) { - printStatus(''); // the key the user tapped might be on this line - final String lower = code.toLowerCase(); - if (lower == 'h' || lower == '?' || code == AnsiTerminal.KEY_F1) { - // F1, help - _printHelp(); - } else if (lower == 'r' || code == AnsiTerminal.KEY_F5) { - // F5, restart - if (hotMode && code == 'r') { - // lower-case 'r' - _reloadSources(); - } else { - // upper-case 'r', or hot restart disabled - if (device.supportsRestart) - restart(); - } - } else if (lower == 'q' || code == AnsiTerminal.KEY_F10) { - // F10, exit - _stopApp(); - } else if (lower == 'w') { - _debugDumpApp(); - } else if (lower == 't') { - _debugDumpRenderTree(); - } - }); - } - - ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) async { - _resetTerminal(); - await _cleanupDevFS(); - await _stopLogger(); - await _stopApp(); - exit(0); - }); - ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) async { - _resetTerminal(); - await _cleanupDevFS(); - await _stopLogger(); - await _stopApp(); - exit(0); - }); + setupTerminal(); + registerSignalHandlers(); } if (benchmark) { @@ -329,159 +204,36 @@ stop(); } - return _exitCompleter.future.then((int exitCode) async { - _resetTerminal(); - _stopLogger(); - return exitCode; - }); + return await waitForAppToFinish(); } - void _debugDumpApp() { - observatory.flutterDebugDumpApp(observatory.firstIsolateId); - } - - void _debugDumpRenderTree() { - observatory.flutterDebugDumpRenderTree(observatory.firstIsolateId); - } - - void _loaderShowMessage(String message, { int progress, int max }) { - observatory.flutterLoaderShowMessage(observatory.firstIsolateId, message); - if (progress != null) { - observatory.flutterLoaderSetProgress(observatory.firstIsolateId, progress.toDouble()); - observatory.flutterLoaderSetProgressMax(observatory.firstIsolateId, max?.toDouble() ?? 0.0); - } else { - observatory.flutterLoaderSetProgress(observatory.firstIsolateId, 0.0); - observatory.flutterLoaderSetProgressMax(observatory.firstIsolateId, -1.0); - } - } - - DevFS _devFS; - String _devFSProjectRootPath; - Future<bool> _updateDevFS({ DevFSProgressReporter progressReporter }) async { - if (_devFS == null) { - Directory directory = Directory.current; - _devFSProjectRootPath = directory.path; - String fsName = path.basename(directory.path); - _devFS = new DevFS(observatory, fsName, directory); - - try { - await _devFS.create(); - } catch (error) { - _devFS = null; - printError('Error initializing DevFS: $error'); - return false; - } - - _exitCompleter.future.then((_) async { - await _cleanupDevFS(); - }); - } - - Status devFSStatus = logger.startProgress('Syncing files on device...'); - await _devFS.update(progressReporter: progressReporter); - devFSStatus.stop(showElapsedTime: true); - printStatus('Synced ${getSizeAsMB(_devFS.bytes)} MB'); - return true; - } - - Future<Null> _cleanupDevFS() async { - if (_devFS != null) { - // Cleanup the devFS. - await _devFS.destroy(); - } - _devFS = null; - } - - Future<Null> _launchFromDevFS(ApplicationPackage package, - String mainScript) async { - String entryPath = path.relative(mainScript, from: _devFSProjectRootPath); - String deviceEntryPath = - _devFS.baseUri.resolve(entryPath).toFilePath(); - String devicePackagesPath = - _devFS.baseUri.resolve('.packages').toFilePath(); - await device.runFromFile(package, - deviceEntryPath, - devicePackagesPath); - } - - Future<bool> _reloadSources() async { - if (observatory.firstIsolateId == null) - throw 'Application isolate not found'; - if (_devFS != null) - await _updateDevFS(); - Status reloadStatus = logger.startProgress('Performing hot reload'); - try { - await observatory.reloadSources(observatory.firstIsolateId); - } catch (errorMessage) { - reloadStatus.stop(showElapsedTime: true); - printError('Hot reload was rejected:\n$errorMessage'); - return false; - } - reloadStatus.stop(showElapsedTime: true); - Status reassembleStatus = - logger.startProgress('Reassembling application'); - try { - await observatory.flutterReassemble(observatory.firstIsolateId); - } catch (_) { - reassembleStatus.stop(showElapsedTime: true); - printError('Reassembling application failed.'); - return false; - } - reassembleStatus.stop(showElapsedTime: true); - return true; - } - - void _printHelp() { - printStatus('Type "h" or F1 for this help message. Type "q", F10, or ctrl-c to quit.', emphasis: true); - String hot = ''; - String cold = ''; - if (hotMode) - hot = 'Type "r" or F5 to perform a hot reload of the app'; - if (device.supportsRestart) { - if (hotMode) { - cold = ', and "R" to cold restart the app'; - } else { - cold = 'Type "r" or F5 to restart the app'; + @override + void handleTerminalCommand(String code) { + String lower = code.toLowerCase(); + if (lower == 'r' || code == AnsiTerminal.KEY_F5) { + if (device.supportsRestart) { + // F5, restart + restart(); } } - if (hot != '' || cold != '') - printStatus('$hot$cold.', emphasis: true); - printStatus('Type "w" to print the widget hierarchy of the app, and "t" for the render tree.', emphasis: true); } - Future<dynamic> _stopLogger() { - return _loggingSubscription?.cancel(); + @override + Future<Null> cleanupAfterSignal() async { + await stopEchoingDeviceLog(); + await stopApp(); } - void _resetTerminal() { - if (usesTerminalUI) - terminal.singleCharMode = false; + @override + Future<Null> cleanupAtFinish() async { + await stopEchoingDeviceLog(); } - Future<Null> _stopApp() { - if (observatory != null && !observatory.isClosed) { - if (observatory.isolates.isNotEmpty) { - observatory.flutterExit(observatory.firstIsolateId); - return new Future<Null>.delayed(new Duration(milliseconds: 100)); - } - } - - if (!_exitCompleter.isCompleted) - _exitCompleter.complete(0); - - return new Future<Null>.value(); - } -} - -String getMissingPackageHintForPlatform(TargetPlatform platform) { - switch (platform) { - case TargetPlatform.android_arm: - case TargetPlatform.android_x64: - return 'Is your project missing an android/AndroidManifest.xml?\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; + @override + void printHelp() { + String restartText = device.supportsRestart ? ', "r" or F5 to restart the app,' : ''; + printStatus('Type "h" or F1 for help$restartText and "q", F10, or ctrl-c to quit.'); + printStatus('Type "w" to print the widget hierarchy of the app, and "t" for the render tree.'); } }