diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 64458eea8d..276ec02294 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -11,6 +11,7 @@ import 'package:stack_trace/stack_trace.dart'; import 'src/commands/build.dart'; import 'src/commands/cache.dart'; +import 'src/commands/daemon.dart'; import 'src/commands/flutter_command_runner.dart'; import 'src/commands/init.dart'; import 'src/commands/install.dart'; @@ -28,7 +29,7 @@ import 'src/process.dart'; /// This function is intended to be used from the [flutter] command line tool. Future main(List args) async { // This level can be adjusted by users through the `--verbose` option. - Logger.root.level = Level.SEVERE; + Logger.root.level = Level.WARNING; Logger.root.onRecord.listen((LogRecord record) { if (record.level >= Level.WARNING) { stderr.writeln(record.message); @@ -44,6 +45,7 @@ Future main(List args) async { FlutterCommandRunner runner = new FlutterCommandRunner() ..addCommand(new BuildCommand()) ..addCommand(new CacheCommand()) + ..addCommand(new DaemonCommand()) ..addCommand(new InitCommand()) ..addCommand(new InstallCommand()) ..addCommand(new ListCommand()) diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart new file mode 100644 index 0000000000..78872bd322 --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -0,0 +1,214 @@ +// 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 'package:logging/logging.dart'; + +import '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'; + +final Logger _logging = new Logger('sky_tools.daemon'); + +// TODO: Create a `device` domain in order to list devices and fire events when +// devices are added or removed. + +// TODO: Is this the best name? Server? Daemon? + +/// 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 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}'; +} diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 5fadb74871..4063e86167 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -886,8 +886,10 @@ class AndroidDevice extends Device { '-v', 'tag', // Only log the tag and the message '-s', - 'sky', - 'chromium', + 'sky:V', + 'chromium:D', + 'ActivityManager:W', + '*:F', ], prefix: 'android: '); } diff --git a/packages/flutter_tools/test/all.dart b/packages/flutter_tools/test/all.dart index 91450afa86..924f8485ff 100644 --- a/packages/flutter_tools/test/all.dart +++ b/packages/flutter_tools/test/all.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'android_device_test.dart' as android_device_test; +import 'daemon_test.dart' as daemon_test; import 'init_test.dart' as init_test; import 'install_test.dart' as install_test; import 'listen_test.dart' as listen_test; @@ -15,6 +16,7 @@ import 'trace_test.dart' as trace_test; main() { android_device_test.defineTests(); + daemon_test.defineTests(); init_test.defineTests(); install_test.defineTests(); listen_test.defineTests(); diff --git a/packages/flutter_tools/test/daemon_test.dart b/packages/flutter_tools/test/daemon_test.dart new file mode 100644 index 0000000000..352bb71f91 --- /dev/null +++ b/packages/flutter_tools/test/daemon_test.dart @@ -0,0 +1,80 @@ +// 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 'package:mockito/mockito.dart'; +import 'package:sky_tools/src/commands/daemon.dart'; +import 'package:test/test.dart'; + +import 'src/mocks.dart'; + +main() => defineTests(); + +defineTests() { + group('daemon', () { + Daemon daemon; + + tearDown(() { + if (daemon != null) + return daemon.shutdown(); + }); + + test('daemon.version', () async { + StreamController commands = new StreamController(); + StreamController responses = new StreamController(); + daemon = new Daemon( + commands.stream, + (Map result) => responses.add(result) + ); + commands.add({'id': 0, 'event': 'daemon.version'}); + Map response = await responses.stream.first; + expect(response['id'], 0); + expect(response['result'], isNotEmpty); + expect(response['result'] is String, true); + }); + + test('daemon.shutdown', () async { + StreamController commands = new StreamController(); + StreamController responses = new StreamController(); + daemon = new Daemon( + commands.stream, + (Map result) => responses.add(result) + ); + commands.add({'id': 0, 'event': 'daemon.shutdown'}); + return daemon.onExit.then((int code) { + expect(code, 0); + }); + }); + + test('daemon.stopAll', () async { + DaemonCommand command = new DaemonCommand(); + applyMocksToCommand(command); + + StreamController commands = new StreamController(); + StreamController responses = new StreamController(); + daemon = new Daemon( + commands.stream, + (Map result) => responses.add(result), + daemonCommand: command + ); + + MockDeviceStore mockDevices = command.devices; + + when(mockDevices.android.isConnected()).thenReturn(true); + when(mockDevices.android.stopApp(any)).thenReturn(true); + + when(mockDevices.iOS.isConnected()).thenReturn(false); + when(mockDevices.iOS.stopApp(any)).thenReturn(false); + + when(mockDevices.iOSSimulator.isConnected()).thenReturn(false); + when(mockDevices.iOSSimulator.stopApp(any)).thenReturn(false); + + commands.add({'id': 0, 'event': 'app.stopAll'}); + Map response = await responses.stream.first; + expect(response['id'], 0); + expect(response['result'], true); + }); + }); +} diff --git a/packages/flutter_tools/test/init_test.dart b/packages/flutter_tools/test/init_test.dart index 4cb74fefe7..c14e4f8d4b 100644 --- a/packages/flutter_tools/test/init_test.dart +++ b/packages/flutter_tools/test/init_test.dart @@ -13,7 +13,7 @@ import 'package:test/test.dart'; main() => defineTests(); defineTests() { - group('', () { + group('init', () { Directory temp; setUp(() { @@ -28,7 +28,7 @@ defineTests() { // covered on the linux one. if (!Platform.isWindows) { // Verify that we create a project that is well-formed. - test('init flutter-simple', () async { + test('flutter-simple', () async { InitCommand command = new InitCommand(); CommandRunner runner = new CommandRunner('test_flutter', '') ..addCommand(command); diff --git a/packages/flutter_tools/test/list_test.dart b/packages/flutter_tools/test/list_test.dart index 0b01b075ff..c2fec2183a 100644 --- a/packages/flutter_tools/test/list_test.dart +++ b/packages/flutter_tools/test/list_test.dart @@ -36,7 +36,6 @@ defineTests() { // Instead, cause the test to run the echo command. when(mockDevices.iOSSimulator.xcrunPath).thenReturn(mockCommand); - CommandRunner runner = new CommandRunner('test_flutter', '') ..addCommand(command); runner.run(['list']).then((int code) => expect(code, equals(0))); diff --git a/packages/flutter_tools/tool/daemon_client.dart b/packages/flutter_tools/tool/daemon_client.dart new file mode 100644 index 0000000000..bb21f2008e --- /dev/null +++ b/packages/flutter_tools/tool/daemon_client.dart @@ -0,0 +1,49 @@ +// 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:convert'; +import 'dart:io'; + +Process daemon; + +main() async { + daemon = await Process.start('dart', ['bin/sky_tools.dart', 'daemon']); + print('daemon process started, pid: ${daemon.pid}'); + + daemon.stdout + .transform(UTF8.decoder) + .transform(const LineSplitter()) + .listen((String line) => print('<== ${line}')); + daemon.stderr.listen((data) => stderr.add(data)); + + stdout.write('> '); + stdin.transform(UTF8.decoder).transform(const LineSplitter()).listen((String line) { + if (line == 'version' || line == 'v') { + _send({'event': 'daemon.version'}); + } else if (line == 'shutdown' || line == 'q') { + _send({'event': 'daemon.shutdown'}); + } else if (line == 'start') { + _send({'event': 'app.start'}); + } else if (line == 'stopAll') { + _send({'event': 'app.stopAll'}); + } else { + print('command not understood: ${line}'); + } + stdout.write('> '); + }); + + daemon.exitCode.then((int code) { + print('daemon exiting (${code})'); + exit(code); + }); +} + +int id = 0; + +void _send(Map map) { + map['id'] = id++; + String str = '[${JSON.encode(map)}]'; + daemon.stdin.writeln(str); + print('==> ${str}'); +}