// 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 '../base/logging.dart'; import '../runner/flutter_command.dart'; import 'start.dart'; import 'stop.dart'; const String protocolVersion = '0.0.1'; /// A @domain annotation. const String domain = 'domain'; /// A domain @command annotation. const String command = 'command'; // TODO: Create a `device` domain in order to list devices and fire events when // devices are added or removed. /// 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 { final String name = 'daemon'; final String description = 'Run a persistent, JSON-RPC based server to communicate with devices.'; final String usageFooter = '\nThis command is intended to be used by tooling environments that need ' 'a programatic interface into launching Flutter applications.'; @override Future runInProject() async { print('Starting device daemon...'); Stream 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); }); await downloadApplicationPackagesAndConnectToDevices(); Daemon daemon = new Daemon(commandStream, (Map command) { stdout.writeln('[${JSON.encode(command)}]'); }, daemonCommand: this); return await daemon.onExit; } } typedef void DispatchComand(Map command); typedef Future CommandHandler(dynamic args); class Daemon { final DispatchComand sendCommand; final DaemonCommand daemonCommand; final Completer _onExitCompleter = new Completer(); final Map _domains = {}; Daemon(Stream commandStream, this.sendCommand, {this.daemonCommand}) { // Set up domains. _registerDomain(new DaemonDomain(this)); _registerDomain(new AppDomain(this)); // Start listening. commandStream.listen( (Map command) => _handleCommand(command), onDone: () => _onExitCompleter.complete(0) ); } void _registerDomain(Domain domain) { _domains[domain.name] = domain; } Future get onExit => _onExitCompleter.future; void _handleCommand(Map command) { // {id, event, params} var id = command['id']; if (id == null) { logging.severe('no id for command: $command'); return; } try { String event = command['event']; if (event.indexOf('.') == -1) throw 'command not understood: $event'; String prefix = event.substring(0, event.indexOf('.')); String name = event.substring(event.indexOf('.') + 1); if (_domains[prefix] == null) throw 'no domain for command: $command'; _domains[prefix].handleEvent(name, id, command['params']); } catch (error, trace) { _send({'id': id, 'error': _toJsonable(error)}); logging.warning('error handling ${command['event']}', error, trace); } } void _send(Map map) => sendCommand(map); void shutdown() { if (!_onExitCompleter.isCompleted) _onExitCompleter.complete(0); } } abstract class Domain { final Daemon daemon; final String name; final Map _handlers = {}; Domain(this.daemon, this.name); void registerHandler(String name, CommandHandler handler) { _handlers[name] = handler; } String toString() => name; void handleEvent(String name, dynamic id, dynamic args) { new Future.sync(() { if (_handlers.containsKey(name)) return _handlers[name](args); throw 'command not understood: $name'; }).then((result) { if (result == null) { _send({'id': id}); } else { _send({'id': id, 'result': _toJsonable(result)}); } }).catchError((error, trace) { _send({'id': id, 'error': _toJsonable(error)}); logging.warning('error handling $name', error, trace); }); } void _send(Map map) => daemon._send(map); } /// This domain responds to methods like [version] and [shutdown]. @domain class DaemonDomain extends Domain { DaemonDomain(Daemon daemon) : super(daemon, 'daemon') { registerHandler('version', version); registerHandler('shutdown', shutdown); } @command Future version(dynamic args) { return new Future.value(protocolVersion); } @command Future shutdown(dynamic args) { Timer.run(() => daemon.shutdown()); return new Future.value(); } } /// This domain responds to methods like [start] and [stopAll]. /// /// It'll be extended to fire events for when applications start, stop, and /// log data. @domain class AppDomain extends Domain { AppDomain(Daemon daemon) : super(daemon, 'app') { registerHandler('start', start); registerHandler('stopAll', stopAll); } @command Future start(dynamic args) { // TODO: Add the ability to pass args: target, http, checked StartCommand startComand = new StartCommand(); startComand.inheritFromParent(daemon.daemonCommand); return startComand.runInProject().then((_) => null); } @command Future stopAll(dynamic args) { StopCommand stopCommand = new StopCommand(); stopCommand.inheritFromParent(daemon.daemonCommand); return stopCommand.stop(); } } dynamic _toJsonable(dynamic obj) { if (obj is String || obj is int || obj is bool || obj is Map || obj is List || obj == null) return obj; return '$obj'; }