// 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 'package:meta/meta.dart'; import '../android/android_device.dart'; import '../base/common.dart'; import '../base/context.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; import '../base/terminal.dart'; import '../base/utils.dart'; import '../build_info.dart'; import '../cache.dart'; import '../device.dart'; import '../emulator.dart'; import '../globals.dart'; import '../ios/devices.dart'; import '../ios/simulators.dart'; import '../resident_runner.dart'; import '../run_cold.dart'; import '../run_hot.dart'; import '../runner/flutter_command.dart'; import '../tester/flutter_tester.dart'; import '../vmservice.dart'; const String protocolVersion = '0.4.2'; /// 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 runCommand() async { printStatus('Starting device daemon...'); final NotifyingLogger notifyingLogger = NotifyingLogger(); Cache.releaseLockEarly(); await context.run( body: () async { final Daemon daemon = Daemon( stdinCommandStream, stdoutCommandResponse, daemonCommand: this, notifyingLogger: notifyingLogger); final int code = await daemon.onExit; if (code != 0) throwToolExit('Daemon exited with non-zero exit code: $code', exitCode: code); }, overrides: { Logger: () => notifyingLogger, }, ); return null; } } typedef DispatchCommand = void Function(Map command); typedef CommandHandler = Future Function(Map args); class Daemon { Daemon( Stream> commandStream, this.sendCommand, { this.daemonCommand, this.notifyingLogger, this.logToStdout = false, }) { // Set up domains. _registerDomain(daemonDomain = DaemonDomain(this)); _registerDomain(appDomain = AppDomain(this)); _registerDomain(deviceDomain = DeviceDomain(this)); _registerDomain(emulatorDomain = EmulatorDomain(this)); // Start listening. _commandSubscription = commandStream.listen( _handleRequest, onDone: () { if (!_onExitCompleter.isCompleted) _onExitCompleter.complete(0); } ); } DaemonDomain daemonDomain; AppDomain appDomain; DeviceDomain deviceDomain; EmulatorDomain emulatorDomain; StreamSubscription> _commandSubscription; final DispatchCommand sendCommand; final DaemonCommand daemonCommand; final NotifyingLogger notifyingLogger; final bool logToStdout; final Completer _onExitCompleter = Completer(); final Map _domainMap = {}; void _registerDomain(Domain domain) { _domainMap[domain.name] = domain; } Future get onExit => _onExitCompleter.future; void _handleRequest(Map request) { // {id, method, params} // [id] is an opaque type to us. final dynamic id = request['id']; if (id == null) { stderr.writeln('no id for request: $request'); return; } try { final String method = request['method']; if (!method.contains('.')) throw 'method not understood: $method'; final String prefix = method.substring(0, method.indexOf('.')); final 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 {}); } catch (error, trace) { _send({ 'id': id, 'error': _toJsonable(error), 'trace': '$trace', }); } } void _send(Map map) => sendCommand(map); void shutdown({dynamic error}) { _commandSubscription?.cancel(); for (Domain domain in _domainMap.values) domain.dispose(); if (!_onExitCompleter.isCompleted) { if (error == null) _onExitCompleter.complete(0); else _onExitCompleter.completeError(error); } } } abstract class Domain { Domain(this.daemon, this.name); final Daemon daemon; final String name; final Map _handlers = {}; 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 args) { // Remove 'new' once Google catches up to dev4.0 Dart SDK. //ignore: unnecessary_new new Future.sync(() { if (_handlers.containsKey(command)) return _handlers[command](args); throw 'command not understood: $name.$command'; }).then((dynamic result) { if (result == null) { _send({'id': id}); } else { _send({'id': id, 'result': _toJsonable(result)}); } }).catchError((dynamic error, dynamic trace) { _send({ 'id': id, 'error': _toJsonable(error), 'trace': '$trace', }); }); } void sendEvent(String name, [dynamic args]) { final Map map = { 'event': name }; if (args != null) map['params'] = _toJsonable(args); _send(map); } void _send(Map map) => daemon._send(map); String _getStringArg(Map args, String name, { bool required = false }) { if (required && !args.containsKey(name)) throw '$name is required'; final dynamic val = args[name]; if (val != null && val is! String) throw '$name is not a String'; return val; } bool _getBoolArg(Map args, String name, { bool required = false }) { if (required && !args.containsKey(name)) throw '$name is required'; final dynamic val = args[name]; if (val != null && val is! bool) throw '$name is not a bool'; return val; } int _getIntArg(Map args, String name, { bool required = false }) { if (required && !args.containsKey(name)) throw '$name is required'; final 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); sendEvent( 'daemon.connected', { 'version': protocolVersion, 'pid': pid, }, ); _subscription = daemon.notifyingLogger.onMessage.listen((LogMessage message) { if (daemon.logToStdout) { if (message.level == 'status') { // We use `print()` here instead of `stdout.writeln()` in order to // capture the print output for testing. print(message.message); } else if (message.level == 'error') { stderr.writeln(message.message); if (message.stackTrace != null) stderr.writeln(message.stackTrace.toString().trimRight()); } } else { 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 _subscription; Future version(Map args) { return Future.value(protocolVersion); } Future shutdown(Map args) { Timer.run(daemon.shutdown); return Future.value(); } @override void dispose() { _subscription?.cancel(); } } typedef _RunOrAttach = Future Function({ Completer connectionInfoCompleter, Completer appStartedCompleter }); /// 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('restart', restart); registerHandler('callServiceExtension', callServiceExtension); registerHandler('stop', stop); registerHandler('detach', detach); } static final Uuid _uuidGenerator = Uuid(); static String _getNewAppId() => _uuidGenerator.generateV4(); final List _apps = []; Future startApp( Device device, String projectDirectory, String target, String route, DebuggingOptions options, bool enableHotReload, { File applicationBinary, @required bool trackWidgetCreation, String projectRootPath, String packagesFilePath, String dillOutputPath, bool ipv6 = false, String isolateFilter, }) async { if (await device.isLocalEmulator && !options.buildInfo.supportsEmulator) { throw '${toTitleCase(options.buildInfo.modeName)} mode is not supported for emulators.'; } // We change the current working directory for the duration of the `start` command. final Directory cwd = fs.currentDirectory; fs.currentDirectory = fs.directory(projectDirectory); final FlutterDevice flutterDevice = FlutterDevice( device, trackWidgetCreation: trackWidgetCreation, dillOutputPath: dillOutputPath, viewFilter: isolateFilter, ); ResidentRunner runner; if (enableHotReload) { runner = HotRunner( [flutterDevice], target: target, debuggingOptions: options, usesTerminalUI: false, applicationBinary: applicationBinary, projectRootPath: projectRootPath, packagesFilePath: packagesFilePath, dillOutputPath: dillOutputPath, ipv6: ipv6, hostIsIde: true, ); } else { runner = ColdRunner( [flutterDevice], target: target, debuggingOptions: options, usesTerminalUI: false, applicationBinary: applicationBinary, ipv6: ipv6, ); } return launch( runner, ({ Completer connectionInfoCompleter, Completer appStartedCompleter }) => runner.run( connectionInfoCompleter: connectionInfoCompleter, appStartedCompleter: appStartedCompleter, route: route), device, projectDirectory, enableHotReload, cwd); } Future launch( ResidentRunner runner, _RunOrAttach runOrAttach, Device device, String projectDirectory, bool enableHotReload, Directory cwd) async { final AppInstance app = AppInstance(_getNewAppId(), runner: runner, logToStdout: daemon.logToStdout); _apps.add(app); _sendAppEvent(app, 'start', { 'deviceId': device.id, 'directory': projectDirectory, 'supportsRestart': isRestartSupported(enableHotReload, device), }); Completer connectionInfoCompleter; if (runner.debuggingOptions.debuggingEnabled) { connectionInfoCompleter = Completer(); // We don't want to wait for this future to complete and callbacks won't fail. // As it just writes to stdout. connectionInfoCompleter.future.then((DebugConnectionInfo info) { // ignore: unawaited_futures final Map params = { 'port': info.httpUri.port, 'wsUri': info.wsUri.toString(), }; if (info.baseUri != null) params['baseUri'] = info.baseUri; _sendAppEvent(app, 'debugPort', params); }); } final Completer appStartedCompleter = Completer(); // We don't want to wait for this future to complete and callbacks won't fail. // As it just writes to stdout. appStartedCompleter.future.then((_) { // ignore: unawaited_futures _sendAppEvent(app, 'started'); }); await app._runInZone(this, () async { try { await runOrAttach( connectionInfoCompleter: connectionInfoCompleter, appStartedCompleter: appStartedCompleter); _sendAppEvent(app, 'stop'); } catch (error, trace) { _sendAppEvent(app, 'stop', { 'error': _toJsonable(error), 'trace': '$trace', }); } finally { fs.currentDirectory = cwd; _apps.remove(app); } }); return app; } bool isRestartSupported(bool enableHotReload, Device device) => enableHotReload && device.supportsHotRestart; Future _inProgressHotReload; Future restart(Map args) async { final String appId = _getStringArg(args, 'appId', required: true); final bool fullRestart = _getBoolArg(args, 'fullRestart') ?? false; final bool pauseAfterRestart = _getBoolArg(args, 'pause') ?? false; final String restartReason = _getStringArg(args, 'reason'); final AppInstance app = _getApp(appId); if (app == null) throw "app '$appId' not found"; if (_inProgressHotReload != null) throw 'hot restart already in progress'; _inProgressHotReload = app._runInZone(this, () { return app.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart, reason: restartReason); }); return _inProgressHotReload.whenComplete(() { _inProgressHotReload = null; }); } /// Returns an error, or the service extension result (a map with two fixed /// keys, `type` and `method`). The result may have one or more additional keys, /// depending on the specific service extension end-point. For example: /// /// { /// "value":"android", /// "type":"_extensionType", /// "method":"ext.flutter.platformOverride" /// } Future> callServiceExtension(Map args) async { final String appId = _getStringArg(args, 'appId', required: true); final String methodName = _getStringArg(args, 'methodName'); final Map params = args['params'] == null ? {} : castStringKeyedMap(args['params']); final AppInstance app = _getApp(appId); if (app == null) throw "app '$appId' not found"; final Isolate isolate = app.runner.flutterDevices.first.views.first.uiIsolate; final Map result = await isolate.invokeFlutterExtensionRpcRaw(methodName, params: params); if (result == null) throw 'method not available: $methodName'; if (result.containsKey('error')) throw result['error']; return result; } Future stop(Map args) async { final String appId = _getStringArg(args, 'appId', required: true); final AppInstance app = _getApp(appId); if (app == null) throw "app '$appId' not found"; return app.stop().timeout(const Duration(seconds: 5)).then((_) { return true; }).catchError((dynamic error) { _sendAppEvent(app, 'log', { 'log': '$error', 'error': true }); app.closeLogger(); _apps.remove(app); return false; }); } Future detach(Map args) async { final String appId = _getStringArg(args, 'appId', required: true); final AppInstance app = _getApp(appId); if (app == null) throw "app '$appId' not found"; return app.detach().timeout(const Duration(seconds: 5)).then((_) { return true; }).catchError((dynamic error) { _sendAppEvent(app, 'log', { 'log': '$error', 'error': true }); app.closeLogger(); _apps.remove(app); return false; }); } AppInstance _getApp(String id) { return _apps.firstWhere((AppInstance app) => app.id == id, orElse: () => null); } void _sendAppEvent(AppInstance app, String name, [Map args]) { final Map eventArgs = { 'appId': app.id }; if (args != null) eventArgs.addAll(args); sendEvent('app.$name', eventArgs); } } typedef _DeviceEventHandler = void Function(Device device); /// 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); addDeviceDiscoverer(AndroidDevices()); addDeviceDiscoverer(IOSDevices()); addDeviceDiscoverer(IOSSimulators()); addDeviceDiscoverer(FlutterTesterDevices()); } void addDeviceDiscoverer(PollingDeviceDiscovery discoverer) { if (!discoverer.supportsPlatform) return; if (!discoverer.canListAnything) { // This event will affect the client UI. Coordinate changes here // with the Flutter IntelliJ team. sendEvent( 'daemon.showMessage', { 'level': 'warning', 'title': 'Unable to list devices', 'message': 'Unable to discover ${discoverer.name}. Please run ' '"flutter doctor" to diagnose potential issues', }, ); } _discoverers.add(discoverer); discoverer.onAdded.listen(_onDeviceEvent('device.added')); discoverer.onRemoved.listen(_onDeviceEvent('device.removed')); } Future _serializeDeviceEvents = Future.value(); _DeviceEventHandler _onDeviceEvent(String eventName) { return (Device device) { _serializeDeviceEvents = _serializeDeviceEvents.then((_) async { sendEvent(eventName, await _deviceToMap(device)); }); }; } final List _discoverers = []; Future> getDevices([Map args]) async { final List devices = []; for (PollingDeviceDiscovery discoverer in _discoverers) { devices.addAll(await discoverer.devices); } return devices; } /// Enable device events. Future enable(Map args) { for (PollingDeviceDiscovery discoverer in _discoverers) discoverer.startPolling(); return Future.value(); } /// Disable device events. Future disable(Map args) { for (PollingDeviceDiscovery discoverer in _discoverers) discoverer.stopPolling(); return Future.value(); } /// Forward a host port to a device port. Future> forward(Map args) async { final String deviceId = _getStringArg(args, 'deviceId', required: true); final int devicePort = _getIntArg(args, 'devicePort', required: true); int hostPort = _getIntArg(args, 'hostPort'); final Device device = await daemon.deviceDomain._getDevice(deviceId); if (device == null) throw "device '$deviceId' not found"; hostPort = await device.portForwarder.forward(devicePort, hostPort: hostPort); return { 'hostPort': hostPort }; } /// Removes a forwarded port. Future unforward(Map args) async { final String deviceId = _getStringArg(args, 'deviceId', required: true); final int devicePort = _getIntArg(args, 'devicePort', required: true); final int hostPort = _getIntArg(args, 'hostPort', required: true); final Device device = await daemon.deviceDomain._getDevice(deviceId); if (device == null) throw "device '$deviceId' not found"; return device.portForwarder.unforward(ForwardedPort(hostPort, devicePort)); } @override void dispose() { for (PollingDeviceDiscovery discoverer in _discoverers) discoverer.dispose(); } /// Return the device matching the deviceId field in the args. Future _getDevice(String deviceId) async { for (PollingDeviceDiscovery discoverer in _discoverers) { final Device device = (await discoverer.devices).firstWhere((Device device) => device.id == deviceId, orElse: () => null); if (device != null) return device; } return null; } } Stream> 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 command) { stdout.writeln('[${jsonEncodeObject(command)}]'); } String jsonEncodeObject(dynamic object) { return json.encode(object, toEncodable: _toEncodable); } dynamic _toEncodable(dynamic object) { if (object is OperationResult) return _operationResultToMap(object); return object; } Future> _deviceToMap(Device device) async { return { 'id': device.id, 'name': device.name, 'platform': getNameForTargetPlatform(await device.targetPlatform), 'emulator': await device.isLocalEmulator, }; } Map _emulatorToMap(Emulator emulator) { return { 'id': emulator.id, 'name': emulator.name, }; } Map _operationResultToMap(OperationResult result) { final Map map = { 'code': result.code, 'message': result.message, }; if (result.hintMessage != null) map['hintMessage'] = result.hintMessage; if (result.hintId != null) map['hintId'] = result.hintId; return map; } 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 OperationResult) return obj; if (obj is ToolExit) return obj.message; return '$obj'; } class NotifyingLogger extends Logger { final StreamController _messageController = StreamController.broadcast(); Stream get onMessage => _messageController.stream; @override void printError( String message, { StackTrace stackTrace, bool emphasis = false, TerminalColor color, int indent, int hangingIndent, bool wrap, }) { _messageController.add(LogMessage('error', message, stackTrace)); } @override void printStatus( String message, { bool emphasis = false, TerminalColor color, bool newline = true, int indent, int hangingIndent, bool wrap, }) { _messageController.add(LogMessage('status', message)); } @override void printTrace(String message) { // This is a lot of traffic to send over the wire. } @override Status startProgress( String message, { String progressId, bool expectSlowOperation = false, bool multilineOutput, int progressIndicatorPadding = kDefaultStatusPadding, }) { printStatus(message); return Status(); } void dispose() { _messageController.close(); } } /// A running application, started by this daemon. class AppInstance { AppInstance(this.id, { this.runner, this.logToStdout = false }); final String id; final ResidentRunner runner; final bool logToStdout; _AppRunLogger _logger; Future restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) { return runner.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart, reason: reason); } Future stop() => runner.stop(); Future detach() => runner.detach(); void closeLogger() { _logger.close(); } Future _runInZone(AppDomain domain, dynamic method()) { _logger ??= _AppRunLogger(domain, this, parent: logToStdout ? logger : null); return context.run( body: method, overrides: { Logger: () => _logger, }, ); } } /// This domain responds to methods like [getEmulators] and [launch]. class EmulatorDomain extends Domain { EmulatorDomain(Daemon daemon) : super(daemon, 'emulator') { registerHandler('getEmulators', getEmulators); registerHandler('launch', launch); registerHandler('create', create); } EmulatorManager emulators = EmulatorManager(); Future>> getEmulators([Map args]) async { final List list = await emulators.getAllAvailableEmulators(); return list.map>(_emulatorToMap).toList(); } Future launch(Map args) async { final String emulatorId = _getStringArg(args, 'emulatorId', required: true); final List matches = await emulators.getEmulatorsMatching(emulatorId); if (matches.isEmpty) { throw "emulator '$emulatorId' not found"; } else if (matches.length > 1) { throw "multiple emulators match '$emulatorId'"; } else { await matches.first.launch(); } } Future> create(Map args) async { final String name = _getStringArg(args, 'name', required: false); final CreateEmulatorResult res = await emulators.createEmulator(name: name); return { 'success': res.success, 'emulatorName': res.emulatorName, 'error': res.error, }; } } /// A [Logger] which sends log messages to a listening daemon client. /// /// This class can either: /// 1) Send stdout messages and progress events to the client IDE /// 1) Log messages to stdout and send progress events to the client IDE // // TODO(devoncarew): To simplify this code a bit, we could choose to specialize // this class into two, one for each of the above use cases. class _AppRunLogger extends Logger { _AppRunLogger(this.domain, this.app, { this.parent }); AppDomain domain; final AppInstance app; final Logger parent; int _nextProgressId = 0; @override void printError( String message, { StackTrace stackTrace, bool emphasis, TerminalColor color, int indent, int hangingIndent, bool wrap, }) { if (parent != null) { parent.printError( message, stackTrace: stackTrace, emphasis: emphasis, indent: indent, hangingIndent: hangingIndent, wrap: wrap, ); } else { if (stackTrace != null) { _sendLogEvent({ 'log': message, 'stackTrace': stackTrace.toString(), 'error': true }); } else { _sendLogEvent({ 'log': message, 'error': true }); } } } @override void printStatus( String message, { bool emphasis = false, TerminalColor color, bool newline = true, int indent, int hangingIndent, bool wrap, }) { if (parent != null) { parent.printStatus( message, emphasis: emphasis, color: color, newline: newline, indent: indent, hangingIndent: hangingIndent, wrap: wrap, ); } else { _sendLogEvent({'log': message}); } } @override void printTrace(String message) { if (parent != null) { parent.printTrace(message); } else { _sendLogEvent({ 'log': message, 'trace': true }); } } Status _status; @override Status startProgress( String message, { String progressId, bool expectSlowOperation = false, bool multilineOutput, int progressIndicatorPadding = 52, }) { final int id = _nextProgressId++; _sendProgressEvent({ 'id': id.toString(), 'progressId': progressId, 'message': message, }); _status = Status(onFinish: () { _status = null; _sendProgressEvent({ 'id': id.toString(), 'progressId': progressId, 'finished': true }); })..start(); return _status; } void close() { domain = null; } void _sendLogEvent(Map event) { if (domain == null) printStatus('event sent after app closed: $event'); else domain._sendAppEvent(app, 'log', event); } void _sendProgressEvent(Map event) { if (domain == null) printStatus('event sent after app closed: $event'); else domain._sendAppEvent(app, 'progress', event); } } class LogMessage { LogMessage(this.level, this.message, [this.stackTrace]); final String level; final String message; final StackTrace stackTrace; }