From 080896a305f9f8404cf97c7b3ffdeee0fb560baa Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Mon, 25 Jan 2016 13:15:01 -0800 Subject: [PATCH] improve device notification support --- .../lib/src/commands/daemon.dart | 97 ++++++++++++++----- packages/flutter_tools/test/daemon_test.dart | 27 +++++- 2 files changed, 99 insertions(+), 25 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 793d6afbe66..cccd32c32a9 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -6,6 +6,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:logging/logging.dart'; + import '../android/adb.dart'; import '../android/device_android.dart'; import '../base/logging.dart'; @@ -14,9 +16,7 @@ import '../runner/flutter_command.dart'; import 'start.dart'; import 'stop.dart' as stop; -const String protocolVersion = '0.0.2'; - -// TODO(devoncarew): Pass logging data back to the client. +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 @@ -28,6 +28,8 @@ class DaemonCommand extends FlutterCommand { final String name = 'daemon'; final String description = 'Run a persistent, JSON-RPC based server to communicate with devices.'; + bool get requiresProjectRoot => false; + Future runInProject() async { print('Starting device daemon...'); @@ -40,8 +42,6 @@ class DaemonCommand extends FlutterCommand { return JSON.decode(line); }); - await downloadApplicationPackagesAndConnectToDevices(); - Daemon daemon = new Daemon(commandStream, (Map command) { stdout.writeln('[${JSON.encode(command, toEncodable: _jsonEncodeObject)}]'); }, daemonCommand: this); @@ -139,11 +139,11 @@ abstract class Domain { String toString() => name; - void handleCommand(String name, dynamic id, dynamic args) { + void handleCommand(String command, dynamic id, dynamic args) { new Future.sync(() { - if (_handlers.containsKey(name)) - return _handlers[name](args); - throw 'command not understood: $name'; + if (_handlers.containsKey(command)) + return _handlers[command](args); + throw 'command not understood: $name.$command'; }).then((result) { if (result == null) { _send({'id': id}); @@ -152,12 +152,12 @@ abstract class Domain { } }).catchError((error, trace) { _send({'id': id, 'error': _toJsonable(error)}); - logging.warning('error handling $name', error, trace); + logging.warning("error handling '$name.$command'", error, trace); }); } void sendEvent(String name, [dynamic args]) { - Map map = { 'method': name }; + Map map = { 'event': name }; if (args != null) map['params'] = _toJsonable(args); _send(map); @@ -169,12 +169,33 @@ abstract class Domain { } /// 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 = Logger.root.onRecord.listen((LogRecord record) { + String message = record.error == null ? record.message : '${record.message}: ${record.error}'; + + if (record.stackTrace != null) { + sendEvent('daemon.logMessage', { + 'level': record.level.name.toLowerCase(), + 'message': message, + 'stackTrace': record.stackTrace.toString() + }); + } else { + sendEvent('daemon.logMessage', { + 'level': record.level.name.toLowerCase(), + 'message': message + }); + } + }); } + StreamSubscription _subscription; + Future version(dynamic args) { return new Future.value(protocolVersion); } @@ -183,6 +204,10 @@ class DaemonDomain extends Domain { Timer.run(() => daemon.shutdown()); return new Future.value(); } + + void dispose() { + _subscription?.cancel(); + } } /// This domain responds to methods like [start] and [stopAll]. @@ -195,19 +220,46 @@ class AppDomain extends Domain { registerHandler('stopAll', stopAll); } - Future start(dynamic args) async { - // TODO: Add the ability to pass args: target, http, checked + Future start(Map args) async { + // TODO(devoncarew): We need to be able to specify the target device. - await Future.wait([ - command.downloadToolchain(), - command.downloadApplicationPackagesAndConnectToDevices(), - ], eagerError: true); + if (args['projectDirectory'] is! String) + throw "A 'projectDirectory' is required"; - return startApp( - command.devices, - command.applicationPackages, - command.toolchain - ).then((int result) => null); + String projectDirectory = args['projectDirectory']; + if (!FileSystemEntity.isDirectorySync(projectDirectory)) + throw "The '$projectDirectory' does not exist"; + + // We change the current working directory for the duration of the `start` + // command. This would have race conditions with other commands happening in + // parallel and doesn't play well with the caching built into `FlutterCommand`. + // TODO(devoncarew): Make flutter_tools work better with commands run from any directory. + // TODO(devoncarew): Use less (or more explicit) caching. + 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, + 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 stopAll(dynamic args) { @@ -328,6 +380,7 @@ class AndroidDeviceDiscovery { Map _deviceToMap(Device device) { return { 'id': device.id, + 'name': device.name, 'platform': _enumToString(device.platform), 'available': device.isConnected() }; diff --git a/packages/flutter_tools/test/daemon_test.dart b/packages/flutter_tools/test/daemon_test.dart index 557079d9c9d..7cfa8c62989 100644 --- a/packages/flutter_tools/test/daemon_test.dart +++ b/packages/flutter_tools/test/daemon_test.dart @@ -4,6 +4,7 @@ import 'dart:async'; +import 'package:flutter_tools/src/base/logging.dart'; import 'package:flutter_tools/src/commands/daemon.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; @@ -29,12 +30,30 @@ defineTests() { (Map result) => responses.add(result) ); commands.add({'id': 0, 'method': 'daemon.version'}); - Map response = await responses.stream.first; + Map response = await responses.stream.where(_notEvent).first; expect(response['id'], 0); expect(response['result'], isNotEmpty); expect(response['result'] is String, true); }); + test('daemon.logMessage', () async { + StreamController commands = new StreamController(); + StreamController responses = new StreamController(); + daemon = new Daemon( + commands.stream, + (Map result) => responses.add(result) + ); + logging.warning('daemon.logMessage test'); + Map response = await responses.stream.where((Map map) { + return map['event'] == 'daemon.logMessage' && map['params']['level'] == 'warning'; + }).first; + expect(response['id'], isNull); + expect(response['event'], 'daemon.logMessage'); + Map logMessage = response['params']; + expect(logMessage['level'], 'warning'); + expect(logMessage['message'], 'daemon.logMessage test'); + }); + test('daemon.shutdown', () async { StreamController commands = new StreamController(); StreamController responses = new StreamController(); @@ -72,7 +91,7 @@ defineTests() { when(mockDevices.iOSSimulator.stopApp(any)).thenReturn(false); commands.add({'id': 0, 'method': 'app.stopAll'}); - Map response = await responses.stream.first; + Map response = await responses.stream.where(_notEvent).first; expect(response['id'], 0); expect(response['result'], true); }); @@ -85,9 +104,11 @@ defineTests() { (Map result) => responses.add(result) ); commands.add({'id': 0, 'method': 'device.getDevices'}); - Map response = await responses.stream.first; + Map response = await responses.stream.where(_notEvent).first; expect(response['id'], 0); expect(response['result'], isList); }); }); } + +bool _notEvent(Map map) => map['event'] == null;