|  | // 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 '../application_package.dart'; | 
|  | import '../base/context.dart'; | 
|  | import '../base/logger.dart'; | 
|  | import '../device.dart'; | 
|  | import '../globals.dart'; | 
|  | import '../ios/devices.dart'; | 
|  | import '../ios/simulators.dart'; | 
|  | import '../runner/flutter_command.dart'; | 
|  | import 'run.dart'; | 
|  |  | 
|  | const String protocolVersion = '0.1.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({ bool hideCommand: false }) : _hideCommand = hideCommand; | 
|  |  | 
|  | final String name = 'daemon'; | 
|  | final String description = 'Run a persistent, JSON-RPC based server to communicate with devices.'; | 
|  | final bool _hideCommand; | 
|  |  | 
|  | bool get requiresProjectRoot => false; | 
|  |  | 
|  | bool get hidden => _hideCommand; | 
|  |  | 
|  | Future<int> runInProject() { | 
|  | printStatus('Starting device daemon...'); | 
|  |  | 
|  | AppContext appContext = new AppContext(); | 
|  | NotifyingLogger notifyingLogger = new NotifyingLogger(); | 
|  | appContext[Logger] = notifyingLogger; | 
|  |  | 
|  | return appContext.runInZone(() { | 
|  | Stream<Map<String, dynamic>> commandStream = 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); | 
|  | }); | 
|  |  | 
|  | Daemon daemon = new Daemon(commandStream, (Map command) { | 
|  | stdout.writeln('[${JSON.encode(command, toEncodable: _jsonEncodeObject)}]'); | 
|  | }, daemonCommand: this, notifyingLogger: notifyingLogger); | 
|  |  | 
|  | return daemon.onExit; | 
|  | }); | 
|  | } | 
|  |  | 
|  | dynamic _jsonEncodeObject(dynamic object) { | 
|  | if (object is Device) | 
|  | return _deviceToMap(object); | 
|  | return object; | 
|  | } | 
|  | } | 
|  |  | 
|  | typedef void DispatchComand(Map<String, dynamic> command); | 
|  |  | 
|  | typedef Future<dynamic> CommandHandler(dynamic args); | 
|  |  | 
|  | class Daemon { | 
|  | Daemon(Stream<Map> 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 request) => _handleRequest(request), | 
|  | onDone: () => _onExitCompleter.complete(0) | 
|  | ); | 
|  | } | 
|  |  | 
|  | DaemonDomain daemonDomain; | 
|  | AppDomain appDomain; | 
|  | DeviceDomain deviceDomain; | 
|  |  | 
|  | final DispatchComand 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 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']); | 
|  | } catch (error) { | 
|  | _send({'id': id, 'error': _toJsonable(error)}); | 
|  | } | 
|  | } | 
|  |  | 
|  | void _send(Map 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 = {}; | 
|  |  | 
|  | void registerHandler(String name, CommandHandler handler) { | 
|  | _handlers[name] = handler; | 
|  | } | 
|  |  | 
|  | FlutterCommand get command => daemon.daemonCommand; | 
|  |  | 
|  | String toString() => name; | 
|  |  | 
|  | void handleCommand(String command, dynamic id, dynamic args) { | 
|  | new Future.sync(() { | 
|  | if (_handlers.containsKey(command)) | 
|  | return _handlers[command](args); | 
|  | throw 'command not understood: $name.$command'; | 
|  | }).then((result) { | 
|  | if (result == null) { | 
|  | _send({'id': id}); | 
|  | } else { | 
|  | _send({'id': id, 'result': _toJsonable(result)}); | 
|  | } | 
|  | }).catchError((error, trace) { | 
|  | _send({'id': id, 'error': _toJsonable(error)}); | 
|  | }); | 
|  | } | 
|  |  | 
|  | void sendEvent(String name, [dynamic args]) { | 
|  | Map<String, dynamic> map = { 'event': name }; | 
|  | if (args != null) | 
|  | map['params'] = _toJsonable(args); | 
|  | _send(map); | 
|  | } | 
|  |  | 
|  | void _send(Map map) => daemon._send(map); | 
|  |  | 
|  | 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', { | 
|  | 'level': message.level, | 
|  | 'message': message.message, | 
|  | 'stackTrace': message.stackTrace.toString() | 
|  | }); | 
|  | } else { | 
|  | sendEvent('daemon.logMessage', { | 
|  | 'level': message.level, | 
|  | 'message': message.message | 
|  | }); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | StreamSubscription<LogMessage> _subscription; | 
|  |  | 
|  | Future<String> version(dynamic args) { | 
|  | return new Future.value(protocolVersion); | 
|  | } | 
|  |  | 
|  | Future shutdown(dynamic args) { | 
|  | Timer.run(() => daemon.shutdown()); | 
|  | return new Future.value(); | 
|  | } | 
|  |  | 
|  | void dispose() { | 
|  | _subscription?.cancel(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /// This domain responds to methods like [start] and [stop]. | 
|  | /// | 
|  | /// It'll be extended to fire events for when applications start, stop, and | 
|  | /// log data. | 
|  | class AppDomain extends Domain { | 
|  | AppDomain(Daemon daemon) : super(daemon, 'app') { | 
|  | registerHandler('start', start); | 
|  | registerHandler('stop', stop); | 
|  | } | 
|  |  | 
|  | Future<dynamic> start(Map<String, dynamic> args) async { | 
|  | if (args == null || args['deviceId'] is! String) | 
|  | throw "deviceId is required"; | 
|  | Device device = await _getDevice(args['deviceId']); | 
|  | if (device == null) | 
|  | throw "device '${args['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"; | 
|  |  | 
|  | // 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 { | 
|  | await Future.wait([ | 
|  | command.downloadToolchain(), | 
|  | command.downloadApplicationPackagesAndConnectToDevices(), | 
|  | ], eagerError: true); | 
|  |  | 
|  | int result = await startApp( | 
|  | command.devices, | 
|  | command.applicationPackages, | 
|  | command.toolchain, | 
|  | command.buildConfigurations, | 
|  | stop: true, | 
|  | target: args['target'], | 
|  | route: args['route'], | 
|  | checked: args['checked'] ?? true | 
|  | ); | 
|  |  | 
|  | if (result != 0) | 
|  | throw 'Error starting app: $result'; | 
|  | } finally { | 
|  | Directory.current = cwd; | 
|  | } | 
|  |  | 
|  | return null; | 
|  | } | 
|  |  | 
|  | Future<bool> stop(dynamic args) async { | 
|  | if (args == null || args['deviceId'] is! String) | 
|  | throw "deviceId is required"; | 
|  | Device device = await _getDevice(args['deviceId']); | 
|  | if (device == null) | 
|  | throw "device '${args['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"; | 
|  |  | 
|  | Directory cwd = Directory.current; | 
|  | Directory.current = new Directory(projectDirectory); | 
|  |  | 
|  | try { | 
|  | await Future.wait([ | 
|  | command.downloadToolchain(), | 
|  | command.downloadApplicationPackages(), | 
|  | ], eagerError: true); | 
|  |  | 
|  | ApplicationPackage app = command.applicationPackages.getPackageForPlatform(device.platform); | 
|  | return device.stopApp(app); | 
|  | } finally { | 
|  | Directory.current = cwd; | 
|  | } | 
|  | } | 
|  |  | 
|  | Future<Device> _getDevice(String deviceId) async { | 
|  | List<Device> devices = await daemon.deviceDomain.getDevices(); | 
|  | return devices.firstWhere((Device device) => device.id == deviceId, orElse: () => null); | 
|  | } | 
|  | } | 
|  |  | 
|  | /// 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); | 
|  |  | 
|  | 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([dynamic args]) { | 
|  | List<Device> devices = _discoverers.expand((PollingDeviceDiscovery discoverer) { | 
|  | return discoverer.devices; | 
|  | }).toList(); | 
|  | return new Future.value(devices); | 
|  | } | 
|  |  | 
|  | /// Enable device events. | 
|  | Future enable(dynamic args) { | 
|  | for (PollingDeviceDiscovery discoverer in _discoverers) { | 
|  | discoverer.startPolling(); | 
|  | } | 
|  | return new Future.value(); | 
|  | } | 
|  |  | 
|  | /// Disable device events. | 
|  | Future disable(dynamic args) { | 
|  | for (PollingDeviceDiscovery discoverer in _discoverers) { | 
|  | discoverer.stopPolling(); | 
|  | } | 
|  | return new Future.value(); | 
|  | } | 
|  |  | 
|  | void dispose() { | 
|  | for (PollingDeviceDiscovery discoverer in _discoverers) { | 
|  | discoverer.dispose(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | Map<String, dynamic> _deviceToMap(Device device) { | 
|  | return <String, dynamic>{ | 
|  | 'id': device.id, | 
|  | 'name': device.name, | 
|  | 'platform': _enumToString(device.platform), | 
|  | 'available': true | 
|  | }; | 
|  | } | 
|  |  | 
|  | /// Take an enum value and get the best string representation of that. | 
|  | /// | 
|  | /// toString() on enums returns 'EnumType.enumName'. | 
|  | String _enumToString(dynamic enumValue) { | 
|  | String str = '$enumValue'; | 
|  | if (str.contains('.')) | 
|  | return str.substring(str.indexOf('.') + 1); | 
|  | return str; | 
|  | } | 
|  |  | 
|  | dynamic _toJsonable(dynamic obj) { | 
|  | if (obj is String || obj is int || obj is bool || obj is Map || obj is List || obj == null) | 
|  | return obj; | 
|  | if (obj is Device) | 
|  | return obj; | 
|  | return '$obj'; | 
|  | } | 
|  |  | 
|  | class NotifyingLogger extends Logger { | 
|  | StreamController<LogMessage> _messageController = new StreamController<LogMessage>.broadcast(); | 
|  |  | 
|  | Stream<LogMessage> get onMessage => _messageController.stream; | 
|  |  | 
|  | void printError(String message, [StackTrace stackTrace]) { | 
|  | _messageController.add(new LogMessage('error', message, stackTrace)); | 
|  | } | 
|  |  | 
|  | void printStatus(String message) { | 
|  | _messageController.add(new LogMessage('status', message)); | 
|  | } | 
|  |  | 
|  | void printTrace(String message) { | 
|  | // This is a lot of traffic to send over the wire. | 
|  | } | 
|  | } | 
|  |  | 
|  | class LogMessage { | 
|  | final String level; | 
|  | final String message; | 
|  | final StackTrace stackTrace; | 
|  |  | 
|  | LogMessage(this.level, this.message, [this.stackTrace]); | 
|  | } |