Commit 6a4b08be authored by Todd Volkert's avatar Todd Volkert Committed by GitHub

Make DeviceDomain echo a msg if doctor says it can't list anything (#9749)

This message will be picked up by IntelliJ and shown to the user in a toast.
parent b8b6a14d
......@@ -5,7 +5,6 @@
import 'dart:async';
import 'package:args/command_runner.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:intl/intl_standalone.dart' as intl;
import 'package:meta/meta.dart';
import 'package:process/process.dart';
......@@ -53,6 +52,7 @@ import 'src/run_hot.dart';
import 'src/runner/flutter_command.dart';
import 'src/runner/flutter_command_runner.dart';
import 'src/usage.dart';
import 'src/version.dart';
/// Main entry point for commands.
///
......
......@@ -17,6 +17,7 @@ import '../base/process_manager.dart';
import '../build_info.dart';
import '../commands/build_apk.dart';
import '../device.dart';
import '../doctor.dart';
import '../globals.dart';
import '../protocol_discovery.dart';
......@@ -27,11 +28,14 @@ import 'android_sdk.dart';
const String _defaultAdbPath = 'adb';
class AndroidDevices extends PollingDeviceDiscovery {
AndroidDevices() : super('AndroidDevices');
AndroidDevices() : super('Android devices');
@override
bool get supportsPlatform => true;
@override
bool get canListAnything => doctor.androidWorkflow.canListDevices;
@override
List<Device> pollingGetDevices() => getAdbDevices();
}
......
......@@ -62,12 +62,7 @@ class DaemonCommand extends FlutterCommand {
final int code = await daemon.onExit;
if (code != 0)
throwToolExit('Daemon exited with non-zero exit code: $code', exitCode: code);
}, onError: _handleError);
}
dynamic _handleError(dynamic error, StackTrace stackTrace) {
printError('Error from flutter daemon: $error', stackTrace: stackTrace);
return null;
});
}
}
......@@ -76,7 +71,9 @@ typedef void DispatchCommand(Map<String, dynamic> command);
typedef Future<dynamic> CommandHandler(Map<String, dynamic> args);
class Daemon {
Daemon(Stream<Map<String, dynamic>> commandStream, this.sendCommand, {
Daemon(
Stream<Map<String, dynamic>> commandStream,
this.sendCommand, {
this.daemonCommand,
this.notifyingLogger,
this.logToStdout: false
......@@ -87,7 +84,7 @@ class Daemon {
_registerDomain(deviceDomain = new DeviceDomain(this));
// Start listening.
commandStream.listen(
_commandSubscription = commandStream.listen(
_handleRequest,
onDone: () {
if (!_onExitCompleter.isCompleted)
......@@ -99,6 +96,7 @@ class Daemon {
DaemonDomain daemonDomain;
AppDomain appDomain;
DeviceDomain deviceDomain;
StreamSubscription<Map<String, dynamic>> _commandSubscription;
final DispatchCommand sendCommand;
final DaemonCommand daemonCommand;
......@@ -143,10 +141,15 @@ class Daemon {
void _send(Map<String, dynamic> map) => sendCommand(map);
void shutdown() {
void shutdown({dynamic error}) {
_commandSubscription?.cancel();
_domainMap.values.forEach((Domain domain) => domain.dispose());
if (!_onExitCompleter.isCompleted)
if (!_onExitCompleter.isCompleted) {
if (error == null)
_onExitCompleter.complete(0);
else
_onExitCompleter.completeError(error);
}
}
}
......@@ -539,6 +542,18 @@ class DeviceDomain extends Domain {
if (!discoverer.supportsPlatform)
return;
if (!discoverer.canListAnything) {
sendEvent(
'daemon.showMessage',
<String, String>{
'title': 'Unable to list devices',
'message':
'Unable to discover ${discoverer.name}. Please run '
'"flutter doctor" to diagnose potential issues',
},
);
}
_discoverers.add(discoverer);
discoverer.onAdded.listen(_onDeviceEvent('device.added'));
......@@ -650,7 +665,8 @@ Stream<Map<String, dynamic>> get stdinCommandStream => stdin
});
void stdoutCommandResponse(Map<String, dynamic> command) {
stdout.writeln('[${JSON.encode(command, toEncodable: _jsonEncodeObject)}]');
final String encoded = JSON.encode(command, toEncodable: _jsonEncodeObject);
stdout.writeln('[$encoded]');
}
dynamic _jsonEncodeObject(dynamic object) {
......
......@@ -92,6 +92,11 @@ class DeviceManager {
/// An abstract class to discover and enumerate a specific type of devices.
abstract class DeviceDiscovery {
bool get supportsPlatform;
/// Whether this device discovery is capable of listing any devices given the
/// current environment configuration.
bool get canListAnything;
List<Device> get devices;
}
......
......@@ -27,11 +27,14 @@ const String _kIdeviceinstallerInstructions =
const Duration kPortForwardTimeout = const Duration(seconds: 10);
class IOSDevices extends PollingDeviceDiscovery {
IOSDevices() : super('IOSDevices');
IOSDevices() : super('iOS devices');
@override
bool get supportsPlatform => platform.isMacOS;
@override
bool get canListAnything => doctor.iosWorkflow.canListDevices;
@override
List<Device> pollingGetDevices() => IOSDevice.getAttachedDevices();
}
......
......@@ -16,6 +16,7 @@ import '../base/process.dart';
import '../base/process_manager.dart';
import '../build_info.dart';
import '../device.dart';
import '../doctor.dart';
import '../flx.dart' as flx;
import '../globals.dart';
import '../protocol_discovery.dart';
......@@ -27,11 +28,14 @@ const String _xcrunPath = '/usr/bin/xcrun';
const String _kFlutterTestDeviceSuffix = '(Flutter)';
class IOSSimulators extends PollingDeviceDiscovery {
IOSSimulators() : super('IOSSimulators');
IOSSimulators() : super('iOS simulators');
@override
bool get supportsPlatform => platform.isMacOS;
@override
bool get canListAnything => doctor.iosWorkflow.canListDevices;
@override
List<Device> pollingGetDevices() => IOSSimulatorUtils.instance.getAttachedDevices();
}
......
......@@ -4,14 +4,13 @@
import 'dart:async';
import 'package:flutter_tools/src/android/android_workflow.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/commands/daemon.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/globals.dart';
import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/ios/ios_workflow.dart';
import 'package:test/test.dart';
import 'src/context.dart';
......@@ -22,22 +21,11 @@ void main() {
AppContext appContext;
NotifyingLogger notifyingLogger;
void _testUsingContext(String description, dynamic testMethod()) {
test(description, () {
return appContext.runInZone(testMethod);
});
}
group('daemon', () {
setUp(() {
appContext = new AppContext();
notifyingLogger = new NotifyingLogger();
appContext.setVariable(Platform, const LocalPlatform());
appContext.setVariable(Logger, notifyingLogger);
appContext.setVariable(Doctor, new Doctor());
if (platform.isMacOS)
appContext.setVariable(Xcode, new Xcode());
appContext.setVariable(DeviceManager, new MockDeviceManager());
});
tearDown(() {
......@@ -46,7 +34,7 @@ void main() {
notifyingLogger.dispose();
});
_testUsingContext('daemon.version', () async {
testUsingContext('daemon.version command should succeed', () async {
final StreamController<Map<String, dynamic>> commands = new StreamController<Map<String, dynamic>>();
final StreamController<Map<String, dynamic>> responses = new StreamController<Map<String, dynamic>>();
daemon = new Daemon(
......@@ -55,7 +43,7 @@ void main() {
notifyingLogger: notifyingLogger
);
commands.add(<String, dynamic>{'id': 0, 'method': 'daemon.version'});
final Map<String, dynamic> response = await responses.stream.where(_notEvent).first;
final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
expect(response['id'], 0);
expect(response['result'], isNotEmpty);
expect(response['result'] is String, true);
......@@ -63,7 +51,7 @@ void main() {
commands.close();
});
_testUsingContext('daemon.logMessage', () {
testUsingContext('printError should send daemon.logMessage event', () {
return appContext.runInZone(() async {
final StreamController<Map<String, dynamic>> commands = new StreamController<Map<String, dynamic>>();
final StreamController<Map<String, dynamic>> responses = new StreamController<Map<String, dynamic>>();
......@@ -73,9 +61,9 @@ void main() {
notifyingLogger: notifyingLogger
);
printError('daemon.logMessage test');
final Map<String, dynamic> response = await responses.stream.where((Map<String, dynamic> map) {
final Map<String, dynamic> response = await responses.stream.firstWhere((Map<String, dynamic> map) {
return map['event'] == 'daemon.logMessage' && map['params']['level'] == 'error';
}).first;
});
expect(response['id'], isNull);
expect(response['event'], 'daemon.logMessage');
final Map<String, String> logMessage = response['params'];
......@@ -86,7 +74,7 @@ void main() {
});
});
_testUsingContext('daemon.logMessage logToStdout', () async {
testUsingContext('printStatus should log to stdout when logToStdout is enabled', () async {
final StringBuffer buffer = new StringBuffer();
await runZoned(() async {
......@@ -110,7 +98,7 @@ void main() {
expect(buffer.toString().trim(), 'daemon.logMessage test');
});
_testUsingContext('daemon.shutdown', () async {
testUsingContext('daemon.shutdown command should stop daemon', () async {
final StreamController<Map<String, dynamic>> commands = new StreamController<Map<String, dynamic>>();
final StreamController<Map<String, dynamic>> responses = new StreamController<Map<String, dynamic>>();
daemon = new Daemon(
......@@ -126,7 +114,7 @@ void main() {
});
});
_testUsingContext('daemon.start', () async {
testUsingContext('app.start without a deviceId should report an error', () async {
final DaemonCommand command = new DaemonCommand();
applyMocksToCommand(command);
......@@ -140,14 +128,14 @@ void main() {
);
commands.add(<String, dynamic>{ 'id': 0, 'method': 'app.start' });
final Map<String, dynamic> response = await responses.stream.where(_notEvent).first;
final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
expect(response['id'], 0);
expect(response['error'], contains('deviceId is required'));
responses.close();
commands.close();
});
_testUsingContext('daemon.restart', () async {
testUsingContext('app.restart without an appId should report an error', () async {
final DaemonCommand command = new DaemonCommand();
applyMocksToCommand(command);
......@@ -161,14 +149,14 @@ void main() {
);
commands.add(<String, dynamic>{ 'id': 0, 'method': 'app.restart' });
final Map<String, dynamic> response = await responses.stream.where(_notEvent).first;
final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
expect(response['id'], 0);
expect(response['error'], contains('appId is required'));
responses.close();
commands.close();
});
_testUsingContext('daemon.callServiceExtension', () async {
testUsingContext('ext.flutter.debugPaint via service extension without an appId should report an error', () async {
final DaemonCommand command = new DaemonCommand();
applyMocksToCommand(command);
......@@ -188,14 +176,14 @@ void main() {
'methodName': 'ext.flutter.debugPaint'
}
});
final Map<String, dynamic> response = await responses.stream.where(_notEvent).first;
final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
expect(response['id'], 0);
expect(response['error'], contains('appId is required'));
responses.close();
commands.close();
});
_testUsingContext('daemon.stop', () async {
testUsingContext('app.stop without appId should report an error', () async {
final DaemonCommand command = new DaemonCommand();
applyMocksToCommand(command);
......@@ -209,14 +197,32 @@ void main() {
);
commands.add(<String, dynamic>{ 'id': 0, 'method': 'app.stop' });
final Map<String, dynamic> response = await responses.stream.where(_notEvent).first;
final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
expect(response['id'], 0);
expect(response['error'], contains('appId is required'));
responses.close();
commands.close();
});
_testUsingContext('device.getDevices', () async {
testUsingContext('daemon should send showMessage on startup if no Android devices are available', () async {
final StreamController<Map<String, dynamic>> commands = new StreamController<Map<String, dynamic>>();
final StreamController<Map<String, dynamic>> responses = new StreamController<Map<String, dynamic>>();
daemon = new Daemon(
commands.stream,
responses.add,
notifyingLogger: notifyingLogger,
);
final Map<String, dynamic> response = await responses.stream.first;
expect(response['event'], 'daemon.showMessage');
expect(response['params'], isMap);
expect(response['params'], containsPair('title', 'Unable to list devices'));
expect(response['params'], containsPair('message', contains('Unable to discover Android devices')));
}, overrides: <Type, Generator>{
Doctor: () => new MockDoctor(androidCanListDevices: false),
});
testUsingContext('device.getDevices should respond with list', () async {
final StreamController<Map<String, dynamic>> commands = new StreamController<Map<String, dynamic>>();
final StreamController<Map<String, dynamic>> responses = new StreamController<Map<String, dynamic>>();
daemon = new Daemon(
......@@ -225,14 +231,14 @@ void main() {
notifyingLogger: notifyingLogger
);
commands.add(<String, dynamic>{'id': 0, 'method': 'device.getDevices'});
final Map<String, dynamic> response = await responses.stream.where(_notEvent).first;
final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
expect(response['id'], 0);
expect(response['result'], isList);
responses.close();
commands.close();
});
_testUsingContext('device.notify', () {
testUsingContext('should send device.added event when device is discovered', () {
final StreamController<Map<String, dynamic>> commands = new StreamController<Map<String, dynamic>>();
final StreamController<Map<String, dynamic>> responses = new StreamController<Map<String, dynamic>>();
daemon = new Daemon(
......@@ -255,8 +261,37 @@ void main() {
responses.close();
commands.close();
});
}, overrides: <Type, Generator>{
Doctor: () => new MockDoctor(),
});
});
}
bool _notEvent(Map<String, dynamic> map) => map['event'] == null;
class MockDoctor extends Doctor {
final bool androidCanListDevices;
final bool iosCanListDevices;
MockDoctor({this.androidCanListDevices: true, this.iosCanListDevices: true});
@override
AndroidWorkflow get androidWorkflow => new MockAndroidWorkflow(androidCanListDevices);
@override
IOSWorkflow get iosWorkflow => new MockIosWorkflow(iosCanListDevices);
}
class MockAndroidWorkflow extends AndroidWorkflow {
@override
final bool canListDevices;
MockAndroidWorkflow(this.canListDevices);
}
class MockIosWorkflow extends IOSWorkflow {
@override
final bool canListDevices;
MockIosWorkflow(this.canListDevices);
}
......@@ -42,6 +42,9 @@ class MockPollingDeviceDiscovery extends PollingDeviceDiscovery {
@override
bool get supportsPlatform => true;
@override
bool get canListAnything => true;
void addDevice(MockAndroidDevice device) {
_devices.add(device);
......
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