blob: 81f08af7972500c8bee7cdd0d3066c6a832b2dee [file] [log] [blame]
// Copyright 2015 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:convert';
import 'dart:io';
import '../android/android_device.dart';
import '../base/context.dart';
import '../base/logger.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../cache.dart';
import '../device.dart';
import '../globals.dart';
import '../hot.dart';
import '../ios/devices.dart';
import '../ios/simulators.dart';
import '../resident_runner.dart';
import '../run.dart';
import '../runner/flutter_command.dart';
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
/// JSON-RPC based responses and events to stdout.
///
/// It can be shutdown with a `daemon.shutdown` command (or by killing the
/// process).
class DaemonCommand extends FlutterCommand {
DaemonCommand({ this.hidden: false });
@override
final String name = 'daemon';
@override
final String description = 'Run a persistent, JSON-RPC based server to communicate with devices.';
@override
final bool hidden;
@override
Future<int> runCommand() {
printStatus('Starting device daemon...');
AppContext appContext = new AppContext();
NotifyingLogger notifyingLogger = new NotifyingLogger();
appContext[Logger] = notifyingLogger;
Cache.releaseLockEarly();
return appContext.runInZone(() {
Daemon daemon = new Daemon(
stdinCommandStream, stdoutCommandResponse,
daemonCommand: this, notifyingLogger: notifyingLogger);
return daemon.onExit;
}, onError: _handleError);
}
dynamic _handleError(dynamic error, StackTrace stackTrace) {
printError('Error from flutter daemon: $error', stackTrace);
return null;
}
}
typedef void DispatchCommand(Map<String, dynamic> command);
typedef Future<dynamic> CommandHandler(Map<String, dynamic> args);
class Daemon {
Daemon(Stream<Map<String, dynamic>> commandStream, this.sendCommand, {
this.daemonCommand,
this.notifyingLogger
}) {
// Set up domains.
_registerDomain(daemonDomain = new DaemonDomain(this));
_registerDomain(appDomain = new AppDomain(this));
_registerDomain(deviceDomain = new DeviceDomain(this));
// Start listening.
commandStream.listen(
(Map<String, dynamic> request) => _handleRequest(request),
onDone: () {
if (!_onExitCompleter.isCompleted)
_onExitCompleter.complete(0);
}
);
}
DaemonDomain daemonDomain;
AppDomain appDomain;
DeviceDomain deviceDomain;
final DispatchCommand sendCommand;
final DaemonCommand daemonCommand;
final NotifyingLogger notifyingLogger;
final Completer<int> _onExitCompleter = new Completer<int>();
final Map<String, Domain> _domainMap = <String, Domain>{};
void _registerDomain(Domain domain) {
_domainMap[domain.name] = domain;
}
Future<int> get onExit => _onExitCompleter.future;
void _handleRequest(Map<String, dynamic> request) {
// {id, method, params}
// [id] is an opaque type to us.
dynamic id = request['id'];
if (id == null) {
stderr.writeln('no id for request: $request');
return;
}
try {
String method = request['method'];
if (method.indexOf('.') == -1)
throw 'method not understood: $method';
String prefix = method.substring(0, method.indexOf('.'));
String name = method.substring(method.indexOf('.') + 1);
if (_domainMap[prefix] == null)
throw 'no domain for method: $method';
_domainMap[prefix].handleCommand(name, id, request['params'] ?? const <String, dynamic>{});
} catch (error) {
_send(<String, dynamic>{'id': id, 'error': _toJsonable(error)});
}
}
void _send(Map<String, dynamic> map) => sendCommand(map);
void shutdown() {
_domainMap.values.forEach((Domain domain) => domain.dispose());
if (!_onExitCompleter.isCompleted)
_onExitCompleter.complete(0);
}
}
abstract class Domain {
Domain(this.daemon, this.name);
final Daemon daemon;
final String name;
final Map<String, CommandHandler> _handlers = <String, CommandHandler>{};
void registerHandler(String name, CommandHandler handler) {
_handlers[name] = handler;
}
FlutterCommand get command => daemon.daemonCommand;
@override
String toString() => name;
void handleCommand(String command, dynamic id, Map<String, dynamic> args) {
new Future<dynamic>.sync(() {
if (_handlers.containsKey(command))
return _handlers[command](args);
throw 'command not understood: $name.$command';
}).then((dynamic result) {
if (result == null) {
_send(<String, dynamic>{'id': id});
} else {
_send(<String, dynamic>{'id': id, 'result': _toJsonable(result)});
}
}).catchError((dynamic error, dynamic trace) {
_send(<String, dynamic>{'id': id, 'error': _toJsonable(error)});
});
}
void sendEvent(String name, [dynamic args]) {
Map<String, dynamic> map = <String, dynamic>{ 'event': name };
if (args != null)
map['params'] = _toJsonable(args);
_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() { }
}
/// This domain responds to methods like [version] and [shutdown].
///
/// This domain fires the `daemon.logMessage` event.
class DaemonDomain extends Domain {
DaemonDomain(Daemon daemon) : super(daemon, 'daemon') {
registerHandler('version', version);
registerHandler('shutdown', shutdown);
_subscription = daemon.notifyingLogger.onMessage.listen((LogMessage message) {
if (message.stackTrace != null) {
sendEvent('daemon.logMessage', <String, dynamic>{
'level': message.level,
'message': message.message,
'stackTrace': message.stackTrace.toString()
});
} else {
sendEvent('daemon.logMessage', <String, dynamic>{
'level': message.level,
'message': message.message
});
}
});
}
StreamSubscription<LogMessage> _subscription;
Future<String> version(Map<String, dynamic> args) {
return new Future<String>.value(protocolVersion);
}
Future<Null> shutdown(Map<String, dynamic> args) {
Timer.run(() => daemon.shutdown());
return new Future<Null>.value();
}
@override
void dispose() {
_subscription?.cancel();
}
}
/// This domain responds to methods like [start] and [stop].
///
/// 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);
}
static Uuid _uuidGenerator = new Uuid();
static String _getNewAppId() => _uuidGenerator.generateV4();
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') ?? false;
String route = _getStringArg(args, 'route');
String mode = _getStringArg(args, 'mode');
String target = _getStringArg(args, 'target');
bool enableHotReload = _getBoolArg(args, 'hot') ?? kHotReloadDefault;
Device device = daemon.deviceDomain._getOrLocateDevice(deviceId);
if (device == null)
throw "device '$deviceId' not found";
if (!FileSystemEntity.isDirectorySync(projectDirectory))
throw "'$projectDirectory' does not exist";
BuildMode buildMode = getBuildModeForName(mode) ?? BuildMode.debug;
AppInstance app = startApp(
device, projectDirectory, target, route,
buildMode, startPaused, enableHotReload);
return <String, dynamic>{
'appId': app.id,
'deviceId': device.id,
'directory': projectDirectory,
'supportsRestart': isRestartSupported(enableHotReload, device)
};
}
AppInstance startApp(
Device device, String projectDirectory, String target, String route,
BuildMode buildMode, bool startPaused, bool enableHotReload) {
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.
Directory cwd = Directory.current;
Directory.current = new Directory(projectDirectory);
ResidentRunner runner;
if (enableHotReload) {
runner = new HotRunner(
device,
target: target,
debuggingOptions: options,
usesTerminalUI: false
);
} else {
runner = new RunAndStayResident(
device,
target: target,
debuggingOptions: options,
usesTerminalUI: false
);
}
AppInstance app = new AppInstance(_getNewAppId(), runner);
_apps.add(app);
_sendAppEvent(app, 'start', <String, dynamic>{
'deviceId': device.id,
'directory': projectDirectory,
'supportsRestart': isRestartSupported(enableHotReload, device)
});
Completer<DebugConnectionInfo> connectionInfoCompleter;
if (options.debuggingEnabled) {
connectionInfoCompleter = new Completer<DebugConnectionInfo>();
connectionInfoCompleter.future.then((DebugConnectionInfo info) {
Map<String, dynamic> params = <String, dynamic>{ 'port': info.port };
if (info.baseUri != null)
params['baseUri'] = info.baseUri;
_sendAppEvent(app, 'debugPort', params);
});
}
app._runInZone(this, () {
runner.run(connectionInfoCompleter: connectionInfoCompleter, route: route).then((_) {
_sendAppEvent(app, 'stop');
}).catchError((dynamic error) {
_sendAppEvent(app, 'stop', <String, dynamic>{ 'error' : error.toString() });
}).whenComplete(() {
Directory.current = cwd;
_apps.remove(app);
});
});
return app;
}
bool isRestartSupported(bool enableHotReload, Device device) =>
enableHotReload ? device.supportsHotMode : device.supportsRestart;
Future<OperationResult> restart(Map<String, dynamic> args) async {
String appId = _getStringArg(args, 'appId', required: true);
bool fullRestart = _getBoolArg(args, 'fullRestart') ?? false;
bool pauseAfterRestart = _getBoolArg(args, 'pause') ?? false;
AppInstance app = _getApp(appId);
if (app == null)
throw "app '$appId' not found";
return app._runInZone(this, () {
return app.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart);
});
}
Future<bool> stop(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.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 {
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) {
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);
}
}
/// This domain lets callers list and monitor connected devices.
///
/// It exports a `getDevices()` call, as well as firing `device.added` and
/// `device.removed` events.
class DeviceDomain extends Domain {
DeviceDomain(Daemon daemon) : super(daemon, 'device') {
registerHandler('getDevices', getDevices);
registerHandler('enable', enable);
registerHandler('disable', disable);
registerHandler('forward', forward);
registerHandler('unforward', unforward);
PollingDeviceDiscovery deviceDiscovery = new AndroidDevices();
if (deviceDiscovery.supportsPlatform)
_discoverers.add(deviceDiscovery);
deviceDiscovery = new IOSDevices();
if (deviceDiscovery.supportsPlatform)
_discoverers.add(deviceDiscovery);
deviceDiscovery = new IOSSimulators();
if (deviceDiscovery.supportsPlatform)
_discoverers.add(deviceDiscovery);
for (PollingDeviceDiscovery discoverer in _discoverers) {
discoverer.onAdded.listen((Device device) {
sendEvent('device.added', _deviceToMap(device));
});
discoverer.onRemoved.listen((Device device) {
sendEvent('device.removed', _deviceToMap(device));
});
}
}
List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
Future<List<Device>> getDevices([Map<String, dynamic> args]) {
List<Device> devices = _discoverers.expand((PollingDeviceDiscovery discoverer) {
return discoverer.devices;
}).toList();
return new Future<List<Device>>.value(devices);
}
/// Enable device events.
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(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 {
String deviceId = _getStringArg(args, 'deviceId', required: true);
int devicePort = _getIntArg(args, 'devicePort', required: true);
int hostPort = _getIntArg(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 };
}
/// Removes a forwarded port.
Future<Null> unforward(Map<String, dynamic> args) async {
String deviceId = _getStringArg(args, 'deviceId', required: true);
int devicePort = _getIntArg(args, 'devicePort', required: true);
int hostPort = _getIntArg(args, 'hostPort', required: true);
Device device = daemon.deviceDomain._getDevice(deviceId);
if (device == null)
throw "device '$deviceId' not found";
return device.portForwarder.unforward(new ForwardedPort(hostPort, devicePort));
}
@override
void dispose() {
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);
}
/// Return a known matching device, or scan for devices if no known match is found.
Device _getOrLocateDevice(String deviceId) {
// Look for an already known device.
Device device = _getDevice(deviceId);
if (device != null)
return device;
// Scan the different device providers for a match.
for (PollingDeviceDiscovery discoverer in _discoverers) {
List<Device> devices = discoverer.pollingGetDevices();
for (Device device in devices)
if (device.id == deviceId)
return device;
}
// No match found.
return null;
}
}
Stream<Map<String, dynamic>> get stdinCommandStream => stdin
.transform(UTF8.decoder)
.transform(const LineSplitter())
.where((String line) => line.startsWith('[{') && line.endsWith('}]'))
.map((String line) {
line = line.substring(1, line.length - 1);
return JSON.decode(line);
});
void stdoutCommandResponse(Map<String, dynamic> command) {
stdout.writeln('[${JSON.encode(command, toEncodable: _jsonEncodeObject)}]');
}
dynamic _jsonEncodeObject(dynamic object) {
if (object is Device)
return _deviceToMap(object);
if (object is OperationResult)
return _operationResultToMap(object);
return object;
}
Map<String, dynamic> _deviceToMap(Device device) {
return <String, dynamic>{
'id': device.id,
'name': device.name,
'platform': getNameForTargetPlatform(device.platform),
'emulator': device.isLocalEmulator
};
}
Map<String, dynamic> _operationResultToMap(OperationResult result) {
return <String, dynamic>{
'code': result.code,
'message': result.message
};
}
dynamic _toJsonable(dynamic obj) {
if (obj is String || obj is int || obj is bool || obj is Map<dynamic, dynamic> || obj is List<dynamic> || obj == null)
return obj;
if (obj is Device)
return obj;
if (obj is OperationResult)
return obj;
return '$obj';
}
class NotifyingLogger extends Logger {
StreamController<LogMessage> _messageController = new StreamController<LogMessage>.broadcast();
Stream<LogMessage> get onMessage => _messageController.stream;
@override
void printError(String message, [StackTrace stackTrace]) {
_messageController.add(new LogMessage('error', message, stackTrace));
}
@override
void printStatus(String message, { bool emphasis: false, bool newline: true }) {
_messageController.add(new LogMessage('status', message));
}
@override
void printTrace(String message) {
// This is a lot of traffic to send over the wire.
}
@override
Status startProgress(String message) {
printStatus(message);
return new Status();
}
void dispose() {
_messageController.close();
}
}
/// A running application, started by this daemon.
class AppInstance {
AppInstance(this.id, [this.runner]);
final String id;
final ResidentRunner runner;
_AppRunLogger _logger;
Future<OperationResult> restart({ bool fullRestart: false, bool pauseAfterRestart: false }) {
return runner.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart);
}
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;
int _nextProgressId = 0;
@override
void printError(String message, [StackTrace stackTrace]) {
if (stackTrace != null) {
_sendLogEvent(<String, dynamic>{
'log': message,
'stackTrace': stackTrace.toString(),
'error': true
});
} else {
_sendLogEvent(<String, dynamic>{
'log': message,
'error': true
});
}
}
@override
void printStatus(String message, { bool emphasis: false, bool newline: true }) {
_sendLogEvent(<String, dynamic>{ 'log': message });
}
@override
void printTrace(String message) { }
@override
Status startProgress(String message) {
int id = _nextProgressId++;
_sendLogEvent(<String, dynamic>{
'log': message,
'progress': true,
'id': id.toString()
});
return new _AppLoggerStatus(this, id);
}
void close() {
domain = null;
}
void _sendLogEvent(Map<String, dynamic> event) {
if (domain == null)
printStatus('event sent after app closed: $event');
else
domain._sendAppEvent(app, 'log', event);
}
}
class _AppLoggerStatus implements Status {
_AppLoggerStatus(this.logger, this.id);
final _AppRunLogger logger;
final int id;
@override
void stop({ bool showElapsedTime: false }) {
_sendFinished();
}
@override
void cancel() {
_sendFinished();
}
void _sendFinished() {
logger._sendLogEvent(<String, dynamic>{
'progress': true,
'id': id.toString(),
'finished': true
});
}
}
class LogMessage {
final String level;
final String message;
final StackTrace stackTrace;
LogMessage(this.level, this.message, [this.stackTrace]);
}