Unverified Commit f3be1d9d authored by Danny Tuppeny's avatar Danny Tuppeny Committed by GitHub

Add `emulatorID` field to devices in daemon (#34794)

* Add emulatorId to Android and iOS emulator devices

* Update docs

* Review tweaks

* Add tests for AndroidConsole for getting avd names

* Remove unused import

* Remove duplicated header

* Fix imports
parent 305a9950
......@@ -157,7 +157,7 @@ This is sent when an app is stopped or detached from. The `params` field will be
#### device.getDevices
Return a list of all connected devices. The `params` field will be a List; each item is a map with the fields `id`, `name`, `platform`, `category`, `platformType`, `ephemeral`, and `emulator` (a boolean).
Return a list of all connected devices. The `params` field will be a List; each item is a map with the fields `id`, `name`, `platform`, `category`, `platformType`, `ephemeral`, `emulator` (a boolean) and `emulatorId`.
`category` is string description of the kind of workflow the device supports. The current categories are "mobile", "web" and "desktop", or null if none.
......@@ -167,6 +167,8 @@ supports. The current catgetories are "android", "ios", "linux", "macos",
`ephemeral` is a boolean which indicates where the device needs to be manually connected to a development machine. For example, a physical Android device is ephemeral, but the "web" device (that is always present) is not.
`emulatorId` is an string ID that matches the ID from `getEmulators` to allow clients to match running devices to the emulators that started them (for example to hide emulators that are already running). This field is not guaranteed to be populated even if a device was spawned from an emulator as it may require a successful connection to the device to retrieve it. In the case of a failed connection or the device is not an emulator, this field will be null.
#### device.enable
Turn on device polling. This will poll for newly connected devices, and fire `device.added` and `device.removed` events.
......@@ -258,6 +260,7 @@ See the [source](https://github.com/flutter/flutter/blob/master/packages/flutter
## Changelog
- 0.5.3: Added `emulatorId` field to device.
- 0.5.2: Added `platformType` and `category` fields to emulator.
- 0.5.1: Added `platformType`, `ephemeral`, and `category` fields to device.
- 0.5.0: Added `daemon.getSupportedPlatforms` command
......
// Copyright 2019 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:async/async.dart';
import '../base/context.dart';
import '../base/io.dart';
import '../convert.dart';
/// Default factory that creates a real Android console connection.
final AndroidConsoleSocketFactory _kAndroidConsoleSocketFactory = (String host, int port) => Socket.connect( host, port);
/// Currently active implementation of the AndroidConsoleFactory.
///
/// The default implementation will create real connections to a device.
/// Override this in tests with an implementation that returns mock responses.
AndroidConsoleSocketFactory get androidConsoleSocketFactory => context.get<AndroidConsoleSocketFactory>() ?? _kAndroidConsoleSocketFactory;
typedef AndroidConsoleSocketFactory = Future<Socket> Function(String host, int port);
/// Creates a console connection to an Android emulator that can be used to run
/// commands such as "avd name" which are not available to ADB.
///
/// See documentation at
/// https://developer.android.com/studio/run/emulator-console
class AndroidConsole {
AndroidConsole(this._socket);
Socket _socket;
StreamQueue<String> _queue;
Future<void> connect() async {
assert(_socket != null);
assert(_queue == null);
_queue = StreamQueue<String>(_socket.asyncMap(ascii.decode));
// Discard any initial connection text.
await _readResponse();
}
Future<String> getAvdName() async {
_write('avd name\n');
return _readResponse();
}
void destroy() {
if (_socket != null) {
_socket.destroy();
_socket = null;
_queue = null;
}
}
Future<String> _readResponse() async {
final StringBuffer output = StringBuffer();
while (true) {
final String text = await _queue.next;
final String trimmedText = text.trim();
if (trimmedText == 'OK')
break;
if (trimmedText.endsWith('\nOK')) {
output.write(trimmedText.substring(0, trimmedText.length - 3));
break;
}
output.write(text);
}
return output.toString().trim();
}
void _write(String text) {
_socket.add(ascii.encode(text));
}
}
......@@ -26,6 +26,7 @@ import '../protocol_discovery.dart';
import 'adb.dart';
import 'android.dart';
import 'android_console.dart';
import 'android_sdk.dart';
enum _HardwareType { emulator, physical }
......@@ -134,6 +135,55 @@ class AndroidDevice extends Device {
return _isLocalEmulator;
}
/// The unique identifier for the emulator that corresponds to this device, or
/// null if it is not an emulator.
///
/// The ID returned matches that in the output of `flutter emulators`. Fetching
/// this name may require connecting to the device and if an error occurs null
/// will be returned.
@override
Future<String> get emulatorId async {
if (!(await isLocalEmulator))
return null;
// Emulators always have IDs in the format emulator-(port) where port is the
// Android Console port number.
final RegExp emulatorPortRegex = RegExp(r'emulator-(\d+)');
final Match portMatch = emulatorPortRegex.firstMatch(id);
if (portMatch == null || portMatch.groupCount < 1) {
return null;
}
const String host = 'localhost';
final int port = int.parse(portMatch.group(1));
printTrace('Fetching avd name for $name via Android console on $host:$port');
try {
final Socket socket = await androidConsoleSocketFactory(host, port);
final AndroidConsole console = AndroidConsole(socket);
try {
await console
.connect()
.timeout(timeoutConfiguration.fastOperation,
onTimeout: () => throw TimeoutException('Connection timed out'));
return await console
.getAvdName()
.timeout(timeoutConfiguration.fastOperation,
onTimeout: () => throw TimeoutException('"avd name" timed out'));
} finally {
console.destroy();
}
} catch (e) {
printTrace('Failed to fetch avd name for emulator at $host:$port: $e');
// If we fail to connect to the device, we should not fail so just return
// an empty name. This data is best-effort.
return null;
}
}
@override
Future<TargetPlatform> get targetPlatform async {
if (_platform == null) {
......
......@@ -26,7 +26,7 @@ import '../run_hot.dart';
import '../runner/flutter_command.dart';
import '../vmservice.dart';
const String protocolVersion = '0.5.2';
const String protocolVersion = '0.5.3';
/// 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
......@@ -777,6 +777,7 @@ Future<Map<String, dynamic>> _deviceToMap(Device device) async {
'category': device.category?.toString(),
'platformType': device.platformType?.toString(),
'ephemeral': device.ephemeral,
'emulatorId': await device.emulatorId,
};
}
......
......@@ -262,6 +262,14 @@ abstract class Device {
/// Whether it is an emulated device running on localhost.
Future<bool> get isLocalEmulator;
/// The unique identifier for the emulator that corresponds to this device, or
/// null if it is not an emulator.
///
/// The ID returned matches that in the output of `flutter emulators`. Fetching
/// this name may require connecting to the device and if an error occurs null
/// will be returned.
Future<String> get emulatorId;
/// Whether the device is a simulator on a platform which supports hardware rendering.
Future<bool> get supportsHardwareRendering async {
assert(await isLocalEmulator);
......
......@@ -197,6 +197,9 @@ class FuchsiaDevice extends Device {
@override
Future<bool> get isLocalEmulator async => false;
@override
Future<String> get emulatorId async => null;
@override
bool get supportsStartPaused => false;
......
......@@ -145,6 +145,9 @@ class IOSDevice extends Device {
@override
Future<bool> get isLocalEmulator async => false;
@override
Future<String> get emulatorId async => null;
@override
bool get supportsStartPaused => false;
......
......@@ -11,6 +11,7 @@ import '../emulator.dart';
import '../globals.dart';
import '../macos/xcode.dart';
import 'ios_workflow.dart';
import 'simulators.dart';
class IOSEmulators extends EmulatorDiscovery {
@override
......@@ -71,5 +72,5 @@ List<IOSEmulator> getEmulators() {
return <IOSEmulator>[];
}
return <IOSEmulator>[IOSEmulator('apple_ios_simulator')];
return <IOSEmulator>[IOSEmulator(iosSimulatorId)];
}
......@@ -27,6 +27,7 @@ import 'mac.dart';
import 'plist_utils.dart';
const String _xcrunPath = '/usr/bin/xcrun';
const String iosSimulatorId = 'apple_ios_simulator';
class IOSSimulators extends PollingDeviceDiscovery {
IOSSimulators() : super('iOS simulators');
......@@ -230,6 +231,9 @@ class IOSSimulator extends Device {
@override
Future<bool> get isLocalEmulator async => true;
@override
Future<String> get emulatorId async => iosSimulatorId;
@override
bool get supportsHotReload => true;
......
......@@ -53,6 +53,9 @@ class LinuxDevice extends Device {
@override
Future<bool> get isLocalEmulator async => false;
@override
Future<String> get emulatorId async => null;
@override
bool isSupported() => true;
......
......@@ -54,6 +54,9 @@ class MacOSDevice extends Device {
@override
Future<bool> get isLocalEmulator async => false;
@override
Future<String> get emulatorId async => null;
@override
bool isSupported() => true;
......
......@@ -55,6 +55,9 @@ class FlutterTesterDevice extends Device {
@override
Future<bool> get isLocalEmulator async => false;
@override
Future<String> get emulatorId async => null;
@override
String get name => 'Flutter test device';
......
......@@ -70,6 +70,9 @@ class ChromeDevice extends Device {
@override
Future<bool> get isLocalEmulator async => false;
@override
Future<String> get emulatorId async => null;
@override
bool isSupported() => flutterWebEnabled && canFindChrome();
......
......@@ -55,6 +55,9 @@ class WindowsDevice extends Device {
@override
Future<bool> get isLocalEmulator async => false;
@override
Future<String> get emulatorId async => null;
@override
bool isSupported() => true;
......
......@@ -3,8 +3,10 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_console.dart';
import 'package:flutter_tools/src/android/android_device.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/base/config.dart';
......@@ -278,6 +280,83 @@ flutter:
FileSystem: () => MemoryFileSystem(),
});
group('emulatorId', () {
final ProcessManager mockProcessManager = MockProcessManager();
const String dummyEmulatorId = 'dummyEmulatorId';
final Future<Socket> Function(String host, int port) unresponsiveSocket =
(String host, int port) async => MockUnresponsiveAndroidConsoleSocket();
final Future<Socket> Function(String host, int port) workingSocket =
(String host, int port) async => MockWorkingAndroidConsoleSocket(dummyEmulatorId);
String hardware;
bool socketWasCreated;
setUp(() {
hardware = 'goldfish'; // Known emulator
socketWasCreated = false;
when(mockProcessManager.run(argThat(contains('getprop')),
stderrEncoding: anyNamed('stderrEncoding'),
stdoutEncoding: anyNamed('stdoutEncoding'))).thenAnswer((_) {
final StringBuffer buf = StringBuffer()
..writeln('[ro.hardware]: [$hardware]');
final ProcessResult result = ProcessResult(1, 0, buf.toString(), '');
return Future<ProcessResult>.value(result);
});
});
testUsingContext('returns correct ID for responsive emulator', () async {
final AndroidDevice device = AndroidDevice('emulator-5555');
expect(await device.emulatorId, equals(dummyEmulatorId));
}, overrides: <Type, Generator>{
AndroidConsoleSocketFactory: () => workingSocket,
ProcessManager: () => mockProcessManager,
});
testUsingContext('does not create socket for non-emulator devices', () async {
hardware = 'samsungexynos7420';
// Still use an emulator-looking ID so we can be sure the failure is due
// to the isLocalEmulator field and not because the ID doesn't contain a
// port.
final AndroidDevice device = AndroidDevice('emulator-5555');
expect(await device.emulatorId, isNull);
expect(socketWasCreated, isFalse);
}, overrides: <Type, Generator>{
AndroidConsoleSocketFactory: () => (String host, int port) async {
socketWasCreated = true;
throw 'Socket was created for non-emulator';
},
ProcessManager: () => mockProcessManager,
});
testUsingContext('does not create socket for emulators with no port', () async {
final AndroidDevice device = AndroidDevice('emulator-noport');
expect(await device.emulatorId, isNull);
expect(socketWasCreated, isFalse);
}, overrides: <Type, Generator>{
AndroidConsoleSocketFactory: () => (String host, int port) async {
socketWasCreated = true;
throw 'Socket was created for emulator without port in ID';
},
ProcessManager: () => mockProcessManager,
});
testUsingContext('returns null for connection error', () async {
final AndroidDevice device = AndroidDevice('emulator-5555');
expect(await device.emulatorId, isNull);
}, overrides: <Type, Generator>{
AndroidConsoleSocketFactory: () => (String host, int port) => throw 'Fake socket error',
ProcessManager: () => mockProcessManager,
});
testUsingContext('returns null for unresponsive device', () async {
final AndroidDevice device = AndroidDevice('emulator-5555');
expect(await device.emulatorId, isNull);
}, overrides: <Type, Generator>{
AndroidConsoleSocketFactory: () => unresponsiveSocket,
ProcessManager: () => mockProcessManager,
});
});
group('portForwarder', () {
final ProcessManager mockProcessManager = MockProcessManager();
final AndroidDevice device = AndroidDevice('1234');
......@@ -480,3 +559,44 @@ const String kAdbShellGetprop = '''
[wlan.driver.status]: [unloaded]
[xmpp.auto-presence]: [true]
''';
/// A mock Android Console that presents a connection banner and responds to
/// "avd name" requests with the supplied name.
class MockWorkingAndroidConsoleSocket extends Mock implements Socket {
MockWorkingAndroidConsoleSocket(this.avdName) {
_controller.add('Android Console: Welcome!\n');
// Include OK in the same packet here. In the response to "avd name"
// it's sent alone to ensure both are handled.
_controller.add('Android Console: Some intro text\nOK\n');
}
final String avdName;
final StreamController<String> _controller = StreamController<String>();
@override
Stream<E> asyncMap<E>(FutureOr<E> convert(List<int> event)) => _controller.stream as Stream<E>;
@override
void add(List<int> data) {
final String text = ascii.decode(data);
if (text == 'avd name\n') {
_controller.add('$avdName\n');
// Include OK in its own packet here. In welcome banner it's included
// as part of the previous text to ensure both are handled.
_controller.add('OK\n');
} else {
throw 'Unexpected command $text';
}
}
}
/// An Android console socket that drops all input and returns no output.
class MockUnresponsiveAndroidConsoleSocket extends Mock implements Socket {
final StreamController<String> _controller = StreamController<String>();
@override
Stream<E> asyncMap<E>(FutureOr<E> convert(List<int> event)) => _controller.stream as Stream<E>;
@override
void add(List<int> data) {}
}
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