Commit 3ba17136 authored by Devon Carew's avatar Devon Carew

add a restart command to the daemon protocol (#4385)

* refactor the --resident run option into a separate file

* update daemon to run --resident apps

* re-plumbing daemon start

* send app logs

* update tests

* review changes

* fix test runner

* remove PackageMap.createGlobalInstance; rely on the ctor

* review comments
parent 68ba5bfd
...@@ -6,7 +6,7 @@ import 'dart:async'; ...@@ -6,7 +6,7 @@ import 'dart:async';
final AppContext _defaultContext = new AppContext(); 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 /// A singleton for application functionality. This singleton can be different
/// on a per-Zone basis. /// on a per-Zone basis.
...@@ -17,6 +17,7 @@ AppContext get context { ...@@ -17,6 +17,7 @@ AppContext get context {
class AppContext { class AppContext {
Map<Type, dynamic> _instances = <Type, dynamic>{}; Map<Type, dynamic> _instances = <Type, dynamic>{};
Zone _zone;
bool isSet(Type type) { bool isSet(Type type) {
if (_instances.containsKey(type)) if (_instances.containsKey(type))
...@@ -30,7 +31,7 @@ class AppContext { ...@@ -30,7 +31,7 @@ class AppContext {
if (_instances.containsKey(type)) if (_instances.containsKey(type))
return _instances[type]; return _instances[type];
AppContext parent = _calcParent(Zone.current); AppContext parent = _calcParent(_zone ?? Zone.current);
return parent?.getVariable(type); return parent?.getVariable(type);
} }
...@@ -58,11 +59,22 @@ class AppContext { ...@@ -58,11 +59,22 @@ class AppContext {
} }
} }
dynamic runInZone(dynamic method(), { ErrorHandler onError }) { dynamic runInZone(dynamic method(), {
ZoneBinaryCallback<dynamic, dynamic, StackTrace> onError
}) {
return runZoned( return runZoned(
method, () => _run(method),
zoneValues: <String, dynamic>{ 'context': this }, zoneValues: <String, dynamic>{ 'context': this },
onError: onError onError: onError
); );
} }
dynamic _run(dynamic method()) async {
try {
_zone = Zone.current;
return await method();
} finally {
_zone = null;
}
}
} }
...@@ -214,7 +214,6 @@ class AnsiTerminal { ...@@ -214,7 +214,6 @@ class AnsiTerminal {
String writeBold(String str) => supportsColor ? '$_bold$str$_reset' : str; String writeBold(String str) => supportsColor ? '$_bold$str$_reset' : str;
set singleCharMode(bool value) { set singleCharMode(bool value) {
stdin.echoMode = !value;
stdin.lineMode = !value; stdin.lineMode = !value;
} }
......
...@@ -78,6 +78,13 @@ String getSizeAsMB(int bytesLength) { ...@@ -78,6 +78,13 @@ String getSizeAsMB(int bytesLength) {
return '${(bytesLength / (1024 * 1024)).toStringAsFixed(1)}MB'; 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 /// 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 /// removed, and calculate a diff of changes when a new list of items is
/// available. /// available.
......
...@@ -22,6 +22,16 @@ enum BuildMode { ...@@ -22,6 +22,16 @@ enum BuildMode {
String getModeName(BuildMode mode) => getEnumName(mode); 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. // Returns true if the selected build mode uses ahead-of-time compilation.
bool isAotBuildMode(BuildMode mode) { bool isAotBuildMode(BuildMode mode) {
return mode == BuildMode.profile || mode == BuildMode.release; return mode == BuildMode.profile || mode == BuildMode.release;
......
...@@ -13,8 +13,8 @@ import '../base/utils.dart'; ...@@ -13,8 +13,8 @@ import '../base/utils.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../dart/sdk.dart'; import '../dart/sdk.dart';
import '../globals.dart'; import '../globals.dart';
import '../run.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import 'run.dart';
const String _kDefaultAotOutputDir = 'build/aot'; const String _kDefaultAotOutputDir = 'build/aot';
......
...@@ -10,17 +10,17 @@ import 'package:path/path.dart' as path; ...@@ -10,17 +10,17 @@ import 'package:path/path.dart' as path;
import '../android/android_sdk.dart'; import '../android/android_sdk.dart';
import '../base/file_system.dart' show ensureDirectoryExists; import '../base/file_system.dart' show ensureDirectoryExists;
import '../base/os.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/os.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../base/utils.dart'; import '../base/utils.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../flx.dart' as flx; import '../flx.dart' as flx;
import '../globals.dart'; import '../globals.dart';
import '../run.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import '../services.dart'; import '../services.dart';
import 'build_aot.dart'; import 'build_aot.dart';
import 'run.dart';
export '../android/android_device.dart' show AndroidDevice; export '../android/android_device.dart' show AndroidDevice;
......
...@@ -7,7 +7,6 @@ import 'dart:convert'; ...@@ -7,7 +7,6 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import '../android/android_device.dart'; import '../android/android_device.dart';
import '../application_package.dart';
import '../base/context.dart'; import '../base/context.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../build_info.dart'; import '../build_info.dart';
...@@ -15,10 +14,10 @@ import '../device.dart'; ...@@ -15,10 +14,10 @@ import '../device.dart';
import '../globals.dart'; import '../globals.dart';
import '../ios/devices.dart'; import '../ios/devices.dart';
import '../ios/simulators.dart'; import '../ios/simulators.dart';
import '../run.dart';
import '../runner/flutter_command.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. /// 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 /// It reads JSON-RPC based commands from stdin, executes them, and returns
...@@ -73,14 +72,15 @@ class DaemonCommand extends FlutterCommand { ...@@ -73,14 +72,15 @@ class DaemonCommand extends FlutterCommand {
return object; return object;
} }
void _handleError(dynamic error) { dynamic _handleError(dynamic error, StackTrace stackTrace) {
printError('Error from flutter daemon: $error'); printError('Error from flutter daemon: $error', stackTrace);
return null;
} }
} }
typedef void DispatchComand(Map<String, dynamic> command); typedef void DispatchComand(Map<String, dynamic> command);
typedef Future<dynamic> CommandHandler(dynamic args); typedef Future<dynamic> CommandHandler(Map<String, dynamic> args);
class Daemon { class Daemon {
Daemon(Stream<Map<String, dynamic>> commandStream, this.sendCommand, { Daemon(Stream<Map<String, dynamic>> commandStream, this.sendCommand, {
...@@ -137,7 +137,7 @@ class Daemon { ...@@ -137,7 +137,7 @@ class Daemon {
if (_domainMap[prefix] == null) if (_domainMap[prefix] == null)
throw 'no domain for method: $method'; throw 'no domain for method: $method';
_domainMap[prefix].handleCommand(name, id, request['params']); _domainMap[prefix].handleCommand(name, id, request['params'] ?? const <String, dynamic>{});
} catch (error) { } catch (error) {
_send(<String, dynamic>{'id': id, 'error': _toJsonable(error)}); _send(<String, dynamic>{'id': id, 'error': _toJsonable(error)});
} }
...@@ -168,7 +168,7 @@ abstract class Domain { ...@@ -168,7 +168,7 @@ abstract class Domain {
@override @override
String toString() => name; String toString() => name;
void handleCommand(String command, dynamic id, dynamic args) { void handleCommand(String command, dynamic id, Map<String, dynamic> args) {
new Future<dynamic>.sync(() { new Future<dynamic>.sync(() {
if (_handlers.containsKey(command)) if (_handlers.containsKey(command))
return _handlers[command](args); return _handlers[command](args);
...@@ -193,6 +193,33 @@ abstract class Domain { ...@@ -193,6 +193,33 @@ abstract class Domain {
void _send(Map<String, dynamic> map) => daemon._send(map); void _send(Map<String, dynamic> map) => daemon._send(map);
String _getStringArg(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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() { } void dispose() { }
} }
...@@ -222,11 +249,11 @@ class DaemonDomain extends Domain { ...@@ -222,11 +249,11 @@ class DaemonDomain extends Domain {
StreamSubscription<LogMessage> _subscription; StreamSubscription<LogMessage> _subscription;
Future<String> version(dynamic args) { Future<String> version(Map<String, dynamic> args) {
return new Future<String>.value(protocolVersion); return new Future<String>.value(protocolVersion);
} }
Future<Null> shutdown(dynamic args) { Future<Null> shutdown(Map<String, dynamic> args) {
Timer.run(() => daemon.shutdown()); Timer.run(() => daemon.shutdown());
return new Future<Null>.value(); return new Future<Null>.value();
} }
...@@ -237,91 +264,149 @@ class DaemonDomain extends Domain { ...@@ -237,91 +264,149 @@ class DaemonDomain extends Domain {
} }
} }
/// Return the device matching the deviceId field in the args.
Future<Device> _getDevice(Daemon daemon, Map<String, dynamic> args) async {
if (args == null || args['deviceId'] is! String)
throw 'deviceId is required';
List<Device> 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]. /// This domain responds to methods like [start] and [stop].
/// ///
/// It'll be extended to fire events for when applications start, stop, and /// It fires events for application start, stop, and stdout and stderr.
/// log data.
class AppDomain extends Domain { class AppDomain extends Domain {
AppDomain(Daemon daemon) : super(daemon, 'app') { AppDomain(Daemon daemon) : super(daemon, 'app') {
registerHandler('start', start); registerHandler('start', start);
registerHandler('restart', restart);
registerHandler('stop', stop); registerHandler('stop', stop);
registerHandler('discover', discover); registerHandler('discover', discover);
} }
Future<dynamic> start(Map<String, dynamic> args) async { static int _nextAppId = 0;
Device device = await _getDevice(daemon, args);
static String _getNextAppId() => 'app-${_nextAppId++}';
List<AppInstance> _apps = <AppInstance>[];
Future<Map<String, dynamic>> start(Map<String, dynamic> 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)) if (!FileSystemEntity.isDirectorySync(projectDirectory))
throw "'$projectDirectory' does not exist"; 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. // 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 cwd = Directory.current;
Directory.current = new Directory(projectDirectory); Directory.current = new Directory(projectDirectory);
try { RunAndStayResident runner = new RunAndStayResident(
int result = await startApp(
device, device,
stop: true, target: target,
target: args['target'], debuggingOptions: options,
route: args['route'] usesTerminalUI: false
); );
if (result != 0) AppInstance app = new AppInstance(_getNextAppId(), runner);
throw 'Error starting app: $result'; _apps.add(app);
} finally { _sendAppEvent(app, 'start', <String, dynamic>{
'directory': projectDirectory,
'deviceId': deviceId
});
Completer<int> observatoryPortCompleter;
if (options.debuggingEnabled) {
observatoryPortCompleter = new Completer<int>();
observatoryPortCompleter.future.then((int port) {
_sendAppEvent(app, 'debugPort', <String, dynamic>{ 'port': port });
});
}
app._runInZone(this, () {
runner.run(observatoryPortCompleter: observatoryPortCompleter).then((_) {
_sendAppEvent(app, 'stop');
}).catchError((dynamic error) {
_sendAppEvent(app, 'stop', <String, dynamic>{ 'error' : error.toString() });
}).whenComplete(() {
Directory.current = cwd; Directory.current = cwd;
_apps.remove(app);
});
});
return <String, dynamic>{ 'appId': app.id };
} }
return null; Future<bool> restart(Map<String, dynamic> 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<bool> stop(Map<String, dynamic> args) async { Future<bool> stop(Map<String, dynamic> args) async {
Device device = await _getDevice(daemon, args); String appId = _getStringArg(args, 'appId', required: true);
AppInstance app = _getApp(appId);
if (app == null)
throw "app '$appId' not found";
return app.stop().timeout(new Duration(seconds: 5)).then((_) {
return true;
}).catchError((dynamic error) {
_sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
app.closeLogger();
_apps.remove(app);
return false;
});
}
if (args['projectDirectory'] is! String) Future<List<Map<String, dynamic>>> discover(Map<String, dynamic> args) async {
throw "projectDirectory is required"; String deviceId = _getStringArg(args, 'deviceId', required: true);
String projectDirectory = args['projectDirectory'];
if (!FileSystemEntity.isDirectorySync(projectDirectory))
throw "'$projectDirectory' does not exist";
Directory cwd = Directory.current; Device device = daemon.deviceDomain._getDevice(deviceId);
Directory.current = new Directory(projectDirectory); if (device == null)
throw "device '$deviceId' not found";
try { List<DiscoveredApp> apps = await device.discoverApps();
ApplicationPackage app = command.applicationPackages.getPackageForPlatform(device.platform); return apps.map((DiscoveredApp app) {
return device.stopApp(app); return <String, dynamic>{
} finally { 'id': app.id,
Directory.current = cwd; 'observatoryDevicePort': app.observatoryPort
};
}).toList();
} }
AppInstance _getApp(String id) {
return _apps.firstWhere((AppInstance app) => app.id == id, orElse: () => null);
} }
Future<List<Map<String, dynamic>>> discover(Map<String, dynamic> args) async { void _sendAppEvent(AppInstance app, String name, [Map<String, dynamic> args]) {
Device device = await _getDevice(daemon, args); Map<String, dynamic> eventArgs = <String, dynamic> { 'appId': app.id };
List<DiscoveredApp> apps = await device.discoverApps(); if (args != null)
return apps.map((DiscoveredApp app) => eventArgs.addAll(args);
<String, dynamic>{'id': app.id, 'observatoryDevicePort': app.observatoryPort} sendEvent('app.$name', eventArgs);
).toList();
} }
} }
...@@ -361,7 +446,7 @@ class DeviceDomain extends Domain { ...@@ -361,7 +446,7 @@ class DeviceDomain extends Domain {
List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[]; List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
Future<List<Device>> getDevices([dynamic args]) { Future<List<Device>> getDevices([Map<String, dynamic> args]) {
List<Device> devices = _discoverers.expand((PollingDeviceDiscovery discoverer) { List<Device> devices = _discoverers.expand((PollingDeviceDiscovery discoverer) {
return discoverer.devices; return discoverer.devices;
}).toList(); }).toList();
...@@ -369,56 +454,59 @@ class DeviceDomain extends Domain { ...@@ -369,56 +454,59 @@ class DeviceDomain extends Domain {
} }
/// Enable device events. /// Enable device events.
Future<Null> enable(dynamic args) { Future<Null> enable(Map<String, dynamic> args) {
for (PollingDeviceDiscovery discoverer in _discoverers) { for (PollingDeviceDiscovery discoverer in _discoverers)
discoverer.startPolling(); discoverer.startPolling();
}
return new Future<Null>.value(); return new Future<Null>.value();
} }
/// Disable device events. /// Disable device events.
Future<Null> disable(dynamic args) { Future<Null> disable(Map<String, dynamic> args) {
for (PollingDeviceDiscovery discoverer in _discoverers) { for (PollingDeviceDiscovery discoverer in _discoverers)
discoverer.stopPolling(); discoverer.stopPolling();
}
return new Future<Null>.value(); return new Future<Null>.value();
} }
/// Forward a host port to a device port. /// Forward a host port to a device port.
Future<Map<String, dynamic>> forward(Map<String, dynamic> args) async { Future<Map<String, dynamic>> forward(Map<String, dynamic> args) async {
Device device = await _getDevice(daemon, args); String deviceId = _getStringArg(args, 'deviceId', required: true);
int devicePort = _getIntArg(args, 'devicePort', required: true);
if (args['devicePort'] is! int) int hostPort = _getIntArg(args, 'hostPort');
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); hostPort = await device.portForwarder.forward(devicePort, hostPort: hostPort);
return <String, dynamic>{'hostPort': hostPort}; return <String, dynamic>{ 'hostPort': hostPort };
} }
/// Removes a forwarded port. /// Removes a forwarded port.
Future<Null> unforward(Map<String, dynamic> args) async { Future<Null> unforward(Map<String, dynamic> 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) Device device = daemon.deviceDomain._getDevice(deviceId);
throw 'devicePort is required'; if (device == null)
int devicePort = args['devicePort']; 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 @override
void dispose() { void dispose() {
for (PollingDeviceDiscovery discoverer in _discoverers) { for (PollingDeviceDiscovery discoverer in _discoverers)
discoverer.dispose(); discoverer.dispose();
} }
/// Return the device matching the deviceId field in the args.
Device _getDevice(String deviceId) {
List<Device> 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 { ...@@ -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<bool> restart() => runner.restart();
Future<Null> 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', <String, dynamic>{
'log': message,
'stackTrace': stackTrace.toString(),
'error': true
});
} else {
domain?._sendAppEvent(app, 'log', <String, dynamic>{
'log': message,
'error': true
});
}
}
@override
void printStatus(String message, { bool emphasis: false }) {
domain?._sendAppEvent(app, 'log', <String, dynamic>{ 'log': message });
}
@override
void printTrace(String message) { }
@override
Status startProgress(String message) {
printStatus(message);
return new Status();
}
void close() {
domain = null;
}
}
class LogMessage { class LogMessage {
final String level; final String level;
final String message; final String message;
......
...@@ -20,6 +20,7 @@ import '../dart/sdk.dart'; ...@@ -20,6 +20,7 @@ import '../dart/sdk.dart';
import '../device.dart'; import '../device.dart';
import '../globals.dart'; import '../globals.dart';
import '../ios/simulators.dart' show SimControl, IOSSimulatorUtils; import '../ios/simulators.dart' show SimControl, IOSSimulatorUtils;
import '../run.dart';
import 'build_apk.dart' as build_apk; import 'build_apk.dart' as build_apk;
import 'run.dart'; import 'run.dart';
......
...@@ -5,16 +5,14 @@ ...@@ -5,16 +5,14 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:path/path.dart' as path;
import '../application_package.dart'; import '../application_package.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/logger.dart';
import '../base/utils.dart'; import '../base/utils.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../device.dart'; import '../device.dart';
import '../globals.dart'; import '../globals.dart';
import '../observatory.dart'; import '../observatory.dart';
import '../run.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import 'build_apk.dart'; import 'build_apk.dart';
import 'install.dart'; import 'install.dart';
...@@ -112,15 +110,16 @@ class RunCommand extends RunCommandBase { ...@@ -112,15 +110,16 @@ class RunCommand extends RunCommandBase {
} }
if (argResults['resident']) { if (argResults['resident']) {
_RunAndStayResident runner = new _RunAndStayResident( RunAndStayResident runner = new RunAndStayResident(
deviceForCommand, deviceForCommand,
target: target, target: target,
debuggingOptions: options, debuggingOptions: options
buildMode: getBuildMode()
); );
return runner.run(traceStartup: traceStartup, benchmark: argResults['benchmark']); return runner.run(traceStartup: traceStartup, benchmark: argResults['benchmark']);
} else { } else {
// TODO(devoncarew): Remove this path and support the `--no-resident` option
// using the `RunAndStayResident` class.
return startApp( return startApp(
deviceForCommand, deviceForCommand,
target: target, target: target,
...@@ -160,7 +159,7 @@ Future<int> startApp( ...@@ -160,7 +159,7 @@ Future<int> startApp(
if (package == null) { if (package == null) {
String message = 'No application found for ${device.platform}.'; String message = 'No application found for ${device.platform}.';
String hint = _getMissingPackageHintForPlatform(device.platform); String hint = getMissingPackageHintForPlatform(device.platform);
if (hint != null) if (hint != null)
message += '\n$hint'; message += '\n$hint';
printError(message); printError(message);
...@@ -211,7 +210,7 @@ Future<int> startApp( ...@@ -211,7 +210,7 @@ Future<int> startApp(
if (traceStartup != null) if (traceStartup != null)
platformArgs['trace-startup'] = traceStartup; platformArgs['trace-startup'] = traceStartup;
printStatus('Running ${_getDisplayPath(mainPath)} on ${device.name}...'); printStatus('Running ${getDisplayPath(mainPath)} on ${device.name}...');
LaunchResult result = await device.startApp( LaunchResult result = await device.startApp(
package, package,
...@@ -229,7 +228,7 @@ Future<int> startApp( ...@@ -229,7 +228,7 @@ Future<int> startApp(
} else if (traceStartup) { } else if (traceStartup) {
try { try {
Observatory observatory = await Observatory.connect(result.observatoryPort); Observatory observatory = await Observatory.connect(result.observatoryPort);
await _downloadStartupTrace(observatory); await downloadStartupTrace(observatory);
} catch (error) { } catch (error) {
printError('Error connecting to observatory: $error'); printError('Error connecting to observatory: $error');
return 1; return 1;
...@@ -237,345 +236,7 @@ Future<int> startApp( ...@@ -237,345 +236,7 @@ Future<int> startApp(
} }
if (benchmark) if (benchmark)
_writeBenchmark(stopwatch); writeRunBenchmarkFile(stopwatch);
return result.started ? 0 : 2; 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<int> _exitCompleter;
StreamSubscription<String> _loggingSubscription;
Observatory observatory;
/// Start the app and keep the process running during its lifetime.
Future<int> 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<int> _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<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)))
return 1;
}
Map<String, dynamic> platformArgs;
if (traceStartup != null)
platformArgs = <String, dynamic>{ '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<int>();
// 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<Null>.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<Null>.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<Null>.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<bool> _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<Event> 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<Null> _downloadStartupTrace(Observatory observatory) async {
Tracing tracing = new Tracing(observatory);
Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline(
waitForFirstFrame: true
);
int extractInstantEventTimestamp(String eventName) {
List<Map<String, dynamic>> events = timeline['traceEvents'];
Map<String, dynamic> event = events.firstWhere(
(Map<String, dynamic> 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<String, dynamic> traceInfo = <String, dynamic>{
'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<String, dynamic> data = <String, dynamic>{
'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).');
}
...@@ -11,8 +11,8 @@ import '../base/process.dart'; ...@@ -11,8 +11,8 @@ import '../base/process.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../flx.dart' as flx; import '../flx.dart' as flx;
import '../globals.dart'; import '../globals.dart';
import '../run.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import 'run.dart';
const String _kDefaultBundlePath = 'build/app.flx'; const String _kDefaultBundlePath = 'build/app.flx';
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:async'; import 'dart:async';
import '../application_package.dart'; import '../application_package.dart';
import '../build_info.dart';
import '../device.dart'; import '../device.dart';
import '../globals.dart'; import '../globals.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
...@@ -23,6 +24,11 @@ class StopCommand extends FlutterCommand { ...@@ -23,6 +24,11 @@ class StopCommand extends FlutterCommand {
Future<int> runInProject() async { Future<int> runInProject() async {
Device device = deviceForCommand; Device device = deviceForCommand;
ApplicationPackage app = applicationPackages.getPackageForPlatform(device.platform); 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}.'); printStatus('Stopping apps on ${device.name}.');
return await device.stopApp(app) ? 0 : 1; return await device.stopApp(app) ? 0 : 1;
} }
......
...@@ -8,6 +8,7 @@ import 'dart:io'; ...@@ -8,6 +8,7 @@ import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:test/src/executable.dart' as executable; // ignore: implementation_imports import 'package:test/src/executable.dart' as executable; // ignore: implementation_imports
import '../dart/package_map.dart';
import '../globals.dart'; import '../globals.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import '../test/flutter_platform.dart' as loader; import '../test/flutter_platform.dart' as loader;
...@@ -58,6 +59,7 @@ class TestCommand extends FlutterCommand { ...@@ -58,6 +59,7 @@ class TestCommand extends FlutterCommand {
try { try {
if (testDirectory != null) { if (testDirectory != null) {
printTrace('switching to directory $testDirectory to run tests'); printTrace('switching to directory $testDirectory to run tests');
PackageMap.globalPackagesPath = path.normalize(path.absolute(PackageMap.globalPackagesPath));
Directory.current = testDirectory; Directory.current = testDirectory;
} }
printTrace('running test package with arguments: $testArgs'); printTrace('running test package with arguments: $testArgs');
......
...@@ -147,3 +147,52 @@ class Tracing { ...@@ -147,3 +147,52 @@ class Tracing {
return timeline.response; return timeline.response;
} }
} }
/// Download the startup trace information from the given observatory client and
/// store it to build/start_up_info.json.
Future<Null> downloadStartupTrace(Observatory observatory) async {
Tracing tracing = new Tracing(observatory);
Map<String, dynamic> timeline = await tracing.stopTracingAndDownloadTimeline(
waitForFirstFrame: true
);
int extractInstantEventTimestamp(String eventName) {
List<Map<String, dynamic>> events = timeline['traceEvents'];
Map<String, dynamic> event = events.firstWhere(
(Map<String, dynamic> 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<String, dynamic> traceInfo = <String, dynamic>{
'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}.');
}
...@@ -17,6 +17,14 @@ Map<String, Uri> _parse(String packagesPath) { ...@@ -17,6 +17,14 @@ Map<String, Uri> _parse(String packagesPath) {
class PackageMap { class PackageMap {
PackageMap(this.packagesPath); PackageMap(this.packagesPath);
static String get globalPackagesPath => _globalPackagesPath ?? kPackagesFileName;
static set globalPackagesPath(String value) {
_globalPackagesPath = value;
}
static String _globalPackagesPath;
final String packagesPath; final String packagesPath;
Map<String, Uri> get map { Map<String, Uri> get map {
...@@ -26,8 +34,6 @@ class PackageMap { ...@@ -26,8 +34,6 @@ class PackageMap {
} }
Map<String, Uri> _map; Map<String, Uri> _map;
static PackageMap instance;
String checkValid() { String checkValid() {
if (FileSystemEntity.isFileSync(packagesPath)) if (FileSystemEntity.isFileSync(packagesPath))
return null; return null;
......
...@@ -321,7 +321,7 @@ abstract class DevicePortForwarder { ...@@ -321,7 +321,7 @@ abstract class DevicePortForwarder {
/// Forward [hostPort] on the host to [devicePort] on the device. /// Forward [hostPort] on the host to [devicePort] on the device.
/// If [hostPort] is null, will auto select a host port. /// If [hostPort] is null, will auto select a host port.
/// Returns a Future that completes with the host port. /// Returns a Future that completes with the host port.
Future<int> forward(int devicePort, { int hostPort: null }); Future<int> forward(int devicePort, { int hostPort });
/// Stops forwarding [forwardedPort]. /// Stops forwarding [forwardedPort].
Future<Null> unforward(ForwardedPort forwardedPort); Future<Null> unforward(ForwardedPort forwardedPort);
......
...@@ -46,7 +46,7 @@ Future<int> createSnapshot({ ...@@ -46,7 +46,7 @@ Future<int> createSnapshot({
final List<String> args = <String>[ final List<String> args = <String>[
tools.getHostToolPath(HostTool.SkySnapshot), tools.getHostToolPath(HostTool.SkySnapshot),
mainPath, mainPath,
'--packages=${PackageMap.instance.packagesPath}', '--packages=${path.absolute(PackageMap.globalPackagesPath)}',
'--snapshot=$snapshotPath' '--snapshot=$snapshotPath'
]; ];
if (depfilePath != null) if (depfilePath != null)
......
// 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<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,
bool benchmark: false,
Completer<int> 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<bool> restart() async {
if (observatory == null) {
printError('Debugging is not enabled.');
return false;
} else {
Status status = logger.startProgress('Re-starting application...');
Future<Event> 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<Null> stop() {
_stopLogger();
return _stopApp();
}
Future<int> _run({
bool traceStartup: false,
bool benchmark: false,
Completer<int> 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<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)))
return 1;
}
Map<String, dynamic> platformArgs;
if (traceStartup != null)
platformArgs = <String, dynamic>{ '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<Null>.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<Null>.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<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?';
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<String, dynamic> data = <String, dynamic>{
'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).');
}
...@@ -193,7 +193,7 @@ abstract class FlutterCommand extends Command { ...@@ -193,7 +193,7 @@ abstract class FlutterCommand extends Command {
// Validate the current package map only if we will not be running "pub get" later. // Validate the current package map only if we will not be running "pub get" later.
if (!(_usesPubOption && argResults['pub'])) { if (!(_usesPubOption && argResults['pub'])) {
String error = PackageMap.instance.checkValid(); String error = new PackageMap(PackageMap.globalPackagesPath).checkValid();
if (error != null) { if (error != null) {
printError(error); printError(error);
return false; return false;
......
...@@ -148,9 +148,8 @@ class FlutterCommandRunner extends CommandRunner { ...@@ -148,9 +148,8 @@ class FlutterCommandRunner extends CommandRunner {
if (!_checkFlutterCopy()) if (!_checkFlutterCopy())
return new Future<int>.value(1); return new Future<int>.value(1);
PackageMap.instance = new PackageMap(path.normalize(path.absolute( if (globalResults.wasParsed('packages'))
globalResults.wasParsed('packages') ? globalResults['packages'] : kPackagesFileName PackageMap.globalPackagesPath = path.normalize(path.absolute(globalResults['packages']));
)));
// See if the user specified a specific device. // See if the user specified a specific device.
deviceManager.specifiedDeviceId = globalResults['device-id']; deviceManager.specifiedDeviceId = globalResults['device-id'];
...@@ -191,7 +190,7 @@ class FlutterCommandRunner extends CommandRunner { ...@@ -191,7 +190,7 @@ class FlutterCommandRunner extends CommandRunner {
if (engineSourcePath == null && globalResults['local-engine'] != null) { if (engineSourcePath == null && globalResults['local-engine'] != null) {
try { 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)))); engineSourcePath = path.dirname(path.dirname(path.dirname(path.dirname(engineUri.path))));
bool dirExists = FileSystemEntity.isDirectorySync(path.join(engineSourcePath, 'out')); bool dirExists = FileSystemEntity.isDirectorySync(path.join(engineSourcePath, 'out'));
if (engineSourcePath == '/' || engineSourcePath.isEmpty || !dirExists) if (engineSourcePath == '/' || engineSourcePath.isEmpty || !dirExists)
......
...@@ -30,7 +30,7 @@ Future<Null> parseServiceConfigs( ...@@ -30,7 +30,7 @@ Future<Null> parseServiceConfigs(
) async { ) async {
Map<String, Uri> packageMap; Map<String, Uri> packageMap;
try { try {
packageMap = PackageMap.instance.map; packageMap = new PackageMap(PackageMap.globalPackagesPath).map;
} on FormatException catch(e) { } on FormatException catch(e) {
printTrace('Invalid ".packages" file while parsing service configs:\n$e'); printTrace('Invalid ".packages" file while parsing service configs:\n$e');
return; return;
......
...@@ -15,6 +15,7 @@ import 'package:test/src/runner/plugin/platform.dart'; // ignore: implementation ...@@ -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 'package:test/src/runner/plugin/hack_register_platform.dart' as hack; // ignore: implementation_imports
import '../dart/package_map.dart'; import '../dart/package_map.dart';
import '../globals.dart';
final String _kSkyShell = Platform.environment['SKY_SHELL']; final String _kSkyShell = Platform.environment['SKY_SHELL'];
const String _kHost = '127.0.0.1'; const String _kHost = '127.0.0.1';
...@@ -46,12 +47,15 @@ Future<_ServerInfo> _startServer() async { ...@@ -46,12 +47,15 @@ Future<_ServerInfo> _startServer() async {
Future<Process> _startProcess(String mainPath, { String packages }) { Future<Process> _startProcess(String mainPath, { String packages }) {
assert(shellPath != null || _kSkyShell != null); // Please provide the path to the shell in the SKY_SHELL environment variable. assert(shellPath != null || _kSkyShell != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
return Process.start(shellPath ?? _kSkyShell, <String>[ String executable = shellPath ?? _kSkyShell;
List<String> arguments = <String>[
'--enable-checked-mode', '--enable-checked-mode',
'--non-interactive', '--non-interactive',
'--packages=$packages', '--packages=$packages',
mainPath, mainPath
], environment: <String, String>{ 'FLUTTER_TEST': 'true' }); ];
printTrace('$executable ${arguments.join(' ')}');
return Process.start(executable, arguments, environment: <String, String>{ 'FLUTTER_TEST': 'true' });
} }
void _attachStandardStreams(Process process) { void _attachStandardStreams(Process process) {
...@@ -102,7 +106,7 @@ void main() { ...@@ -102,7 +106,7 @@ void main() {
'''); ''');
Process process = await _startProcess( Process process = await _startProcess(
listenerFile.path, packages: PackageMap.instance.packagesPath listenerFile.path, packages: PackageMap.globalPackagesPath
); );
_attachStandardStreams(process); _attachStandardStreams(process);
......
...@@ -14,7 +14,7 @@ void main() { ...@@ -14,7 +14,7 @@ void main() {
BufferLogger mockLogger = new BufferLogger(); BufferLogger mockLogger = new BufferLogger();
context[Logger] = mockLogger; context[Logger] = mockLogger;
context.runInZone(() { await context.runInZone(() {
printError('foo bar'); printError('foo bar');
}); });
...@@ -28,7 +28,7 @@ void main() { ...@@ -28,7 +28,7 @@ void main() {
BufferLogger mockLogger = new BufferLogger(); BufferLogger mockLogger = new BufferLogger();
context[Logger] = mockLogger; context[Logger] = mockLogger;
context.runInZone(() { await context.runInZone(() {
printStatus('foo bar'); printStatus('foo bar');
}); });
...@@ -42,7 +42,7 @@ void main() { ...@@ -42,7 +42,7 @@ void main() {
BufferLogger mockLogger = new BufferLogger(); BufferLogger mockLogger = new BufferLogger();
context[Logger] = mockLogger; context[Logger] = mockLogger;
context.runInZone(() { await context.runInZone(() {
printTrace('foo bar'); printTrace('foo bar');
}); });
......
...@@ -94,6 +94,44 @@ void main() { ...@@ -94,6 +94,44 @@ void main() {
}); });
}); });
_testUsingContext('daemon.start', () async {
DaemonCommand command = new DaemonCommand();
applyMocksToCommand(command);
StreamController<Map<String, dynamic>> commands = new StreamController<Map<String, dynamic>>();
StreamController<Map<String, dynamic>> responses = new StreamController<Map<String, dynamic>>();
daemon = new Daemon(
commands.stream,
(Map<String, dynamic> result) => responses.add(result),
daemonCommand: command,
notifyingLogger: notifyingLogger
);
commands.add(<String, dynamic>{ 'id': 0, 'method': 'app.start' });
Map<String, dynamic> 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<Map<String, dynamic>> commands = new StreamController<Map<String, dynamic>>();
StreamController<Map<String, dynamic>> responses = new StreamController<Map<String, dynamic>>();
daemon = new Daemon(
commands.stream,
(Map<String, dynamic> result) => responses.add(result),
daemonCommand: command,
notifyingLogger: notifyingLogger
);
commands.add(<String, dynamic>{ 'id': 0, 'method': 'app.restart' });
Map<String, dynamic> response = await responses.stream.where(_notEvent).first;
expect(response['id'], 0);
expect(response['error'], contains('appId is required'));
});
_testUsingContext('daemon.stop', () async { _testUsingContext('daemon.stop', () async {
DaemonCommand command = new DaemonCommand(); DaemonCommand command = new DaemonCommand();
applyMocksToCommand(command); applyMocksToCommand(command);
...@@ -110,7 +148,7 @@ void main() { ...@@ -110,7 +148,7 @@ void main() {
commands.add(<String, dynamic>{ 'id': 0, 'method': 'app.stop' }); commands.add(<String, dynamic>{ 'id': 0, 'method': 'app.stop' });
Map<String, dynamic> response = await responses.stream.where(_notEvent).first; Map<String, dynamic> response = await responses.stream.where(_notEvent).first;
expect(response['id'], 0); expect(response['id'], 0);
expect(response['error'], contains('deviceId is required')); expect(response['error'], contains('appId is required'));
}); });
_testUsingContext('device.getDevices', () async { _testUsingContext('device.getDevices', () async {
......
...@@ -12,7 +12,7 @@ Process daemon; ...@@ -12,7 +12,7 @@ Process daemon;
// version: print version // version: print version
// shutdown: terminate the server // shutdown: terminate the server
// start: start an app // start: start an app
// stopAll: stop any running app // stop: stop a running app
// devices: list devices // devices: list devices
Future<Null> main() async { Future<Null> main() async {
...@@ -27,18 +27,44 @@ Future<Null> main() async { ...@@ -27,18 +27,44 @@ Future<Null> main() async {
stdout.write('> '); stdout.write('> ');
stdin.transform(UTF8.decoder).transform(const LineSplitter()).listen((String line) { stdin.transform(UTF8.decoder).transform(const LineSplitter()).listen((String line) {
List<String> words = line.split(' ');
if (line == 'version' || line == 'v') { if (line == 'version' || line == 'v') {
_send(<String, dynamic>{'method': 'daemon.version'}); _send(<String, dynamic>{'method': 'daemon.version'});
} else if (line == 'shutdown' || line == 'q') { } else if (line == 'shutdown' || line == 'q') {
_send(<String, dynamic>{'method': 'daemon.shutdown'}); _send(<String, dynamic>{'method': 'daemon.shutdown'});
} else if (line == 'start') { } else if (words.first == 'start') {
_send(<String, dynamic>{'method': 'app.start'}); _send(<String, dynamic>{
} else if (line == 'stopAll') { 'method': 'app.start',
_send(<String, dynamic>{'method': 'app.stopAll'}); 'params': <String, dynamic> {
'deviceId': words[1],
'projectDirectory': words[2]
}
});
} else if (words.first == 'stop') {
if (words.length > 1) {
_send(<String, dynamic>{
'method': 'app.stop',
'params': <String, dynamic> { 'appId': words[1] }
});
} else {
_send(<String, dynamic>{'method': 'app.stop'});
}
} else if (words.first == 'restart') {
if (words.length > 1) {
_send(<String, dynamic>{
'method': 'app.restart',
'params': <String, dynamic> { 'appId': words[1] }
});
} else {
_send(<String, dynamic>{'method': 'app.restart'});
}
} else if (line == 'devices') { } else if (line == 'devices') {
_send(<String, dynamic>{'method': 'device.getDevices'}); _send(<String, dynamic>{'method': 'device.getDevices'});
} else if (line == 'enable') {
_send(<String, dynamic>{'method': 'device.enable'});
} else { } else {
print('command not understood: $line'); _send(<String, dynamic>{'method': line.trim()});
} }
stdout.write('> '); stdout.write('> ');
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment