diff --git a/packages/flutter_tools/lib/src/base/context.dart b/packages/flutter_tools/lib/src/base/context.dart index e830d6bc577..12d91564bc2 100644 --- a/packages/flutter_tools/lib/src/base/context.dart +++ b/packages/flutter_tools/lib/src/base/context.dart @@ -6,7 +6,7 @@ import 'dart:async'; final AppContext _defaultContext = new AppContext(); -typedef void ErrorHandler(dynamic error); +typedef void ErrorHandler(dynamic error, StackTrace stackTrace); /// A singleton for application functionality. This singleton can be different /// on a per-Zone basis. @@ -17,6 +17,7 @@ AppContext get context { class AppContext { Map _instances = {}; + Zone _zone; bool isSet(Type type) { if (_instances.containsKey(type)) @@ -30,7 +31,7 @@ class AppContext { if (_instances.containsKey(type)) return _instances[type]; - AppContext parent = _calcParent(Zone.current); + AppContext parent = _calcParent(_zone ?? Zone.current); return parent?.getVariable(type); } @@ -58,11 +59,22 @@ class AppContext { } } - dynamic runInZone(dynamic method(), { ErrorHandler onError }) { + dynamic runInZone(dynamic method(), { + ZoneBinaryCallback onError + }) { return runZoned( - method, + () => _run(method), zoneValues: { 'context': this }, onError: onError ); } + + dynamic _run(dynamic method()) async { + try { + _zone = Zone.current; + return await method(); + } finally { + _zone = null; + } + } } diff --git a/packages/flutter_tools/lib/src/base/logger.dart b/packages/flutter_tools/lib/src/base/logger.dart index cbcc5161390..2ba170d6209 100644 --- a/packages/flutter_tools/lib/src/base/logger.dart +++ b/packages/flutter_tools/lib/src/base/logger.dart @@ -214,7 +214,6 @@ class AnsiTerminal { String writeBold(String str) => supportsColor ? '$_bold$str$_reset' : str; set singleCharMode(bool value) { - stdin.echoMode = !value; stdin.lineMode = !value; } diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart index 7f9f5e48a9e..280c3935ae9 100644 --- a/packages/flutter_tools/lib/src/base/utils.dart +++ b/packages/flutter_tools/lib/src/base/utils.dart @@ -78,6 +78,13 @@ String getSizeAsMB(int bytesLength) { return '${(bytesLength / (1024 * 1024)).toStringAsFixed(1)}MB'; } +/// Return a relative path if [fullPath] is contained by the cwd, else return an +/// absolute path. +String getDisplayPath(String fullPath) { + String cwd = Directory.current.path + Platform.pathSeparator; + return fullPath.startsWith(cwd) ? fullPath.substring(cwd.length) : fullPath; +} + /// A class to maintain a list of items, fire events when items are added or /// removed, and calculate a diff of changes when a new list of items is /// available. diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 76b663211f1..c8d600e2fee 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -22,6 +22,16 @@ enum BuildMode { String getModeName(BuildMode mode) => getEnumName(mode); +BuildMode getBuildModeForName(String mode) { + if (mode == 'debug') + return BuildMode.debug; + if (mode == 'profile') + return BuildMode.profile; + if (mode == 'release') + return BuildMode.release; + return null; +} + // Returns true if the selected build mode uses ahead-of-time compilation. bool isAotBuildMode(BuildMode mode) { return mode == BuildMode.profile || mode == BuildMode.release; diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart index cc5148caff3..e5707de9b72 100644 --- a/packages/flutter_tools/lib/src/commands/build_aot.dart +++ b/packages/flutter_tools/lib/src/commands/build_aot.dart @@ -13,8 +13,8 @@ import '../base/utils.dart'; import '../build_info.dart'; import '../dart/sdk.dart'; import '../globals.dart'; +import '../run.dart'; import '../runner/flutter_command.dart'; -import 'run.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 38f576fd04f..08ee4ffac4d 100644 --- a/packages/flutter_tools/lib/src/commands/build_apk.dart +++ b/packages/flutter_tools/lib/src/commands/build_apk.dart @@ -10,17 +10,17 @@ import 'package:path/path.dart' as path; import '../android/android_sdk.dart'; import '../base/file_system.dart' show ensureDirectoryExists; -import '../base/os.dart'; import '../base/logger.dart'; +import '../base/os.dart'; import '../base/process.dart'; import '../base/utils.dart'; import '../build_info.dart'; import '../flx.dart' as flx; import '../globals.dart'; +import '../run.dart'; import '../runner/flutter_command.dart'; import '../services.dart'; import 'build_aot.dart'; -import 'run.dart'; export '../android/android_device.dart' show AndroidDevice; diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 247a4c3ac97..6b850f03c22 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -7,7 +7,6 @@ import 'dart:convert'; import 'dart:io'; import '../android/android_device.dart'; -import '../application_package.dart'; import '../base/context.dart'; import '../base/logger.dart'; import '../build_info.dart'; @@ -15,10 +14,10 @@ import '../device.dart'; import '../globals.dart'; import '../ios/devices.dart'; import '../ios/simulators.dart'; +import '../run.dart'; import '../runner/flutter_command.dart'; -import 'run.dart'; -const String protocolVersion = '0.1.0'; +const String protocolVersion = '0.2.0'; /// A server process command. This command will start up a long-lived server. /// It reads JSON-RPC based commands from stdin, executes them, and returns @@ -73,14 +72,15 @@ class DaemonCommand extends FlutterCommand { return object; } - void _handleError(dynamic error) { - printError('Error from flutter daemon: $error'); + dynamic _handleError(dynamic error, StackTrace stackTrace) { + printError('Error from flutter daemon: $error', stackTrace); + return null; } } typedef void DispatchComand(Map command); -typedef Future CommandHandler(dynamic args); +typedef Future CommandHandler(Map args); class Daemon { Daemon(Stream> commandStream, this.sendCommand, { @@ -137,7 +137,7 @@ class Daemon { if (_domainMap[prefix] == null) throw 'no domain for method: $method'; - _domainMap[prefix].handleCommand(name, id, request['params']); + _domainMap[prefix].handleCommand(name, id, request['params'] ?? const {}); } catch (error) { _send({'id': id, 'error': _toJsonable(error)}); } @@ -168,7 +168,7 @@ abstract class Domain { @override String toString() => name; - void handleCommand(String command, dynamic id, dynamic args) { + void handleCommand(String command, dynamic id, Map args) { new Future.sync(() { if (_handlers.containsKey(command)) return _handlers[command](args); @@ -193,6 +193,33 @@ abstract class Domain { void _send(Map map) => daemon._send(map); + String _getStringArg(Map args, String name, { bool required: false }) { + if (required && !args.containsKey(name)) + throw "$name is required"; + dynamic val = args[name]; + if (val != null && val is! String) + throw "$name is not a String"; + return val; + } + + bool _getBoolArg(Map args, String name, { bool required: false }) { + if (required && !args.containsKey(name)) + throw "$name is required"; + dynamic val = args[name]; + if (val != null && val is! bool) + throw "$name is not a bool"; + return val; + } + + int _getIntArg(Map args, String name, { bool required: false }) { + if (required && !args.containsKey(name)) + throw "$name is required"; + dynamic val = args[name]; + if (val != null && val is! int) + throw "$name is not an int"; + return val; + } + void dispose() { } } @@ -222,11 +249,11 @@ class DaemonDomain extends Domain { StreamSubscription _subscription; - Future version(dynamic args) { + Future version(Map args) { return new Future.value(protocolVersion); } - Future shutdown(dynamic args) { + Future shutdown(Map args) { Timer.run(() => daemon.shutdown()); return new Future.value(); } @@ -237,91 +264,149 @@ class DaemonDomain extends Domain { } } -/// Return the device matching the deviceId field in the args. -Future _getDevice(Daemon daemon, Map args) async { - if (args == null || args['deviceId'] is! String) - throw 'deviceId is required'; - - List devices = await daemon.deviceDomain.getDevices(); - Device device = devices.firstWhere( - (Device device) => device.id == args['deviceId'], - orElse: () => null - ); - - if (device == null) - throw "device '${args['deviceId']}' not found"; - - return device; -} - /// This domain responds to methods like [start] and [stop]. /// -/// It'll be extended to fire events for when applications start, stop, and -/// log data. +/// It fires events for application start, stop, and stdout and stderr. class AppDomain extends Domain { AppDomain(Daemon daemon) : super(daemon, 'app') { registerHandler('start', start); + registerHandler('restart', restart); registerHandler('stop', stop); registerHandler('discover', discover); } - Future start(Map args) async { - Device device = await _getDevice(daemon, args); + static int _nextAppId = 0; + + static String _getNextAppId() => 'app-${_nextAppId++}'; + + List _apps = []; + + Future> start(Map args) async { + String deviceId = _getStringArg(args, 'deviceId', required: true); + String projectDirectory = _getStringArg(args, 'projectDirectory', required: true); + bool startPaused = _getBoolArg(args, 'startPaused'); + // TODO(devoncarew): Use the route param. + String route = _getStringArg(args, 'route'); // ignore: unused_local_variable + String mode = _getStringArg(args, 'mode'); + String target = _getStringArg(args, 'target'); + + Device device = daemon.deviceDomain._getDevice(deviceId); + if (device == null) + throw "device '$deviceId' not found"; - if (args['projectDirectory'] is! String) - throw "projectDirectory is required"; - String projectDirectory = args['projectDirectory']; if (!FileSystemEntity.isDirectorySync(projectDirectory)) throw "'$projectDirectory' does not exist"; + BuildMode buildMode = getBuildModeForName(mode) ?? BuildMode.debug; + DebuggingOptions options; + + switch (buildMode) { + case BuildMode.debug: + case BuildMode.profile: + options = new DebuggingOptions.enabled(buildMode, startPaused: startPaused); + break; + case BuildMode.release: + options = new DebuggingOptions.disabled(buildMode); + break; + default: + throw 'unhandle build mode: $buildMode'; + } + // We change the current working directory for the duration of the `start` command. - // TODO(devoncarew): Make flutter_tools work better with commands run from any directory. Directory cwd = Directory.current; Directory.current = new Directory(projectDirectory); - try { - int result = await startApp( - device, - stop: true, - target: args['target'], - route: args['route'] - ); + RunAndStayResident runner = new RunAndStayResident( + device, + target: target, + debuggingOptions: options, + usesTerminalUI: false + ); - if (result != 0) - throw 'Error starting app: $result'; - } finally { - Directory.current = cwd; + AppInstance app = new AppInstance(_getNextAppId(), runner); + _apps.add(app); + _sendAppEvent(app, 'start', { + 'directory': projectDirectory, + 'deviceId': deviceId + }); + + Completer observatoryPortCompleter; + + if (options.debuggingEnabled) { + observatoryPortCompleter = new Completer(); + observatoryPortCompleter.future.then((int port) { + _sendAppEvent(app, 'debugPort', { 'port': port }); + }); } - return null; + app._runInZone(this, () { + runner.run(observatoryPortCompleter: observatoryPortCompleter).then((_) { + _sendAppEvent(app, 'stop'); + }).catchError((dynamic error) { + _sendAppEvent(app, 'stop', { 'error' : error.toString() }); + }).whenComplete(() { + Directory.current = cwd; + _apps.remove(app); + }); + }); + + return { 'appId': app.id }; + } + + Future restart(Map args) async { + String appId = _getStringArg(args, 'appId', required: true); + + AppInstance app = _getApp(appId); + if (app == null) + throw "app '$appId' not found"; + + return app._runInZone(this, () { + return app.restart(); + }); } Future stop(Map args) async { - Device device = await _getDevice(daemon, args); + String appId = _getStringArg(args, 'appId', required: true); - if (args['projectDirectory'] is! String) - throw "projectDirectory is required"; - String projectDirectory = args['projectDirectory']; - if (!FileSystemEntity.isDirectorySync(projectDirectory)) - throw "'$projectDirectory' does not exist"; + AppInstance app = _getApp(appId); + if (app == null) + throw "app '$appId' not found"; - Directory cwd = Directory.current; - Directory.current = new Directory(projectDirectory); - - try { - ApplicationPackage app = command.applicationPackages.getPackageForPlatform(device.platform); - return device.stopApp(app); - } finally { - Directory.current = cwd; - } + return app.stop().timeout(new Duration(seconds: 5)).then((_) { + return true; + }).catchError((dynamic error) { + _sendAppEvent(app, 'log', { 'log': '$error', 'error': true }); + app.closeLogger(); + _apps.remove(app); + return false; + }); } Future>> discover(Map args) async { - Device device = await _getDevice(daemon, args); + String deviceId = _getStringArg(args, 'deviceId', required: true); + + Device device = daemon.deviceDomain._getDevice(deviceId); + if (device == null) + throw "device '$deviceId' not found"; + List apps = await device.discoverApps(); - return apps.map((DiscoveredApp app) => - {'id': app.id, 'observatoryDevicePort': app.observatoryPort} - ).toList(); + return apps.map((DiscoveredApp app) { + return { + 'id': app.id, + 'observatoryDevicePort': app.observatoryPort + }; + }).toList(); + } + + AppInstance _getApp(String id) { + return _apps.firstWhere((AppInstance app) => app.id == id, orElse: () => null); + } + + void _sendAppEvent(AppInstance app, String name, [Map args]) { + Map eventArgs = { 'appId': app.id }; + if (args != null) + eventArgs.addAll(args); + sendEvent('app.$name', eventArgs); } } @@ -361,7 +446,7 @@ class DeviceDomain extends Domain { List _discoverers = []; - Future> getDevices([dynamic args]) { + Future> getDevices([Map args]) { List devices = _discoverers.expand((PollingDeviceDiscovery discoverer) { return discoverer.devices; }).toList(); @@ -369,56 +454,59 @@ class DeviceDomain extends Domain { } /// Enable device events. - Future enable(dynamic args) { - for (PollingDeviceDiscovery discoverer in _discoverers) { + Future enable(Map args) { + for (PollingDeviceDiscovery discoverer in _discoverers) discoverer.startPolling(); - } return new Future.value(); } /// Disable device events. - Future disable(dynamic args) { - for (PollingDeviceDiscovery discoverer in _discoverers) { + Future disable(Map args) { + for (PollingDeviceDiscovery discoverer in _discoverers) discoverer.stopPolling(); - } return new Future.value(); } /// Forward a host port to a device port. Future> forward(Map args) async { - Device device = await _getDevice(daemon, args); + String deviceId = _getStringArg(args, 'deviceId', required: true); + int devicePort = _getIntArg(args, 'devicePort', required: true); + int hostPort = _getIntArg(args, 'hostPort'); - if (args['devicePort'] is! int) - throw 'devicePort is required'; - int devicePort = args['devicePort']; - - int hostPort = args['hostPort']; + Device device = daemon.deviceDomain._getDevice(deviceId); + if (device == null) + throw "device '$deviceId' not found"; hostPort = await device.portForwarder.forward(devicePort, hostPort: hostPort); - return {'hostPort': hostPort}; + return { 'hostPort': hostPort }; } /// Removes a forwarded port. Future unforward(Map args) async { - Device device = await _getDevice(daemon, args); + String deviceId = _getStringArg(args, 'deviceId', required: true); + int devicePort = _getIntArg(args, 'devicePort', required: true); + int hostPort = _getIntArg(args, 'hostPort', required: true); - if (args['devicePort'] is! int) - throw 'devicePort is required'; - int devicePort = args['devicePort']; + Device device = daemon.deviceDomain._getDevice(deviceId); + if (device == null) + throw "device '$deviceId' not found"; - if (args['hostPort'] is! int) - throw 'hostPort is required'; - int hostPort = args['hostPort']; - - device.portForwarder.unforward(new ForwardedPort(hostPort, devicePort)); + return device.portForwarder.unforward(new ForwardedPort(hostPort, devicePort)); } @override void dispose() { - for (PollingDeviceDiscovery discoverer in _discoverers) { + for (PollingDeviceDiscovery discoverer in _discoverers) discoverer.dispose(); - } + } + + /// Return the device matching the deviceId field in the args. + Device _getDevice(String deviceId) { + List devices = _discoverers.expand((PollingDeviceDiscovery discoverer) { + return discoverer.devices; + }).toList(); + return devices.firstWhere((Device device) => device.id == deviceId, orElse: () => null); } } @@ -465,6 +553,75 @@ class NotifyingLogger extends Logger { } } +/// A running application, started by this daemon. +class AppInstance { + AppInstance(this.id, [this.runner]); + + final String id; + final RunAndStayResident runner; + + _AppRunLogger _logger; + + Future restart() => runner.restart(); + + Future stop() => runner.stop(); + + void closeLogger() { + _logger.close(); + } + + dynamic _runInZone(AppDomain domain, dynamic method()) { + if (_logger == null) + _logger = new _AppRunLogger(domain, this); + + AppContext appContext = new AppContext(); + appContext[Logger] = _logger; + return appContext.runInZone(method); + } +} + +/// A [Logger] which sends log messages to a listening daemon client. +class _AppRunLogger extends Logger { + _AppRunLogger(this.domain, this.app); + + AppDomain domain; + final AppInstance app; + + @override + void printError(String message, [StackTrace stackTrace]) { + if (stackTrace != null) { + domain?._sendAppEvent(app, 'log', { + 'log': message, + 'stackTrace': stackTrace.toString(), + 'error': true + }); + } else { + domain?._sendAppEvent(app, 'log', { + 'log': message, + 'error': true + }); + } + } + + @override + void printStatus(String message, { bool emphasis: false }) { + domain?._sendAppEvent(app, 'log', { 'log': message }); + } + + @override + void printTrace(String message) { } + + @override + Status startProgress(String message) { + printStatus(message); + return new Status(); + } + + void close() { + domain = null; + } +} + class LogMessage { final String level; final String message; diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index 3bf2d90d039..9cc65aa535d 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -20,6 +20,7 @@ import '../dart/sdk.dart'; import '../device.dart'; import '../globals.dart'; import '../ios/simulators.dart' show SimControl, IOSSimulatorUtils; +import '../run.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 bb0b4000abb..91925abb6ec 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -5,16 +5,14 @@ import 'dart:async'; import 'dart:io'; -import 'package:path/path.dart' as path; - import '../application_package.dart'; import '../base/common.dart'; -import '../base/logger.dart'; import '../base/utils.dart'; import '../build_info.dart'; import '../device.dart'; import '../globals.dart'; import '../observatory.dart'; +import '../run.dart'; import '../runner/flutter_command.dart'; import 'build_apk.dart'; import 'install.dart'; @@ -112,15 +110,16 @@ class RunCommand extends RunCommandBase { } if (argResults['resident']) { - _RunAndStayResident runner = new _RunAndStayResident( + RunAndStayResident runner = new RunAndStayResident( deviceForCommand, target: target, - debuggingOptions: options, - buildMode: getBuildMode() + debuggingOptions: options ); return runner.run(traceStartup: traceStartup, benchmark: argResults['benchmark']); } else { + // TODO(devoncarew): Remove this path and support the `--no-resident` option + // using the `RunAndStayResident` class. return startApp( deviceForCommand, target: target, @@ -160,7 +159,7 @@ Future startApp( if (package == null) { String message = 'No application found for ${device.platform}.'; - String hint = _getMissingPackageHintForPlatform(device.platform); + String hint = getMissingPackageHintForPlatform(device.platform); if (hint != null) message += '\n$hint'; printError(message); @@ -211,7 +210,7 @@ Future startApp( if (traceStartup != null) platformArgs['trace-startup'] = traceStartup; - printStatus('Running ${_getDisplayPath(mainPath)} on ${device.name}...'); + printStatus('Running ${getDisplayPath(mainPath)} on ${device.name}...'); LaunchResult result = await device.startApp( package, @@ -229,7 +228,7 @@ Future startApp( } else if (traceStartup) { try { Observatory observatory = await Observatory.connect(result.observatoryPort); - await _downloadStartupTrace(observatory); + await downloadStartupTrace(observatory); } catch (error) { printError('Error connecting to observatory: $error'); return 1; @@ -237,345 +236,7 @@ Future startApp( } if (benchmark) - _writeBenchmark(stopwatch); + writeRunBenchmarkFile(stopwatch); return result.started ? 0 : 2; } - -/// 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?'; - case TargetPlatform.ios: - return 'Is your project missing an ios/Info.plist?'; - default: - return null; - } -} - -/// Return a relative path if [fullPath] is contained by the cwd, else return an -/// absolute path. -String _getDisplayPath(String fullPath) { - String cwd = Directory.current.path + Platform.pathSeparator; - return fullPath.startsWith(cwd) ? fullPath.substring(cwd.length) : fullPath; -} - -class _RunAndStayResident { - _RunAndStayResident( - this.device, { - this.target, - this.debuggingOptions, - this.buildMode : BuildMode.debug - }); - - final Device device; - final String target; - final DebuggingOptions debuggingOptions; - final BuildMode buildMode; - - Completer _exitCompleter; - StreamSubscription _loggingSubscription; - - Observatory observatory; - - /// Start the app and keep the process running during its lifetime. - Future run({ bool traceStartup: false, bool benchmark: false }) { - // Don't let uncaught errors kill the process. - return runZoned(() { - return _run(traceStartup: traceStartup, benchmark: benchmark); - }, onError: (dynamic error) { - printError('Exception from flutter run: $error'); - }); - } - - Future _run({ bool traceStartup: false, bool benchmark: false }) async { - String 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; - } - - ApplicationPackage 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; - } - - Stopwatch startTime = new Stopwatch()..start(); - - // 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: 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.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))) - return 1; - } - - Map platformArgs; - if (traceStartup != null) - platformArgs = { 'trace-startup': traceStartup }; - - 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); - }); - - LaunchResult result = await device.startApp( - package, - buildMode, - mainPath: mainPath, - debuggingOptions: debuggingOptions, - platformArgs: platformArgs - ); - - if (!result.started) { - printError('Error running application on ${device.name}.'); - await _loggingSubscription.cancel(); - return 2; - } - - startTime.stop(); - - _exitCompleter = new Completer(); - - // 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 (benchmark) - await observatory.waitFirstIsolate; - - // Listen for observatory connection close. - observatory.done.whenComplete(() { - _handleExit(); - }); - } - - printStatus('Application running.'); - - if (observatory != null && traceStartup) { - printStatus('Downloading startup trace info...'); - - await _downloadStartupTrace(observatory); - - _handleExit(); - } else { - if (!logger.quiet) - _printHelp(); - - terminal.singleCharMode = true; - - terminal.onCharInput.listen((String code) { - String lower = code.toLowerCase(); - - if (lower == 'h' || code == AnsiTerminal.KEY_F1) { - // F1, help - _printHelp(); - } else if (lower == 'r' || code == AnsiTerminal.KEY_F5) { - // F5, refresh - _handleRefresh(package, result, mainPath); - } else if (lower == 'q' || code == AnsiTerminal.KEY_F10) { - // F10, exit - _handleExit(); - } - }); - - ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) { - _handleExit(); - }); - ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) { - _handleExit(); - }); - } - - if (benchmark) { - await new Future.delayed(new Duration(seconds: 4)); - - // Touch the file. - File mainFile = new File(mainPath); - mainFile.writeAsBytesSync(mainFile.readAsBytesSync()); - - Stopwatch restartTime = new Stopwatch()..start(); - bool restarted = await _handleRefresh(package, result, mainPath); - restartTime.stop(); - _writeBenchmark(startTime, restarted ? restartTime : null); - await new Future.delayed(new Duration(seconds: 2)); - _handleExit(); - } - - return _exitCompleter.future.then((int exitCode) async { - try { - if (observatory != null && !observatory.isClosed) { - if (observatory.isolates.isNotEmpty) { - observatory.flutterExit(observatory.firstIsolateId); - // The Dart WebSockets API does not have a flush() method. - await new Future.delayed(new Duration(milliseconds: 100)); - } - } - } catch (error) { - stderr.writeln(error.toString()); - } - - return exitCode; - }); - } - - void _printHelp() { - printStatus('Type "h" or F1 for help, "r" or F5 to restart the app, and "q", F10, or ctrl-c to quit.'); - } - - Future _handleRefresh(ApplicationPackage package, LaunchResult result, String mainPath) async { - if (observatory == null) { - printError('Debugging is not enabled.'); - return false; - } else { - Status status = logger.startProgress('Re-starting application...'); - - Future extensionAddedEvent = observatory.onExtensionEvent - .where((Event event) => event.extensionKind == 'Flutter.FrameworkInitialization') - .first; - - bool restartResult = await device.restartApp( - package, - result, - mainPath: mainPath, - observatory: observatory - ); - - status.stop(showElapsedTime: true); - - if (restartResult) { - // TODO(devoncarew): We should restore the route here. - - await extensionAddedEvent; - } - - return restartResult; - } - } - - void _handleExit() { - terminal.singleCharMode = false; - - if (!_exitCompleter.isCompleted) { - _loggingSubscription?.cancel(); - printStatus('Application finished.'); - _exitCompleter.complete(0); - } - } -} - -Future _downloadStartupTrace(Observatory observatory) async { - Tracing tracing = new Tracing(observatory); - - Map timeline = await tracing.stopTracingAndDownloadTimeline( - waitForFirstFrame: true - ); - - int extractInstantEventTimestamp(String eventName) { - List> events = timeline['traceEvents']; - Map event = events.firstWhere( - (Map event) => event['name'] == eventName, orElse: () => null - ); - return event == null ? null : event['ts']; - } - - int engineEnterTimestampMicros = extractInstantEventTimestamp(kFlutterEngineMainEnterEventName); - int frameworkInitTimestampMicros = extractInstantEventTimestamp(kFrameworkInitEventName); - int firstFrameTimestampMicros = extractInstantEventTimestamp(kFirstUsefulFrameEventName); - - if (engineEnterTimestampMicros == null) { - printError('Engine start event is missing in the timeline. Cannot compute startup time.'); - return null; - } - - if (firstFrameTimestampMicros == null) { - printError('First frame event is missing in the timeline. Cannot compute startup time.'); - return null; - } - - File traceInfoFile = new File('build/start_up_info.json'); - int timeToFirstFrameMicros = firstFrameTimestampMicros - engineEnterTimestampMicros; - Map traceInfo = { - 'engineEnterTimestampMicros': engineEnterTimestampMicros, - 'timeToFirstFrameMicros': timeToFirstFrameMicros, - }; - - if (frameworkInitTimestampMicros != null) { - traceInfo['timeToFrameworkInitMicros'] = frameworkInitTimestampMicros - engineEnterTimestampMicros; - traceInfo['timeAfterFrameworkInitMicros'] = firstFrameTimestampMicros - frameworkInitTimestampMicros; - } - - traceInfoFile.writeAsStringSync(toPrettyJson(traceInfo)); - - printStatus('Time to first frame: ${timeToFirstFrameMicros ~/ 1000}ms.'); - printStatus('Saved startup trace info in ${traceInfoFile.path}.'); -} - -void _writeBenchmark(Stopwatch startTime, [Stopwatch restartTime]) { - final String benchmarkOut = 'refresh_benchmark.json'; - Map data = { - 'start': startTime.elapsedMilliseconds, - 'time': (restartTime ?? startTime).elapsedMilliseconds // time and restart are the same - }; - if (restartTime != null) - data['restart'] = restartTime.elapsedMilliseconds; - - new File(benchmarkOut).writeAsStringSync(toPrettyJson(data)); - printStatus('Run benchmark written to $benchmarkOut ($data).'); -} diff --git a/packages/flutter_tools/lib/src/commands/run_mojo.dart b/packages/flutter_tools/lib/src/commands/run_mojo.dart index b3cc0bb062c..67da3832388 100644 --- a/packages/flutter_tools/lib/src/commands/run_mojo.dart +++ b/packages/flutter_tools/lib/src/commands/run_mojo.dart @@ -11,8 +11,8 @@ import '../base/process.dart'; import '../build_info.dart'; import '../flx.dart' as flx; import '../globals.dart'; +import '../run.dart'; import '../runner/flutter_command.dart'; -import 'run.dart'; const String _kDefaultBundlePath = 'build/app.flx'; diff --git a/packages/flutter_tools/lib/src/commands/stop.dart b/packages/flutter_tools/lib/src/commands/stop.dart index 630ecb110c8..123c2a935c5 100644 --- a/packages/flutter_tools/lib/src/commands/stop.dart +++ b/packages/flutter_tools/lib/src/commands/stop.dart @@ -5,6 +5,7 @@ import 'dart:async'; import '../application_package.dart'; +import '../build_info.dart'; import '../device.dart'; import '../globals.dart'; import '../runner/flutter_command.dart'; @@ -23,6 +24,11 @@ class StopCommand extends FlutterCommand { Future runInProject() async { Device device = deviceForCommand; ApplicationPackage app = applicationPackages.getPackageForPlatform(device.platform); + if (app == null) { + String platformName = getNameForTargetPlatform(device.platform); + printError('No Flutter application for $platformName found in the current directory.'); + return 1; + } printStatus('Stopping apps on ${device.name}.'); return await device.stopApp(app) ? 0 : 1; } diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index 53df9c20447..9ad502afe1c 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'package:path/path.dart' as path; import 'package:test/src/executable.dart' as executable; // ignore: implementation_imports +import '../dart/package_map.dart'; import '../globals.dart'; import '../runner/flutter_command.dart'; import '../test/flutter_platform.dart' as loader; @@ -58,6 +59,7 @@ class TestCommand extends FlutterCommand { try { if (testDirectory != null) { printTrace('switching to directory $testDirectory to run tests'); + PackageMap.globalPackagesPath = path.normalize(path.absolute(PackageMap.globalPackagesPath)); Directory.current = testDirectory; } printTrace('running test package with arguments: $testArgs'); diff --git a/packages/flutter_tools/lib/src/commands/trace.dart b/packages/flutter_tools/lib/src/commands/trace.dart index 9d90983fdb2..674940a62a5 100644 --- a/packages/flutter_tools/lib/src/commands/trace.dart +++ b/packages/flutter_tools/lib/src/commands/trace.dart @@ -147,3 +147,52 @@ class Tracing { return timeline.response; } } + +/// Download the startup trace information from the given observatory client and +/// store it to build/start_up_info.json. +Future downloadStartupTrace(Observatory observatory) async { + Tracing tracing = new Tracing(observatory); + + Map timeline = await tracing.stopTracingAndDownloadTimeline( + waitForFirstFrame: true + ); + + int extractInstantEventTimestamp(String eventName) { + List> events = timeline['traceEvents']; + Map event = events.firstWhere( + (Map event) => event['name'] == eventName, orElse: () => null + ); + return event == null ? null : event['ts']; + } + + int engineEnterTimestampMicros = extractInstantEventTimestamp(kFlutterEngineMainEnterEventName); + int frameworkInitTimestampMicros = extractInstantEventTimestamp(kFrameworkInitEventName); + int firstFrameTimestampMicros = extractInstantEventTimestamp(kFirstUsefulFrameEventName); + + if (engineEnterTimestampMicros == null) { + printError('Engine start event is missing in the timeline. Cannot compute startup time.'); + return null; + } + + if (firstFrameTimestampMicros == null) { + printError('First frame event is missing in the timeline. Cannot compute startup time.'); + return null; + } + + File traceInfoFile = new File('build/start_up_info.json'); + int timeToFirstFrameMicros = firstFrameTimestampMicros - engineEnterTimestampMicros; + Map traceInfo = { + 'engineEnterTimestampMicros': engineEnterTimestampMicros, + 'timeToFirstFrameMicros': timeToFirstFrameMicros, + }; + + if (frameworkInitTimestampMicros != null) { + traceInfo['timeToFrameworkInitMicros'] = frameworkInitTimestampMicros - engineEnterTimestampMicros; + traceInfo['timeAfterFrameworkInitMicros'] = firstFrameTimestampMicros - frameworkInitTimestampMicros; + } + + traceInfoFile.writeAsStringSync(toPrettyJson(traceInfo)); + + printStatus('Time to first frame: ${timeToFirstFrameMicros ~/ 1000}ms.'); + printStatus('Saved startup trace info in ${traceInfoFile.path}.'); +} diff --git a/packages/flutter_tools/lib/src/dart/package_map.dart b/packages/flutter_tools/lib/src/dart/package_map.dart index 6c4252878d1..2e0705c0078 100644 --- a/packages/flutter_tools/lib/src/dart/package_map.dart +++ b/packages/flutter_tools/lib/src/dart/package_map.dart @@ -17,6 +17,14 @@ Map _parse(String packagesPath) { class PackageMap { PackageMap(this.packagesPath); + static String get globalPackagesPath => _globalPackagesPath ?? kPackagesFileName; + + static set globalPackagesPath(String value) { + _globalPackagesPath = value; + } + + static String _globalPackagesPath; + final String packagesPath; Map get map { @@ -26,8 +34,6 @@ class PackageMap { } Map _map; - static PackageMap instance; - String checkValid() { if (FileSystemEntity.isFileSync(packagesPath)) return null; diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index fc96d64ae38..5d96756d790 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -321,7 +321,7 @@ abstract class DevicePortForwarder { /// Forward [hostPort] on the host to [devicePort] on the device. /// If [hostPort] is null, will auto select a host port. /// Returns a Future that completes with the host port. - Future forward(int devicePort, { int hostPort: null }); + Future forward(int devicePort, { int hostPort }); /// Stops forwarding [forwardedPort]. Future unforward(ForwardedPort forwardedPort); diff --git a/packages/flutter_tools/lib/src/flx.dart b/packages/flutter_tools/lib/src/flx.dart index e4309938148..77457598e1d 100644 --- a/packages/flutter_tools/lib/src/flx.dart +++ b/packages/flutter_tools/lib/src/flx.dart @@ -46,7 +46,7 @@ Future createSnapshot({ final List args = [ tools.getHostToolPath(HostTool.SkySnapshot), mainPath, - '--packages=${PackageMap.instance.packagesPath}', + '--packages=${path.absolute(PackageMap.globalPackagesPath)}', '--snapshot=$snapshotPath' ]; if (depfilePath != null) diff --git a/packages/flutter_tools/lib/src/run.dart b/packages/flutter_tools/lib/src/run.dart new file mode 100644 index 00000000000..7d95e0cd596 --- /dev/null +++ b/packages/flutter_tools/lib/src/run.dart @@ -0,0 +1,338 @@ +// 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 'base/logger.dart'; +import 'base/utils.dart'; +import 'build_info.dart'; +import 'commands/build_apk.dart'; +import 'commands/install.dart'; +import 'commands/trace.dart'; +import 'device.dart'; +import 'globals.dart'; +import 'observatory.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; +} + +// TODO: split out the cli part of the UI from this class + +class RunAndStayResident { + RunAndStayResident( + this.device, { + this.target, + this.debuggingOptions, + this.usesTerminalUI: true + }); + + final Device device; + final String target; + final DebuggingOptions debuggingOptions; + final bool usesTerminalUI; + + ApplicationPackage _package; + String _mainPath; + LaunchResult _result; + + Completer _exitCompleter = new Completer(); + StreamSubscription _loggingSubscription; + + Observatory observatory; + + /// Start the app and keep the process running during its lifetime. + Future run({ + bool traceStartup: false, + bool benchmark: false, + Completer observatoryPortCompleter + }) { + // Don't let uncaught errors kill the process. + return runZoned(() { + return _run( + traceStartup: traceStartup, + benchmark: benchmark, + observatoryPortCompleter: observatoryPortCompleter + ); + }, onError: (dynamic error, StackTrace stackTrace) { + printError('Exception from flutter run: $error', stackTrace); + }); + } + + Future restart() async { + if (observatory == null) { + printError('Debugging is not enabled.'); + return false; + } else { + Status status = logger.startProgress('Re-starting application...'); + + Future extensionAddedEvent = observatory.onExtensionEvent + .where((Event event) => event.extensionKind == 'Flutter.FrameworkInitialization') + .first; + + bool restartResult = await device.restartApp( + _package, + _result, + mainPath: _mainPath, + observatory: observatory + ); + + status.stop(showElapsedTime: true); + + if (restartResult) { + // TODO(devoncarew): We should restore the route here. + await extensionAddedEvent; + } + + return restartResult; + } + } + + Future stop() { + _stopLogger(); + return _stopApp(); + } + + Future _run({ + bool traceStartup: false, + bool benchmark: false, + Completer observatoryPortCompleter + }) 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; + } + + Stopwatch startTime = new Stopwatch()..start(); + + // 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.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))) + return 1; + } + + Map platformArgs; + if (traceStartup != null) + platformArgs = { 'trace-startup': traceStartup }; + + 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); + }); + + _result = await device.startApp( + _package, + debuggingOptions.buildMode, + mainPath: _mainPath, + debuggingOptions: debuggingOptions, + platformArgs: platformArgs + ); + + if (!_result.started) { + printError('Error running application on ${device.name}.'); + await _loggingSubscription.cancel(); + return 2; + } + + startTime.stop(); + + if (observatoryPortCompleter != null && _result.hasObservatory) + observatoryPortCompleter.complete(_result.observatoryPort); + + // 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 (benchmark) + await observatory.waitFirstIsolate; + + // Listen for observatory connection close. + observatory.done.whenComplete(() { + if (!_exitCompleter.isCompleted) { + printStatus('Application finished.'); + _exitCompleter.complete(0); + } + }); + } + + printStatus('Application running.'); + + if (observatory != null && traceStartup) { + printStatus('Downloading startup trace info...'); + + await downloadStartupTrace(observatory); + + if (!_exitCompleter.isCompleted) + _exitCompleter.complete(0); + } else { + if (usesTerminalUI) { + if (!logger.quiet) + _printHelp(); + + terminal.singleCharMode = true; + terminal.onCharInput.listen((String code) { + String lower = code.toLowerCase(); + + if (lower == 'h' || code == AnsiTerminal.KEY_F1) { + // F1, help + _printHelp(); + } else if (lower == 'r' || code == AnsiTerminal.KEY_F5) { + // F5, restart + restart(); + } else if (lower == 'q' || code == AnsiTerminal.KEY_F10) { + // F10, exit + _stopApp(); + } + }); + } + + ProcessSignal.SIGINT.watch().listen((ProcessSignal signal) { + _resetTerminal(); + _stopLogger(); + _stopApp(); + }); + ProcessSignal.SIGTERM.watch().listen((ProcessSignal signal) { + _resetTerminal(); + _stopLogger(); + _stopApp(); + }); + } + + if (benchmark) { + await new Future.delayed(new Duration(seconds: 4)); + + // Touch the file. + File mainFile = new File(_mainPath); + mainFile.writeAsBytesSync(mainFile.readAsBytesSync()); + + Stopwatch restartTime = new Stopwatch()..start(); + bool restarted = await restart(); + restartTime.stop(); + writeRunBenchmarkFile(startTime, restarted ? restartTime : null); + await new Future.delayed(new Duration(seconds: 2)); + stop(); + } + + return _exitCompleter.future.then((int exitCode) async { + _resetTerminal(); + _stopLogger(); + return exitCode; + }); + } + + void _printHelp() { + printStatus('Type "h" or F1 for help, "r" or F5 to restart the app, and "q", F10, or ctrl-c to quit.'); + } + + void _stopLogger() { + _loggingSubscription?.cancel(); + } + + void _resetTerminal() { + if (usesTerminalUI) + terminal.singleCharMode = false; + } + + Future _stopApp() { + if (observatory != null && !observatory.isClosed) { + if (observatory.isolates.isNotEmpty) { + observatory.flutterExit(observatory.firstIsolateId); + return new Future.delayed(new Duration(milliseconds: 100)); + } + } + + if (!_exitCompleter.isCompleted) + _exitCompleter.complete(0); + + return new Future.value(); + } +} + +String getMissingPackageHintForPlatform(TargetPlatform platform) { + switch (platform) { + case TargetPlatform.android_arm: + case TargetPlatform.android_x64: + return 'Is your project missing an android/AndroidManifest.xml?'; + case TargetPlatform.ios: + return 'Is your project missing an ios/Info.plist?'; + default: + return null; + } +} + +void writeRunBenchmarkFile(Stopwatch startTime, [Stopwatch restartTime]) { + final String benchmarkOut = 'refresh_benchmark.json'; + Map data = { + 'start': startTime.elapsedMilliseconds, + 'time': (restartTime ?? startTime).elapsedMilliseconds // time and restart are the same + }; + if (restartTime != null) + data['restart'] = restartTime.elapsedMilliseconds; + + new File(benchmarkOut).writeAsStringSync(toPrettyJson(data)); + printStatus('Run benchmark written to $benchmarkOut ($data).'); +} diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 2917a5a4f84..cb476b5dfe3 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -193,7 +193,7 @@ abstract class FlutterCommand extends Command { // Validate the current package map only if we will not be running "pub get" later. if (!(_usesPubOption && argResults['pub'])) { - String error = PackageMap.instance.checkValid(); + String error = new PackageMap(PackageMap.globalPackagesPath).checkValid(); if (error != null) { printError(error); return false; diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart index 98f176b859f..18a79b045f4 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart @@ -148,9 +148,8 @@ class FlutterCommandRunner extends CommandRunner { if (!_checkFlutterCopy()) return new Future.value(1); - PackageMap.instance = new PackageMap(path.normalize(path.absolute( - globalResults.wasParsed('packages') ? globalResults['packages'] : kPackagesFileName - ))); + if (globalResults.wasParsed('packages')) + PackageMap.globalPackagesPath = path.normalize(path.absolute(globalResults['packages'])); // See if the user specified a specific device. deviceManager.specifiedDeviceId = globalResults['device-id']; @@ -191,7 +190,7 @@ class FlutterCommandRunner extends CommandRunner { if (engineSourcePath == null && globalResults['local-engine'] != null) { try { - Uri engineUri = PackageMap.instance.map[kFlutterEnginePackageName]; + Uri engineUri = new PackageMap(PackageMap.globalPackagesPath).map[kFlutterEnginePackageName]; engineSourcePath = path.dirname(path.dirname(path.dirname(path.dirname(engineUri.path)))); bool dirExists = FileSystemEntity.isDirectorySync(path.join(engineSourcePath, 'out')); if (engineSourcePath == '/' || engineSourcePath.isEmpty || !dirExists) diff --git a/packages/flutter_tools/lib/src/services.dart b/packages/flutter_tools/lib/src/services.dart index 7f8b3818b4f..2fe5947794f 100644 --- a/packages/flutter_tools/lib/src/services.dart +++ b/packages/flutter_tools/lib/src/services.dart @@ -30,7 +30,7 @@ Future parseServiceConfigs( ) async { Map packageMap; try { - packageMap = PackageMap.instance.map; + packageMap = new PackageMap(PackageMap.globalPackagesPath).map; } on FormatException catch(e) { printTrace('Invalid ".packages" file while parsing service configs:\n$e'); return; diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart index 43d3401ea71..d3e6ecbd7e1 100644 --- a/packages/flutter_tools/lib/src/test/flutter_platform.dart +++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart @@ -15,6 +15,7 @@ import 'package:test/src/runner/plugin/platform.dart'; // ignore: implementation import 'package:test/src/runner/plugin/hack_register_platform.dart' as hack; // ignore: implementation_imports import '../dart/package_map.dart'; +import '../globals.dart'; final String _kSkyShell = Platform.environment['SKY_SHELL']; const String _kHost = '127.0.0.1'; @@ -46,12 +47,15 @@ Future<_ServerInfo> _startServer() async { Future _startProcess(String mainPath, { String packages }) { assert(shellPath != null || _kSkyShell != null); // Please provide the path to the shell in the SKY_SHELL environment variable. - return Process.start(shellPath ?? _kSkyShell, [ + String executable = shellPath ?? _kSkyShell; + List arguments = [ '--enable-checked-mode', '--non-interactive', '--packages=$packages', - mainPath, - ], environment: { 'FLUTTER_TEST': 'true' }); + mainPath + ]; + printTrace('$executable ${arguments.join(' ')}'); + return Process.start(executable, arguments, environment: { 'FLUTTER_TEST': 'true' }); } void _attachStandardStreams(Process process) { @@ -102,7 +106,7 @@ void main() { '''); Process process = await _startProcess( - listenerFile.path, packages: PackageMap.instance.packagesPath + listenerFile.path, packages: PackageMap.globalPackagesPath ); _attachStandardStreams(process); diff --git a/packages/flutter_tools/test/context_test.dart b/packages/flutter_tools/test/context_test.dart index 8141909618a..51e5464deeb 100644 --- a/packages/flutter_tools/test/context_test.dart +++ b/packages/flutter_tools/test/context_test.dart @@ -14,7 +14,7 @@ void main() { BufferLogger mockLogger = new BufferLogger(); context[Logger] = mockLogger; - context.runInZone(() { + await context.runInZone(() { printError('foo bar'); }); @@ -28,7 +28,7 @@ void main() { BufferLogger mockLogger = new BufferLogger(); context[Logger] = mockLogger; - context.runInZone(() { + await context.runInZone(() { printStatus('foo bar'); }); @@ -42,7 +42,7 @@ void main() { BufferLogger mockLogger = new BufferLogger(); context[Logger] = mockLogger; - context.runInZone(() { + await context.runInZone(() { printTrace('foo bar'); }); diff --git a/packages/flutter_tools/test/daemon_test.dart b/packages/flutter_tools/test/daemon_test.dart index 23032e30fd9..0780514e88d 100644 --- a/packages/flutter_tools/test/daemon_test.dart +++ b/packages/flutter_tools/test/daemon_test.dart @@ -94,6 +94,44 @@ void main() { }); }); + _testUsingContext('daemon.start', () async { + DaemonCommand command = new DaemonCommand(); + applyMocksToCommand(command); + + StreamController> commands = new StreamController>(); + StreamController> responses = new StreamController>(); + daemon = new Daemon( + commands.stream, + (Map result) => responses.add(result), + daemonCommand: command, + notifyingLogger: notifyingLogger + ); + + commands.add({ 'id': 0, 'method': 'app.start' }); + Map response = await responses.stream.where(_notEvent).first; + expect(response['id'], 0); + expect(response['error'], contains('deviceId is required')); + }); + + _testUsingContext('daemon.restart', () async { + DaemonCommand command = new DaemonCommand(); + applyMocksToCommand(command); + + StreamController> commands = new StreamController>(); + StreamController> responses = new StreamController>(); + daemon = new Daemon( + commands.stream, + (Map result) => responses.add(result), + daemonCommand: command, + notifyingLogger: notifyingLogger + ); + + commands.add({ 'id': 0, 'method': 'app.restart' }); + Map response = await responses.stream.where(_notEvent).first; + expect(response['id'], 0); + expect(response['error'], contains('appId is required')); + }); + _testUsingContext('daemon.stop', () async { DaemonCommand command = new DaemonCommand(); applyMocksToCommand(command); @@ -110,7 +148,7 @@ void main() { commands.add({ 'id': 0, 'method': 'app.stop' }); Map response = await responses.stream.where(_notEvent).first; expect(response['id'], 0); - expect(response['error'], contains('deviceId is required')); + expect(response['error'], contains('appId is required')); }); _testUsingContext('device.getDevices', () async { diff --git a/packages/flutter_tools/tool/daemon_client.dart b/packages/flutter_tools/tool/daemon_client.dart index f11fc0f0a95..2cf2cd6d25d 100644 --- a/packages/flutter_tools/tool/daemon_client.dart +++ b/packages/flutter_tools/tool/daemon_client.dart @@ -12,7 +12,7 @@ Process daemon; // version: print version // shutdown: terminate the server // start: start an app -// stopAll: stop any running app +// stop: stop a running app // devices: list devices Future main() async { @@ -27,18 +27,44 @@ Future main() async { stdout.write('> '); stdin.transform(UTF8.decoder).transform(const LineSplitter()).listen((String line) { + List words = line.split(' '); + if (line == 'version' || line == 'v') { _send({'method': 'daemon.version'}); } else if (line == 'shutdown' || line == 'q') { _send({'method': 'daemon.shutdown'}); - } else if (line == 'start') { - _send({'method': 'app.start'}); - } else if (line == 'stopAll') { - _send({'method': 'app.stopAll'}); + } else if (words.first == 'start') { + _send({ + 'method': 'app.start', + 'params': { + 'deviceId': words[1], + 'projectDirectory': words[2] + } + }); + } else if (words.first == 'stop') { + if (words.length > 1) { + _send({ + 'method': 'app.stop', + 'params': { 'appId': words[1] } + }); + } else { + _send({'method': 'app.stop'}); + } + } else if (words.first == 'restart') { + if (words.length > 1) { + _send({ + 'method': 'app.restart', + 'params': { 'appId': words[1] } + }); + } else { + _send({'method': 'app.restart'}); + } } else if (line == 'devices') { _send({'method': 'device.getDevices'}); + } else if (line == 'enable') { + _send({'method': 'device.enable'}); } else { - print('command not understood: $line'); + _send({'method': line.trim()}); } stdout.write('> '); });