// Copyright 2014 The Flutter 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 'package:meta/meta.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 '../convert.dart'; import '../device.dart'; import '../emulator.dart'; import '../globals.dart' as globals; import '../project.dart'; import '../resident_runner.dart'; import '../run_cold.dart'; import '../run_hot.dart'; import '../runner/flutter_command.dart'; import '../web/web_runner.dart'; const String protocolVersion = '0.5.3'; /// 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 }) { usesDartDefines(); } @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 { globals.printStatus('Starting device daemon...'); isRunningFromDaemon = true; final NotifyingLogger notifyingLogger = NotifyingLogger(); Cache.releaseLockEarly(); await context.run( body: () async { final Daemon daemon = Daemon( stdinCommandStream, stdoutCommandResponse, notifyingLogger: notifyingLogger, dartDefines: dartDefines, ); final int code = await daemon.onExit; if (code != 0) { throwToolExit('Daemon exited with non-zero exit code: $code', exitCode: code); } }, overrides: { Logger: () => notifyingLogger, }, ); return FlutterCommandResult.success(); } } typedef DispatchCommand = void Function(Map command); typedef CommandHandler = Future Function(Map args); class Daemon { Daemon( Stream> commandStream, this.sendCommand, { this.notifyingLogger, this.logToStdout = false, @required this.dartDefines, }) { if (dartDefines == null) { throw Exception( 'dartDefines must not be null. This is a bug in Flutter.\n' 'Please file an issue at https://github.com/flutter/flutter/issues/new/choose', ); } // 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; int _outgoingRequestId = 1; final Map> _outgoingRequestCompleters = >{}; final DispatchCommand sendCommand; final NotifyingLogger notifyingLogger; final bool logToStdout; final List dartDefines; 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) { globals.stdio.stderrWrite('no id for request: $request\n'); return; } try { final String method = request['method'] as String; if (method != null) { 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, castStringKeyedMap(request['params']) ?? const {}); } else { // If there was no 'method' field then it's a response to a daemon-to-editor request. final Completer completer = _outgoingRequestCompleters[id.toString()]; if (completer == null) { throw 'unexpected response with id: $id'; } _outgoingRequestCompleters.remove(id.toString()); if (request['error'] != null) { completer.completeError(request['error']); } else { completer.complete(request['result']); } } } catch (error, trace) { _send({ 'id': id, 'error': _toJsonable(error), 'trace': '$trace', }); } } Future sendRequest(String method, [ dynamic args ]) { final Map map = {'method': method}; if (args != null) { map['params'] = _toJsonable(args); } final int id = _outgoingRequestId++; final Completer completer = Completer(); map['id'] = id.toString(); _outgoingRequestCompleters[id.toString()] = completer; _send(map); return completer.future; } void _send(Map map) => sendCommand(map); void shutdown({ dynamic error }) { _commandSubscription?.cancel(); for (final 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; } @override String toString() => name; void handleCommand(String command, dynamic id, Map args) { 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 as String; } 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 as bool; } 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 as int; } 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); registerHandler('getSupportedPlatforms', getSupportedPlatforms); 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') { globals.stdio.stderrWrite('${message.message}\n'); if (message.stackTrace != null) { globals.stdio.stderrWrite( '${message.stackTrace.toString().trimRight()}\n', ); } } } 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); } /// Sends a request back to the client asking it to expose/tunnel a URL. /// /// This method should only be called if the client opted-in with the /// --web-allow-expose-url switch. The client may return the same URL back if /// tunnelling is not required for a given URL. Future exposeUrl(String url) async { final dynamic res = await daemon.sendRequest('app.exposeUrl', {'url': url}); if (res is Map && res['url'] is String) { return res['url'] as String; } else { globals.printError('Invalid response to exposeUrl - params should include a String url field'); return url; } } Future shutdown(Map args) { Timer.run(daemon.shutdown); return Future.value(); } @override void dispose() { _subscription?.cancel(); } /// Enumerates the platforms supported by the provided project. /// /// This does not filter based on the current workflow restrictions, such /// as whether command line tools are installed or whether the host platform /// is correct. Future> getSupportedPlatforms(Map args) async { final String projectRoot = _getStringArg(args, 'projectRoot', required: true); final List result = []; try { // TODO(jonahwilliams): replace this with a project metadata check once // that has been implemented. final FlutterProject flutterProject = FlutterProject.fromDirectory(globals.fs.directory(projectRoot)); if (flutterProject.linux.existsSync()) { result.add('linux'); } if (flutterProject.macos.existsSync()) { result.add('macos'); } if (flutterProject.windows.existsSync()) { result.add('windows'); } if (flutterProject.ios.existsSync()) { result.add('ios'); } if (flutterProject.android.existsSync()) { result.add('android'); } if (flutterProject.web.existsSync()) { result.add('web'); } if (flutterProject.fuchsia.existsSync()) { result.add('fuchsia'); } return { 'platforms': result, }; } catch (err, stackTrace) { sendEvent('log', { 'log': 'Failed to parse project metadata', 'stackTrace': stackTrace.toString(), 'error': true, }); // On any sort of failure, fall back to Android and iOS for backwards // comparability. return { 'platforms': [ 'android', 'ios', ], }; } } } 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('reloadMethod', reloadMethod); 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.friendlyModeName)} mode is not supported for emulators.'; } // We change the current working directory for the duration of the `start` command. final Directory cwd = globals.fs.currentDirectory; globals.fs.currentDirectory = globals.fs.directory(projectDirectory); final FlutterProject flutterProject = FlutterProject.current(); final FlutterDevice flutterDevice = await FlutterDevice.create( device, flutterProject: flutterProject, trackWidgetCreation: trackWidgetCreation, viewFilter: isolateFilter, target: target, buildMode: options.buildInfo.mode, dartDefines: daemon.dartDefines, ); ResidentRunner runner; if (await device.targetPlatform == TargetPlatform.web_javascript) { runner = webRunnerFactory.createWebRunner( flutterDevice, flutterProject: flutterProject, target: target, debuggingOptions: options, ipv6: ipv6, stayResident: true, dartDefines: daemon.dartDefines, urlTunneller: options.webEnableExposeUrl ? daemon.daemonDomain.exposeUrl : null, ); } else if (enableHotReload) { runner = HotRunner( [flutterDevice], target: target, debuggingOptions: options, applicationBinary: applicationBinary, projectRootPath: projectRootPath, packagesFilePath: packagesFilePath, dillOutputPath: dillOutputPath, ipv6: ipv6, hostIsIde: true, ); } else { runner = ColdRunner( [flutterDevice], target: target, debuggingOptions: options, applicationBinary: applicationBinary, ipv6: ipv6, ); } return launch( runner, ({ Completer connectionInfoCompleter, Completer appStartedCompleter, }) { return runner.run( connectionInfoCompleter: connectionInfoCompleter, appStartedCompleter: appStartedCompleter, route: route, ); }, device, projectDirectory, enableHotReload, cwd, LaunchMode.run, ); } Future launch( ResidentRunner runner, _RunOrAttach runOrAttach, Device device, String projectDirectory, bool enableHotReload, Directory cwd, LaunchMode launchMode, ) 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), 'launchMode': launchMode.toString(), }); Completer connectionInfoCompleter; if (runner.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. unawaited(connectionInfoCompleter.future.then( (DebugConnectionInfo info) { final Map params = { // The web vmservice proxy does not have an http address. 'port': info.httpUri?.port ?? info.wsUri.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. unawaited(appStartedCompleter.future.then((void value) { _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 { globals.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, pause: pauseAfterRestart, reason: restartReason); }); return _inProgressHotReload.whenComplete(() { _inProgressHotReload = null; }); } Future reloadMethod(Map args) async { final String appId = _getStringArg(args, 'appId', required: true); final String classId = _getStringArg(args, 'class', required: true); final String libraryId = _getStringArg(args, 'library', required: true); 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.reloadMethod(classId: classId, libraryId: libraryId); }); 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 Map result = await app.runner .invokeFlutterExtensionRpcRawOnFirstIsolate(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().then( (void value) => true, onError: (dynamic error, StackTrace stack) { _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().then( (void value) => true, onError: (dynamic error, StackTrace stack) { _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 ]) { sendEvent('app.$name', { 'appId': app.id, ...?args, }); } } 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); // Use the device manager discovery so that client provided device types // are usable via the daemon protocol. deviceManager.deviceDiscoverers.forEach(addDeviceDiscoverer); } void addDeviceDiscoverer(DeviceDiscovery discoverer) { if (!discoverer.supportsPlatform) { return; } if (discoverer is PollingDeviceDiscovery) { _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 { try { final Map response = await _deviceToMap(device); sendEvent(eventName, response); } catch (err) { globals.printError('$err'); } }); }; } final List _discoverers = []; /// Return a list of the current devices, with each device represented as a map /// of properties (id, name, platform, ...). Future>> getDevices([ Map args ]) async { return >[ for (final PollingDeviceDiscovery discoverer in _discoverers) for (final Device device in await discoverer.devices) await _deviceToMap(device), ]; } /// Enable device events. Future enable(Map args) { for (final PollingDeviceDiscovery discoverer in _discoverers) { discoverer.startPolling(); } return Future.value(); } /// Disable device events. Future disable(Map args) { for (final 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 (final PollingDeviceDiscovery discoverer in _discoverers) { discoverer.dispose(); } } /// Return the device matching the deviceId field in the args. Future _getDevice(String deviceId) async { for (final 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 => globals.stdio.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 castStringKeyedMap(json.decode(line)); }); void stdoutCommandResponse(Map command) { globals.stdio.stdoutWrite( '[${jsonEncodeObject(command)}]\n', fallback: (String message, dynamic error, StackTrace stack) { throwToolExit('Failed to write daemon command response to stdout: $error'); }, ); } 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, 'category': device.category?.toString(), 'platformType': device.platformType?.toString(), 'ephemeral': device.ephemeral, 'emulatorId': await device.emulatorId, }; } Map _emulatorToMap(Emulator emulator) { return { 'id': emulator.id, 'name': emulator.name, 'category': emulator.category?.toString(), 'platformType': emulator.platformType?.toString(), }; } Map _operationResultToMap(OperationResult result) { return { 'code': result.code, 'message': result.message, }; } 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, { @required Duration timeout, String progressId, bool multilineOutput = false, int progressIndicatorPadding = kDefaultStatusPadding, }) { assert(timeout != null); printStatus(message); return SilentStatus( timeout: timeout, timeoutConfiguration: timeoutConfiguration, stopwatch: Stopwatch(), ); } void dispose() { _messageController.close(); } @override void sendEvent(String name, [Map args]) { } @override bool get supportsColor => throw UnimplementedError(); @override bool get hasTerminal => false; // This method is only relevant for terminals. @override void clear() { } } /// 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 pause = false, String reason }) { return runner.restart(fullRestart: fullRestart, pause: pause, reason: reason); } Future reloadMethod({ String classId, String libraryId }) { return runner.reloadMethod(classId: classId, libraryId: libraryId); } Future stop() => runner.exit(); Future detach() => runner.detach(); void closeLogger() { _logger.close(); } Future _runInZone(AppDomain domain, FutureOr method()) { _logger ??= _AppRunLogger(domain, this, parent: logToStdout ? globals.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, { @required Duration timeout, String progressId, bool multilineOutput = false, int progressIndicatorPadding = kDefaultStatusPadding, }) { assert(timeout != null); final int id = _nextProgressId++; _sendProgressEvent({ 'id': id.toString(), 'progressId': progressId, 'message': message, }); _status = SilentStatus( timeout: timeout, timeoutConfiguration: timeoutConfiguration, onFinish: () { _status = null; _sendProgressEvent({ 'id': id.toString(), 'progressId': progressId, 'finished': true, }); }, stopwatch: Stopwatch())..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); } } @override void sendEvent(String name, [Map args]) { if (domain == null) { printStatus('event sent after app closed: $name'); } else { domain.sendEvent(name, args); } } @override bool get supportsColor => throw UnimplementedError(); @override bool get hasTerminal => false; // This method is only relevant for terminals. @override void clear() { } } class LogMessage { LogMessage(this.level, this.message, [this.stackTrace]); final String level; final String message; final StackTrace stackTrace; } /// The method by which the flutter app was launched. class LaunchMode { const LaunchMode._(this._value); /// The app was launched via `flutter run`. static const LaunchMode run = LaunchMode._('run'); /// The app was launched via `flutter attach`. static const LaunchMode attach = LaunchMode._('attach'); final String _value; @override String toString() => _value; }