Commit 7737117a authored by Devon Carew's avatar Devon Carew

Merge pull request #1369 from devoncarew/improve_device_support

improve device notification support
parents a46fb2c4 080896a3
...@@ -6,6 +6,8 @@ import 'dart:async'; ...@@ -6,6 +6,8 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:logging/logging.dart';
import '../android/adb.dart'; import '../android/adb.dart';
import '../android/device_android.dart'; import '../android/device_android.dart';
import '../base/logging.dart'; import '../base/logging.dart';
...@@ -14,9 +16,7 @@ import '../runner/flutter_command.dart'; ...@@ -14,9 +16,7 @@ import '../runner/flutter_command.dart';
import 'start.dart'; import 'start.dart';
import 'stop.dart' as stop; import 'stop.dart' as stop;
const String protocolVersion = '0.0.2'; const String protocolVersion = '0.1.0';
// TODO(devoncarew): Pass logging data back to the client.
/// A server process command. This command will start up a long-lived server. /// 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 /// It reads JSON-RPC based commands from stdin, executes them, and returns
...@@ -28,6 +28,8 @@ class DaemonCommand extends FlutterCommand { ...@@ -28,6 +28,8 @@ class DaemonCommand extends FlutterCommand {
final String name = 'daemon'; final String name = 'daemon';
final String description = 'Run a persistent, JSON-RPC based server to communicate with devices.'; final String description = 'Run a persistent, JSON-RPC based server to communicate with devices.';
bool get requiresProjectRoot => false;
Future<int> runInProject() async { Future<int> runInProject() async {
print('Starting device daemon...'); print('Starting device daemon...');
...@@ -40,8 +42,6 @@ class DaemonCommand extends FlutterCommand { ...@@ -40,8 +42,6 @@ class DaemonCommand extends FlutterCommand {
return JSON.decode(line); return JSON.decode(line);
}); });
await downloadApplicationPackagesAndConnectToDevices();
Daemon daemon = new Daemon(commandStream, (Map command) { Daemon daemon = new Daemon(commandStream, (Map command) {
stdout.writeln('[${JSON.encode(command, toEncodable: _jsonEncodeObject)}]'); stdout.writeln('[${JSON.encode(command, toEncodable: _jsonEncodeObject)}]');
}, daemonCommand: this); }, daemonCommand: this);
...@@ -139,11 +139,11 @@ abstract class Domain { ...@@ -139,11 +139,11 @@ abstract class Domain {
String toString() => name; String toString() => name;
void handleCommand(String name, dynamic id, dynamic args) { void handleCommand(String command, dynamic id, dynamic args) {
new Future.sync(() { new Future.sync(() {
if (_handlers.containsKey(name)) if (_handlers.containsKey(command))
return _handlers[name](args); return _handlers[command](args);
throw 'command not understood: $name'; throw 'command not understood: $name.$command';
}).then((result) { }).then((result) {
if (result == null) { if (result == null) {
_send({'id': id}); _send({'id': id});
...@@ -152,12 +152,12 @@ abstract class Domain { ...@@ -152,12 +152,12 @@ abstract class Domain {
} }
}).catchError((error, trace) { }).catchError((error, trace) {
_send({'id': id, 'error': _toJsonable(error)}); _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]) { void sendEvent(String name, [dynamic args]) {
Map<String, dynamic> map = { 'method': name }; Map<String, dynamic> map = { 'event': name };
if (args != null) if (args != null)
map['params'] = _toJsonable(args); map['params'] = _toJsonable(args);
_send(map); _send(map);
...@@ -169,12 +169,33 @@ abstract class Domain { ...@@ -169,12 +169,33 @@ abstract class Domain {
} }
/// This domain responds to methods like [version] and [shutdown]. /// This domain responds to methods like [version] and [shutdown].
///
/// This domain fires the `daemon.logMessage` event.
class DaemonDomain extends Domain { class DaemonDomain extends Domain {
DaemonDomain(Daemon daemon) : super(daemon, 'daemon') { DaemonDomain(Daemon daemon) : super(daemon, 'daemon') {
registerHandler('version', version); registerHandler('version', version);
registerHandler('shutdown', shutdown); 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<LogRecord> _subscription;
Future<String> version(dynamic args) { Future<String> version(dynamic args) {
return new Future.value(protocolVersion); return new Future.value(protocolVersion);
} }
...@@ -183,6 +204,10 @@ class DaemonDomain extends Domain { ...@@ -183,6 +204,10 @@ class DaemonDomain extends Domain {
Timer.run(() => daemon.shutdown()); Timer.run(() => daemon.shutdown());
return new Future.value(); return new Future.value();
} }
void dispose() {
_subscription?.cancel();
}
} }
/// This domain responds to methods like [start] and [stopAll]. /// This domain responds to methods like [start] and [stopAll].
...@@ -195,19 +220,46 @@ class AppDomain extends Domain { ...@@ -195,19 +220,46 @@ class AppDomain extends Domain {
registerHandler('stopAll', stopAll); registerHandler('stopAll', stopAll);
} }
Future<dynamic> start(dynamic args) async { Future<dynamic> start(Map<String, dynamic> args) async {
// TODO: Add the ability to pass args: target, http, checked // TODO(devoncarew): We need to be able to specify the target device.
await Future.wait([ if (args['projectDirectory'] is! String)
command.downloadToolchain(), throw "A 'projectDirectory' is required";
command.downloadApplicationPackagesAndConnectToDevices(),
], eagerError: true); 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 startApp( return null;
command.devices,
command.applicationPackages,
command.toolchain
).then((int result) => null);
} }
Future<bool> stopAll(dynamic args) { Future<bool> stopAll(dynamic args) {
...@@ -328,6 +380,7 @@ class AndroidDeviceDiscovery { ...@@ -328,6 +380,7 @@ class AndroidDeviceDiscovery {
Map<String, dynamic> _deviceToMap(Device device) { Map<String, dynamic> _deviceToMap(Device device) {
return <String, dynamic>{ return <String, dynamic>{
'id': device.id, 'id': device.id,
'name': device.name,
'platform': _enumToString(device.platform), 'platform': _enumToString(device.platform),
'available': device.isConnected() 'available': device.isConnected()
}; };
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter_tools/src/base/logging.dart';
import 'package:flutter_tools/src/commands/daemon.dart'; import 'package:flutter_tools/src/commands/daemon.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -29,12 +30,30 @@ defineTests() { ...@@ -29,12 +30,30 @@ defineTests() {
(Map<String, dynamic> result) => responses.add(result) (Map<String, dynamic> result) => responses.add(result)
); );
commands.add({'id': 0, 'method': 'daemon.version'}); 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['id'], 0);
expect(response['result'], isNotEmpty); expect(response['result'], isNotEmpty);
expect(response['result'] is String, true); expect(response['result'] is String, true);
}); });
test('daemon.logMessage', () async {
StreamController<Map> commands = new StreamController();
StreamController<Map> responses = new StreamController();
daemon = new Daemon(
commands.stream,
(Map<String, dynamic> result) => responses.add(result)
);
logging.warning('daemon.logMessage test');
Map response = await responses.stream.where((Map<String, dynamic> map) {
return map['event'] == 'daemon.logMessage' && map['params']['level'] == 'warning';
}).first;
expect(response['id'], isNull);
expect(response['event'], 'daemon.logMessage');
Map<String, String> logMessage = response['params'];
expect(logMessage['level'], 'warning');
expect(logMessage['message'], 'daemon.logMessage test');
});
test('daemon.shutdown', () async { test('daemon.shutdown', () async {
StreamController<Map> commands = new StreamController(); StreamController<Map> commands = new StreamController();
StreamController<Map> responses = new StreamController(); StreamController<Map> responses = new StreamController();
...@@ -72,7 +91,7 @@ defineTests() { ...@@ -72,7 +91,7 @@ defineTests() {
when(mockDevices.iOSSimulator.stopApp(any)).thenReturn(false); when(mockDevices.iOSSimulator.stopApp(any)).thenReturn(false);
commands.add({'id': 0, 'method': 'app.stopAll'}); 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['id'], 0);
expect(response['result'], true); expect(response['result'], true);
}); });
...@@ -85,9 +104,11 @@ defineTests() { ...@@ -85,9 +104,11 @@ defineTests() {
(Map<String, dynamic> result) => responses.add(result) (Map<String, dynamic> result) => responses.add(result)
); );
commands.add({'id': 0, 'method': 'device.getDevices'}); 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['id'], 0);
expect(response['result'], isList); expect(response['result'], isList);
}); });
}); });
} }
bool _notEvent(Map<String, dynamic> map) => map['event'] == null;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment