mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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
This commit is contained in:
parent
68ba5bfd7c
commit
3ba17136b7
@ -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<Type, dynamic> _instances = <Type, dynamic>{};
|
||||
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<dynamic, dynamic, StackTrace> onError
|
||||
}) {
|
||||
return runZoned(
|
||||
method,
|
||||
() => _run(method),
|
||||
zoneValues: <String, dynamic>{ 'context': this },
|
||||
onError: onError
|
||||
);
|
||||
}
|
||||
|
||||
dynamic _run(dynamic method()) async {
|
||||
try {
|
||||
_zone = Zone.current;
|
||||
return await method();
|
||||
} finally {
|
||||
_zone = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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<String, dynamic> command);
|
||||
|
||||
typedef Future<dynamic> CommandHandler(dynamic args);
|
||||
typedef Future<dynamic> CommandHandler(Map<String, dynamic> args);
|
||||
|
||||
class Daemon {
|
||||
Daemon(Stream<Map<String, dynamic>> 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 <String, dynamic>{});
|
||||
} catch (error) {
|
||||
_send(<String, dynamic>{'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<String, dynamic> args) {
|
||||
new Future<dynamic>.sync(() {
|
||||
if (_handlers.containsKey(command))
|
||||
return _handlers[command](args);
|
||||
@ -193,6 +193,33 @@ abstract class Domain {
|
||||
|
||||
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() { }
|
||||
}
|
||||
|
||||
@ -222,11 +249,11 @@ class DaemonDomain extends Domain {
|
||||
|
||||
StreamSubscription<LogMessage> _subscription;
|
||||
|
||||
Future<String> version(dynamic args) {
|
||||
Future<String> version(Map<String, dynamic> args) {
|
||||
return new Future<String>.value(protocolVersion);
|
||||
}
|
||||
|
||||
Future<Null> shutdown(dynamic args) {
|
||||
Future<Null> shutdown(Map<String, dynamic> args) {
|
||||
Timer.run(() => daemon.shutdown());
|
||||
return new Future<Null>.value();
|
||||
}
|
||||
@ -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].
|
||||
///
|
||||
/// 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<dynamic> start(Map<String, dynamic> args) async {
|
||||
Device device = await _getDevice(daemon, args);
|
||||
static int _nextAppId = 0;
|
||||
|
||||
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))
|
||||
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', <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 });
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
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;
|
||||
_apps.remove(app);
|
||||
});
|
||||
});
|
||||
|
||||
return <String, dynamic>{ 'appId': app.id };
|
||||
}
|
||||
|
||||
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 {
|
||||
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', <String, dynamic>{ 'log': '$error', 'error': true });
|
||||
app.closeLogger();
|
||||
_apps.remove(app);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> discover(Map<String, dynamic> 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<DiscoveredApp> apps = await device.discoverApps();
|
||||
return apps.map((DiscoveredApp app) =>
|
||||
<String, dynamic>{'id': app.id, 'observatoryDevicePort': app.observatoryPort}
|
||||
).toList();
|
||||
return apps.map((DiscoveredApp app) {
|
||||
return <String, dynamic>{
|
||||
'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<String, dynamic> args]) {
|
||||
Map<String, dynamic> eventArgs = <String, dynamic> { 'appId': app.id };
|
||||
if (args != null)
|
||||
eventArgs.addAll(args);
|
||||
sendEvent('app.$name', eventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
@ -361,7 +446,7 @@ class DeviceDomain extends Domain {
|
||||
|
||||
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) {
|
||||
return discoverer.devices;
|
||||
}).toList();
|
||||
@ -369,56 +454,59 @@ class DeviceDomain extends Domain {
|
||||
}
|
||||
|
||||
/// Enable device events.
|
||||
Future<Null> enable(dynamic args) {
|
||||
for (PollingDeviceDiscovery discoverer in _discoverers) {
|
||||
Future<Null> enable(Map<String, dynamic> args) {
|
||||
for (PollingDeviceDiscovery discoverer in _discoverers)
|
||||
discoverer.startPolling();
|
||||
}
|
||||
return new Future<Null>.value();
|
||||
}
|
||||
|
||||
/// Disable device events.
|
||||
Future<Null> disable(dynamic args) {
|
||||
for (PollingDeviceDiscovery discoverer in _discoverers) {
|
||||
Future<Null> disable(Map<String, dynamic> args) {
|
||||
for (PollingDeviceDiscovery discoverer in _discoverers)
|
||||
discoverer.stopPolling();
|
||||
}
|
||||
return new Future<Null>.value();
|
||||
}
|
||||
|
||||
/// Forward a host port to a device port.
|
||||
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);
|
||||
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 <String, dynamic>{'hostPort': hostPort};
|
||||
return <String, dynamic>{ 'hostPort': hostPort };
|
||||
}
|
||||
|
||||
/// Removes a forwarded port.
|
||||
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)
|
||||
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<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 {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
final String level;
|
||||
final String message;
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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<int> 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<int> 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<int> 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<int> 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<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';
|
||||
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';
|
||||
|
||||
|
@ -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<int> 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;
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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<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) {
|
||||
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<String, Uri> get map {
|
||||
@ -26,8 +34,6 @@ class PackageMap {
|
||||
}
|
||||
Map<String, Uri> _map;
|
||||
|
||||
static PackageMap instance;
|
||||
|
||||
String checkValid() {
|
||||
if (FileSystemEntity.isFileSync(packagesPath))
|
||||
return null;
|
||||
|
@ -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<int> forward(int devicePort, { int hostPort: null });
|
||||
Future<int> forward(int devicePort, { int hostPort });
|
||||
|
||||
/// Stops forwarding [forwardedPort].
|
||||
Future<Null> unforward(ForwardedPort forwardedPort);
|
||||
|
@ -46,7 +46,7 @@ Future<int> createSnapshot({
|
||||
final List<String> args = <String>[
|
||||
tools.getHostToolPath(HostTool.SkySnapshot),
|
||||
mainPath,
|
||||
'--packages=${PackageMap.instance.packagesPath}',
|
||||
'--packages=${path.absolute(PackageMap.globalPackagesPath)}',
|
||||
'--snapshot=$snapshotPath'
|
||||
];
|
||||
if (depfilePath != null)
|
||||
|
338
packages/flutter_tools/lib/src/run.dart
Normal file
338
packages/flutter_tools/lib/src/run.dart
Normal file
@ -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<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 {
|
||||
|
||||
// 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;
|
||||
|
@ -148,9 +148,8 @@ class FlutterCommandRunner extends CommandRunner {
|
||||
if (!_checkFlutterCopy())
|
||||
return new Future<int>.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)
|
||||
|
@ -30,7 +30,7 @@ Future<Null> parseServiceConfigs(
|
||||
) async {
|
||||
Map<String, Uri> 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;
|
||||
|
@ -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<Process> _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>[
|
||||
String executable = shellPath ?? _kSkyShell;
|
||||
List<String> arguments = <String>[
|
||||
'--enable-checked-mode',
|
||||
'--non-interactive',
|
||||
'--packages=$packages',
|
||||
mainPath,
|
||||
], environment: <String, String>{ 'FLUTTER_TEST': 'true' });
|
||||
mainPath
|
||||
];
|
||||
printTrace('$executable ${arguments.join(' ')}');
|
||||
return Process.start(executable, arguments, environment: <String, String>{ '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);
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
|
@ -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 {
|
||||
DaemonCommand command = new DaemonCommand();
|
||||
applyMocksToCommand(command);
|
||||
@ -110,7 +148,7 @@ void main() {
|
||||
commands.add(<String, dynamic>{ 'id': 0, 'method': 'app.stop' });
|
||||
Map<String, dynamic> 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 {
|
||||
|
@ -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<Null> main() async {
|
||||
@ -27,18 +27,44 @@ Future<Null> main() async {
|
||||
|
||||
stdout.write('> ');
|
||||
stdin.transform(UTF8.decoder).transform(const LineSplitter()).listen((String line) {
|
||||
List<String> words = line.split(' ');
|
||||
|
||||
if (line == 'version' || line == 'v') {
|
||||
_send(<String, dynamic>{'method': 'daemon.version'});
|
||||
} else if (line == 'shutdown' || line == 'q') {
|
||||
_send(<String, dynamic>{'method': 'daemon.shutdown'});
|
||||
} else if (line == 'start') {
|
||||
_send(<String, dynamic>{'method': 'app.start'});
|
||||
} else if (line == 'stopAll') {
|
||||
_send(<String, dynamic>{'method': 'app.stopAll'});
|
||||
} else if (words.first == 'start') {
|
||||
_send(<String, dynamic>{
|
||||
'method': 'app.start',
|
||||
'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') {
|
||||
_send(<String, dynamic>{'method': 'device.getDevices'});
|
||||
} else if (line == 'enable') {
|
||||
_send(<String, dynamic>{'method': 'device.enable'});
|
||||
} else {
|
||||
print('command not understood: $line');
|
||||
_send(<String, dynamic>{'method': line.trim()});
|
||||
}
|
||||
stdout.write('> ');
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user