flutter/dev/devicelab/lib/framework/devices.dart
Reid Baker 1eb7cd2c73
allow adb to set canfail then use canFail=true for clearing logs (#150517)
Fixes https://github.com/flutter/flutter/issues/150093

New tests added to cover that we at least pass the arguments we expect to adb. 

The test for #150093  is not ideal in that it does not verify the behavior of a failed process but instead ensures we set the parameter that contains the behavior we want. 

devicelab code and tests are not setup to enable fake process or fake output from stdin/stderr and hang if adb or no hardware are present.
2024-06-24 19:13:24 +00:00

1477 lines
39 KiB
Dart

// Copyright 2014 The Flutter 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 'dart:math' as math;
import 'package:path/path.dart' as path;
import 'package:retry/retry.dart';
import 'utils.dart';
const String DeviceIdEnvName = 'FLUTTER_DEVICELAB_DEVICEID';
class DeviceException implements Exception {
const DeviceException(this.message);
final String message;
@override
String toString() => '$DeviceException: $message';
}
/// Gets the artifact path relative to the current directory.
String getArtifactPath() {
return path.normalize(
path.join(
path.current,
'../../bin/cache/artifacts',
)
);
}
/// Return the item is in idList if find a match, otherwise return null
String? _findMatchId(List<String> idList, String idPattern) {
String? candidate;
idPattern = idPattern.toLowerCase();
for (final String id in idList) {
if (id.toLowerCase() == idPattern) {
return id;
}
if (id.toLowerCase().startsWith(idPattern)) {
candidate ??= id;
}
}
return candidate;
}
/// The root of the API for controlling devices.
DeviceDiscovery get devices => DeviceDiscovery();
/// Device operating system the test is configured to test.
enum DeviceOperatingSystem {
android,
androidArm,
androidArm64,
fake,
fuchsia,
ios,
linux,
macos,
windows,
}
/// Device OS to test on.
DeviceOperatingSystem deviceOperatingSystem = DeviceOperatingSystem.android;
/// Discovers available devices and chooses one to work with.
abstract class DeviceDiscovery {
factory DeviceDiscovery() {
switch (deviceOperatingSystem) {
case DeviceOperatingSystem.android:
return AndroidDeviceDiscovery();
case DeviceOperatingSystem.androidArm:
return AndroidDeviceDiscovery(cpu: AndroidCPU.arm);
case DeviceOperatingSystem.androidArm64:
return AndroidDeviceDiscovery(cpu: AndroidCPU.arm64);
case DeviceOperatingSystem.ios:
return IosDeviceDiscovery();
case DeviceOperatingSystem.fuchsia:
return FuchsiaDeviceDiscovery();
case DeviceOperatingSystem.linux:
return LinuxDeviceDiscovery();
case DeviceOperatingSystem.macos:
return MacosDeviceDiscovery();
case DeviceOperatingSystem.windows:
return WindowsDeviceDiscovery();
case DeviceOperatingSystem.fake:
print('Looking for fake devices! You should not see this in release builds.');
return FakeDeviceDiscovery();
}
}
/// Selects a device to work with, load-balancing between devices if more than
/// one are available.
///
/// Calling this method does not guarantee that the same device will be
/// returned. For such behavior see [workingDevice].
Future<void> chooseWorkingDevice();
/// Selects a device to work with by device ID.
Future<void> chooseWorkingDeviceById(String deviceId);
/// A device to work with.
///
/// Returns the same device when called repeatedly (unlike
/// [chooseWorkingDevice]). This is useful when you need to perform multiple
/// operations on one.
Future<Device> get workingDevice;
/// Lists all available devices' IDs.
Future<List<String>> discoverDevices();
/// Checks the health of the available devices.
Future<Map<String, HealthCheckResult>> checkDevices();
/// Prepares the system to run tasks.
Future<void> performPreflightTasks();
}
/// A proxy for one specific device.
abstract class Device {
// Const constructor so subclasses may be const.
const Device();
/// A unique device identifier.
String get deviceId;
/// Whether the device is awake.
Future<bool> isAwake();
/// Whether the device is asleep.
Future<bool> isAsleep();
/// Wake up the device if it is not awake.
Future<void> wakeUp();
/// Send the device to sleep mode.
Future<void> sendToSleep();
/// Emulates pressing the home button.
Future<void> home();
/// Emulates pressing the power button, toggling the device's on/off state.
Future<void> togglePower();
/// Unlocks the device.
///
/// Assumes the device doesn't have a secure unlock pattern.
Future<void> unlock();
/// Attempt to reboot the phone, if possible.
Future<void> reboot();
/// Emulate a tap on the touch screen.
Future<void> tap(int x, int y);
/// Read memory statistics for a process.
Future<Map<String, dynamic>> getMemoryStats(String packageName);
/// Stream the system log from the device.
///
/// Flutter applications' `print` statements end up in this log
/// with some prefix.
Stream<String> get logcat;
/// Clears the device logs.
///
/// This is important because benchmarks tests rely on the logs produced by
/// the flutter run command.
///
/// On Android, those logs may contain logs from previous test.
Future<void> clearLogs();
/// Whether this device supports calls to [startLoggingToSink]
/// and [stopLoggingToSink].
bool get canStreamLogs => false;
/// Starts logging to an [IOSink].
///
/// If `clear` is set to true, the log will be cleared before starting. This
/// is not supported on all platforms.
Future<void> startLoggingToSink(IOSink sink, {bool clear = true}) {
throw UnimplementedError();
}
/// Stops logging that was started by [startLoggingToSink].
Future<void> stopLoggingToSink() {
throw UnimplementedError();
}
/// Stop a process.
Future<void> stop(String packageName);
/// Wait for the device to become ready.
Future<void> awaitDevice();
Future<void> uninstallApp() async {
await flutter('install', options: <String>[
'--uninstall-only',
'-d',
deviceId]);
await Future<void>.delayed(const Duration(seconds: 2));
await awaitDevice();
}
@override
String toString() {
return 'device: $deviceId';
}
}
enum AndroidCPU {
arm,
arm64,
}
class AndroidDeviceDiscovery implements DeviceDiscovery {
factory AndroidDeviceDiscovery({AndroidCPU? cpu}) {
return _instance ??= AndroidDeviceDiscovery._(cpu);
}
AndroidDeviceDiscovery._(this.cpu);
final AndroidCPU? cpu;
// Parses information about a device. Example:
//
// 015d172c98400a03 device usb:340787200X product:nakasi model:Nexus_7 device:grouper
static final RegExp _kDeviceRegex = RegExp(r'^(\S+)\s+(\S+)(.*)');
static AndroidDeviceDiscovery? _instance;
AndroidDevice? _workingDevice;
@override
Future<AndroidDevice> get workingDevice async {
if (_workingDevice == null) {
if (Platform.environment.containsKey(DeviceIdEnvName)) {
final String deviceId = Platform.environment[DeviceIdEnvName]!;
await chooseWorkingDeviceById(deviceId);
return _workingDevice!;
}
await chooseWorkingDevice();
}
return _workingDevice!;
}
Future<bool> _matchesCPURequirement(AndroidDevice device) async {
return switch (cpu) {
null => Future<bool>.value(true),
AndroidCPU.arm64 => device.isArm64(),
AndroidCPU.arm => device.isArm(),
};
}
/// Picks a random Android device out of connected devices and sets it as
/// [workingDevice].
@override
Future<void> chooseWorkingDevice() async {
final List<AndroidDevice> allDevices = (await discoverDevices())
.map<AndroidDevice>((String id) => AndroidDevice(deviceId: id))
.toList();
if (allDevices.isEmpty) {
throw const DeviceException('No Android devices detected');
}
if (cpu != null) {
for (final AndroidDevice device in allDevices) {
if (await _matchesCPURequirement(device)) {
_workingDevice = device;
break;
}
}
} else {
// TODO(yjbanov): filter out and warn about those with low battery level
_workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
}
if (_workingDevice == null) {
throw const DeviceException('Cannot find a suitable Android device');
}
print('Device chosen: $_workingDevice');
}
@override
Future<void> chooseWorkingDeviceById(String deviceId) async {
final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
if (matchedId != null) {
_workingDevice = AndroidDevice(deviceId: matchedId);
if (cpu != null) {
if (!await _matchesCPURequirement(_workingDevice!)) {
throw DeviceException('The selected device $matchedId does not match the cpu requirement');
}
}
print('Choose device by ID: $matchedId');
return;
}
throw DeviceException(
'Device with ID $deviceId is not found for operating system: '
'$deviceOperatingSystem'
);
}
@override
Future<List<String>> discoverDevices() async {
final List<String> output = (await eval(adbPath, <String>['devices', '-l']))
.trim().split('\n');
final List<String> results = <String>[];
for (final String line in output) {
// Skip lines like: * daemon started successfully *
if (line.startsWith('* daemon ')) {
continue;
}
if (line.startsWith('List of devices')) {
continue;
}
if (_kDeviceRegex.hasMatch(line)) {
final Match match = _kDeviceRegex.firstMatch(line)!;
final String deviceID = match[1]!;
final String deviceState = match[2]!;
if (!const <String>['unauthorized', 'offline'].contains(deviceState)) {
results.add(deviceID);
}
} else {
throw FormatException('Failed to parse device from adb output: "$line"');
}
}
return results;
}
@override
Future<Map<String, HealthCheckResult>> checkDevices() async {
final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
for (final String deviceId in await discoverDevices()) {
try {
final AndroidDevice device = AndroidDevice(deviceId: deviceId);
// Just a smoke test that we can read wakefulness state
// TODO(yjbanov): check battery level
await device._getWakefulness();
results['android-device-$deviceId'] = HealthCheckResult.success();
} on Exception catch (e, s) {
results['android-device-$deviceId'] = HealthCheckResult.error(e, s);
}
}
return results;
}
@override
Future<void> performPreflightTasks() async {
// Kills the `adb` server causing it to start a new instance upon next
// command.
//
// Restarting `adb` helps with keeping device connections alive. When `adb`
// runs non-stop for too long it loses connections to devices. There may be
// a better method, but so far that's the best one I've found.
await exec(adbPath, <String>['kill-server']);
}
}
class LinuxDeviceDiscovery implements DeviceDiscovery {
factory LinuxDeviceDiscovery() {
return _instance ??= LinuxDeviceDiscovery._();
}
LinuxDeviceDiscovery._();
static LinuxDeviceDiscovery? _instance;
static const LinuxDevice _device = LinuxDevice();
@override
Future<Map<String, HealthCheckResult>> checkDevices() async {
return <String, HealthCheckResult>{};
}
@override
Future<void> chooseWorkingDevice() async { }
@override
Future<void> chooseWorkingDeviceById(String deviceId) async { }
@override
Future<List<String>> discoverDevices() async {
return <String>['linux'];
}
@override
Future<void> performPreflightTasks() async { }
@override
Future<Device> get workingDevice async => _device;
}
class MacosDeviceDiscovery implements DeviceDiscovery {
factory MacosDeviceDiscovery() {
return _instance ??= MacosDeviceDiscovery._();
}
MacosDeviceDiscovery._();
static MacosDeviceDiscovery? _instance;
static const MacosDevice _device = MacosDevice();
@override
Future<Map<String, HealthCheckResult>> checkDevices() async {
return <String, HealthCheckResult>{};
}
@override
Future<void> chooseWorkingDevice() async { }
@override
Future<void> chooseWorkingDeviceById(String deviceId) async { }
@override
Future<List<String>> discoverDevices() async {
return <String>['macos'];
}
@override
Future<void> performPreflightTasks() async { }
@override
Future<Device> get workingDevice async => _device;
}
class WindowsDeviceDiscovery implements DeviceDiscovery {
factory WindowsDeviceDiscovery() {
return _instance ??= WindowsDeviceDiscovery._();
}
WindowsDeviceDiscovery._();
static WindowsDeviceDiscovery? _instance;
static const WindowsDevice _device = WindowsDevice();
@override
Future<Map<String, HealthCheckResult>> checkDevices() async {
return <String, HealthCheckResult>{};
}
@override
Future<void> chooseWorkingDevice() async { }
@override
Future<void> chooseWorkingDeviceById(String deviceId) async { }
@override
Future<List<String>> discoverDevices() async {
return <String>['windows'];
}
@override
Future<void> performPreflightTasks() async { }
@override
Future<Device> get workingDevice async => _device;
}
class FuchsiaDeviceDiscovery implements DeviceDiscovery {
factory FuchsiaDeviceDiscovery() {
return _instance ??= FuchsiaDeviceDiscovery._();
}
FuchsiaDeviceDiscovery._();
static FuchsiaDeviceDiscovery? _instance;
FuchsiaDevice? _workingDevice;
String get _ffx {
final String ffx = path.join(getArtifactPath(), 'fuchsia', 'tools','x64', 'ffx');
if (!File(ffx).existsSync()) {
throw FileSystemException("Couldn't find ffx at location $ffx");
}
return ffx;
}
@override
Future<FuchsiaDevice> get workingDevice async {
if (_workingDevice == null) {
if (Platform.environment.containsKey(DeviceIdEnvName)) {
final String deviceId = Platform.environment[DeviceIdEnvName]!;
await chooseWorkingDeviceById(deviceId);
return _workingDevice!;
}
await chooseWorkingDevice();
}
return _workingDevice!;
}
/// Picks the first connected Fuchsia device.
@override
Future<void> chooseWorkingDevice() async {
final List<FuchsiaDevice> allDevices = (await discoverDevices())
.map<FuchsiaDevice>((String id) => FuchsiaDevice(deviceId: id))
.toList();
if (allDevices.isEmpty) {
throw const DeviceException('No Fuchsia devices detected');
}
_workingDevice = allDevices.first;
print('Device chosen: $_workingDevice');
}
@override
Future<void> chooseWorkingDeviceById(String deviceId) async {
final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
if (matchedId != null) {
_workingDevice = FuchsiaDevice(deviceId: matchedId);
print('Choose device by ID: $matchedId');
return;
}
throw DeviceException(
'Device with ID $deviceId is not found for operating system: '
'$deviceOperatingSystem'
);
}
@override
Future<List<String>> discoverDevices() async {
final List<String> output = (await eval(_ffx, <String>['target', 'list', '-f', 's']))
.trim()
.split('\n');
final List<String> devices = <String>[];
for (final String line in output) {
final List<String> parts = line.split(' ');
assert(parts.length == 2);
devices.add(parts.last); // The device id.
}
return devices;
}
@override
Future<Map<String, HealthCheckResult>> checkDevices() async {
final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
for (final String deviceId in await discoverDevices()) {
try {
final int resolveResult = await exec(
_ffx,
<String>[
'target',
'list',
'-f',
'a',
deviceId,
]
);
if (resolveResult == 0) {
results['fuchsia-device-$deviceId'] = HealthCheckResult.success();
} else {
results['fuchsia-device-$deviceId'] = HealthCheckResult.failure('Cannot resolve device $deviceId');
}
} on Exception catch (error, stacktrace) {
results['fuchsia-device-$deviceId'] = HealthCheckResult.error(error, stacktrace);
}
}
return results;
}
@override
Future<void> performPreflightTasks() async {}
}
class AndroidDevice extends Device {
AndroidDevice({required this.deviceId}) {
_updateDeviceInfo();
}
@override
final String deviceId;
String deviceInfo = '';
int apiLevel = 0;
/// Whether the device is awake.
@override
Future<bool> isAwake() async {
return await _getWakefulness() == 'Awake';
}
/// Whether the device is asleep.
@override
Future<bool> isAsleep() async {
return await _getWakefulness() == 'Asleep';
}
/// Wake up the device if it is not awake using [togglePower].
@override
Future<void> wakeUp() async {
if (!(await isAwake())) {
await togglePower();
}
}
/// Send the device to sleep mode if it is not asleep using [togglePower].
@override
Future<void> sendToSleep() async {
if (!(await isAsleep())) {
await togglePower();
}
}
/// Sends `KEYCODE_HOME` (3), which causes the device to go to the home screen.
@override
Future<void> home() async {
await shellExec('input', const <String>['keyevent', '3']);
}
/// Sends `KEYCODE_POWER` (26), which causes the device to toggle its mode
/// between awake and asleep.
@override
Future<void> togglePower() async {
await shellExec('input', const <String>['keyevent', '26']);
}
/// Unlocks the device by sending `KEYCODE_MENU` (82).
///
/// This only works when the device doesn't have a secure unlock pattern.
@override
Future<void> unlock() async {
await wakeUp();
await shellExec('input', const <String>['keyevent', '82']);
}
@override
Future<void> tap(int x, int y) async {
await shellExec('input', <String>['tap', '$x', '$y']);
}
/// Retrieves device's wakefulness state.
///
/// See: https://android.googlesource.com/platform/frameworks/base/+/main/core/java/android/os/PowerManagerInternal.java
Future<String> _getWakefulness() async {
final String powerInfo = await shellEval('dumpsys', <String>['power']);
// A motoG4 phone returns `mWakefulness=Awake`.
// A Samsung phone returns `getWakefullnessLocked()=Awake`.
final RegExp wakefulnessRegexp = RegExp(r'.*(mWakefulness=|getWakefulnessLocked\(\)=).*');
final String wakefulness = grep(wakefulnessRegexp, from: powerInfo).single.split('=')[1].trim();
return wakefulness;
}
Future<bool> isArm64() async {
final String cpuInfo = await shellEval('getprop', const <String>['ro.product.cpu.abi']);
return cpuInfo.contains('arm64');
}
Future<bool> isArm() async {
final String cpuInfo = await shellEval('getprop', const <String>['ro.product.cpu.abi']);
return cpuInfo.contains('armeabi');
}
Future<void> _updateDeviceInfo() async {
String info;
try {
info = await shellEval(
'getprop',
<String>[
'ro.bootimage.build.fingerprint', ';',
'getprop', 'ro.build.version.release', ';',
'getprop', 'ro.build.version.sdk',
],
silent: true,
);
} on IOException {
info = '';
}
final List<String> list = info.split('\n');
if (list.length == 3) {
apiLevel = int.parse(list[2]);
deviceInfo = 'fingerprint: ${list[0]} os: ${list[1]} api-level: $apiLevel';
} else {
apiLevel = 0;
deviceInfo = '';
}
}
/// Executes [command] on `adb shell`.
Future<void> shellExec(String command, List<String> arguments, { Map<String, String>? environment, bool silent = false }) async {
await adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
}
/// Executes [command] on `adb shell` and returns its standard output as a [String].
Future<String> shellEval(String command, List<String> arguments, { Map<String, String>? environment, bool silent = false }) {
return adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
}
/// Runs `adb` with the given [arguments], selecting this device.
Future<String> adb(
List<String> arguments, {
Map<String, String>? environment,
bool silent = false,
bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
}) {
return eval(
adbPath,
<String>['-s', deviceId, ...arguments],
environment: environment,
printStdout: !silent,
printStderr: !silent,
canFail: canFail,
);
}
@override
Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
final String meminfo = await shellEval('dumpsys', <String>['meminfo', packageName]);
final Match? match = RegExp(r'TOTAL\s+(\d+)').firstMatch(meminfo);
assert(match != null, 'could not parse dumpsys meminfo output');
return <String, dynamic>{
'total_kb': int.parse(match!.group(1)!),
};
}
@override
bool get canStreamLogs => true;
bool _abortedLogging = false;
Process? _loggingProcess;
@override
Future<void> startLoggingToSink(IOSink sink, {bool clear = true}) async {
if (clear) {
await adb(<String>['logcat', '--clear'], silent: true, canFail: true);
}
_loggingProcess = await startProcess(
adbPath,
// Catch the whole log.
<String>['-s', deviceId, 'logcat'],
);
_loggingProcess!.stdout
.transform<String>(const Utf8Decoder(allowMalformed: true))
.listen((String line) {
sink.write(line);
});
_loggingProcess!.stderr
.transform<String>(const Utf8Decoder(allowMalformed: true))
.listen((String line) {
sink.write(line);
});
unawaited(_loggingProcess!.exitCode.then<void>((int exitCode) {
if (!_abortedLogging) {
sink.writeln('adb logcat failed with exit code $exitCode.\n');
}
}));
}
@override
Future<void> stopLoggingToSink() async {
if (_loggingProcess != null) {
_abortedLogging = true;
_loggingProcess!.kill();
await _loggingProcess!.exitCode;
}
}
@override
Future<void> clearLogs() {
return adb(<String>['logcat', '-c'], canFail: true);
}
@override
Stream<String> get logcat {
final Completer<void> stdoutDone = Completer<void>();
final Completer<void> stderrDone = Completer<void>();
final Completer<void> processDone = Completer<void>();
final Completer<void> abort = Completer<void>();
bool aborted = false;
late final StreamController<String> stream;
stream = StreamController<String>(
onListen: () async {
await clearLogs();
final Process process = await startProcess(
adbPath,
// Make logcat less chatty by filtering down to just ActivityManager
// (to let us know when app starts), flutter (needed by tests to see
// log output), and fatal messages (hopefully catches tombstones).
// For local testing, this can just be:
// <String>['-s', deviceId, 'logcat']
// to view the whole log, or just run logcat alongside this.
<String>['-s', deviceId, 'logcat', 'ActivityManager:I', 'flutter:V', '*:F'],
);
process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('adb logcat: $line');
if (!stream.isClosed) {
stream.sink.add(line);
}
}, onDone: () { stdoutDone.complete(); });
process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('adb logcat stderr: $line');
}, onDone: () { stderrDone.complete(); });
unawaited(process.exitCode.then<void>((int exitCode) {
print('adb logcat process terminated with exit code $exitCode');
if (!aborted) {
stream.addError(BuildFailedError('adb logcat failed with exit code $exitCode.\n'));
processDone.complete();
}
}));
await Future.any<dynamic>(<Future<dynamic>>[
Future.wait<void>(<Future<void>>[
stdoutDone.future,
stderrDone.future,
processDone.future,
]),
abort.future,
]);
aborted = true;
print('terminating adb logcat');
process.kill();
print('closing logcat stream');
await stream.close();
},
onCancel: () {
if (!aborted) {
print('adb logcat aborted');
aborted = true;
abort.complete();
}
},
);
return stream.stream;
}
@override
Future<void> stop(String packageName) async {
return shellExec('am', <String>['force-stop', packageName]);
}
@override
String toString() {
return '$deviceId $deviceInfo';
}
@override
Future<void> reboot() {
return adb(<String>['reboot']);
}
@override
Future<void> awaitDevice() async {
print('Waiting for device.');
final String waitOut = await adb(<String>['wait-for-device']);
print(waitOut);
const RetryOptions retryOptions = RetryOptions(delayFactor: Duration(seconds: 1), maxAttempts: 10, maxDelay: Duration(minutes: 1));
await retryOptions.retry(() async {
final String adbShellOut = await adb(<String>['shell', 'getprop sys.boot_completed']);
if (adbShellOut != '1') {
print('Device not ready.');
print(adbShellOut);
throw const DeviceException('Phone not ready.');
}
}, retryIf: (Exception e) => e is DeviceException);
print('Done waiting for device.');
}
}
class IosDeviceDiscovery implements DeviceDiscovery {
factory IosDeviceDiscovery() {
return _instance ??= IosDeviceDiscovery._();
}
IosDeviceDiscovery._();
static IosDeviceDiscovery? _instance;
IosDevice? _workingDevice;
@override
Future<IosDevice> get workingDevice async {
if (_workingDevice == null) {
if (Platform.environment.containsKey(DeviceIdEnvName)) {
final String deviceId = Platform.environment[DeviceIdEnvName]!;
await chooseWorkingDeviceById(deviceId);
return _workingDevice!;
}
await chooseWorkingDevice();
}
return _workingDevice!;
}
/// Picks a random iOS device out of connected devices and sets it as
/// [workingDevice].
@override
Future<void> chooseWorkingDevice() async {
final List<IosDevice> allDevices = (await discoverDevices())
.map<IosDevice>((String id) => IosDevice(deviceId: id))
.toList();
if (allDevices.isEmpty) {
throw const DeviceException('No iOS devices detected');
}
// TODO(yjbanov): filter out and warn about those with low battery level
_workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
print('Device chosen: $_workingDevice');
}
@override
Future<void> chooseWorkingDeviceById(String deviceId) async {
final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
if (matchedId != null) {
_workingDevice = IosDevice(deviceId: matchedId);
print('Choose device by ID: $matchedId');
return;
}
throw DeviceException(
'Device with ID $deviceId is not found for operating system: '
'$deviceOperatingSystem'
);
}
@override
Future<List<String>> discoverDevices() async {
final List<dynamic> results = json.decode(await eval(
path.join(flutterDirectory.path, 'bin', 'flutter'),
<String>['devices', '--machine', '--suppress-analytics', '--device-timeout', '5'],
)) as List<dynamic>;
// [
// {
// "name": "Flutter's iPhone",
// "id": "00008020-00017DA80CC1002E",
// "isSupported": true,
// "targetPlatform": "ios",
// "emulator": false,
// "sdk": "iOS 13.2",
// "capabilities": {
// "hotReload": true,
// "hotRestart": true,
// "screenshot": true,
// "fastStart": false,
// "flutterExit": true,
// "hardwareRendering": false,
// "startPaused": false
// }
// }
// ]
final List<String> deviceIds = <String>[];
for (final dynamic result in results) {
final Map<String, dynamic> device = result as Map<String, dynamic>;
if (device['targetPlatform'] == 'ios' &&
device['id'] != null &&
device['emulator'] != true &&
device['isSupported'] == true) {
deviceIds.add(device['id'] as String);
}
}
if (deviceIds.isEmpty) {
throw const DeviceException('No connected physical iOS devices found.');
}
return deviceIds;
}
@override
Future<Map<String, HealthCheckResult>> checkDevices() async {
final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
for (final String deviceId in await discoverDevices()) {
// TODO(ianh): do a more meaningful connectivity check than just recording the ID
results['ios-device-$deviceId'] = HealthCheckResult.success();
}
return results;
}
@override
Future<void> performPreflightTasks() async {
// Currently we do not have preflight tasks for iOS.
}
}
/// iOS device.
class IosDevice extends Device {
IosDevice({ required this.deviceId });
@override
final String deviceId;
String get idevicesyslogPath {
return path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libimobiledevice', 'idevicesyslog');
}
String get dyldLibraryPath {
final List<String> dylibsPaths = <String>[
path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libimobiledevice'),
path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'openssl'),
path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'usbmuxd'),
path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libplist'),
];
return dylibsPaths.join(':');
}
@override
bool get canStreamLogs => true;
bool _abortedLogging = false;
Process? _loggingProcess;
@override
Future<void> startLoggingToSink(IOSink sink, {bool clear = true}) async {
// Clear is not supported.
_loggingProcess = await startProcess(
idevicesyslogPath,
<String>['-u', deviceId, '--quiet'],
environment: <String, String>{
'DYLD_LIBRARY_PATH': dyldLibraryPath,
},
);
_loggingProcess!.stdout
.transform<String>(const Utf8Decoder(allowMalformed: true))
.listen((String line) {
sink.write(line);
});
_loggingProcess!.stderr
.transform<String>(const Utf8Decoder(allowMalformed: true))
.listen((String line) {
sink.write(line);
});
unawaited(_loggingProcess!.exitCode.then<void>((int exitCode) {
if (!_abortedLogging) {
sink.writeln('idevicesyslog failed with exit code $exitCode.\n');
}
}));
}
@override
Future<void> stopLoggingToSink() async {
if (_loggingProcess != null) {
_abortedLogging = true;
_loggingProcess!.kill();
await _loggingProcess!.exitCode;
}
}
// The methods below are stubs for now. They will need to be expanded.
// We currently do not have a way to lock/unlock iOS devices. So we assume the
// devices are already unlocked. For now we'll just keep them at minimum
// screen brightness so they don't drain battery too fast.
@override
Future<bool> isAwake() async => true;
@override
Future<bool> isAsleep() async => false;
@override
Future<void> wakeUp() async {}
@override
Future<void> sendToSleep() async {}
@override
Future<void> home() async {}
@override
Future<void> togglePower() async {}
@override
Future<void> unlock() async {}
@override
Future<void> tap(int x, int y) async {
throw UnimplementedError();
}
@override
Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
throw UnimplementedError();
}
@override
Stream<String> get logcat {
throw UnimplementedError();
}
@override
Future<void> clearLogs() async {}
@override
Future<void> stop(String packageName) async {}
@override
Future<void> reboot() {
return Process.run('idevicediagnostics', <String>['restart', '-u', deviceId]);
}
@override
Future<void> awaitDevice() async {}
}
class LinuxDevice extends Device {
const LinuxDevice();
@override
String get deviceId => 'linux';
@override
Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
return <String, dynamic>{};
}
@override
Future<void> home() async { }
@override
Future<bool> isAsleep() async {
return false;
}
@override
Future<bool> isAwake() async {
return true;
}
@override
Stream<String> get logcat => const Stream<String>.empty();
@override
Future<void> clearLogs() async {}
@override
Future<void> reboot() async { }
@override
Future<void> sendToSleep() async { }
@override
Future<void> stop(String packageName) async { }
@override
Future<void> tap(int x, int y) async { }
@override
Future<void> togglePower() async { }
@override
Future<void> unlock() async { }
@override
Future<void> wakeUp() async { }
@override
Future<void> awaitDevice() async {}
}
class MacosDevice extends Device {
const MacosDevice();
@override
String get deviceId => 'macos';
@override
Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
return <String, dynamic>{};
}
@override
Future<void> home() async { }
@override
Future<bool> isAsleep() async {
return false;
}
@override
Future<bool> isAwake() async {
return true;
}
@override
Stream<String> get logcat => const Stream<String>.empty();
@override
Future<void> clearLogs() async {}
@override
Future<void> reboot() async { }
@override
Future<void> sendToSleep() async { }
@override
Future<void> stop(String packageName) async { }
@override
Future<void> tap(int x, int y) async { }
@override
Future<void> togglePower() async { }
@override
Future<void> unlock() async { }
@override
Future<void> wakeUp() async { }
@override
Future<void> awaitDevice() async {}
}
class WindowsDevice extends Device {
const WindowsDevice();
@override
String get deviceId => 'windows';
@override
Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
return <String, dynamic>{};
}
@override
Future<void> home() async { }
@override
Future<bool> isAsleep() async {
return false;
}
@override
Future<bool> isAwake() async {
return true;
}
@override
Stream<String> get logcat => const Stream<String>.empty();
@override
Future<void> clearLogs() async {}
@override
Future<void> reboot() async { }
@override
Future<void> sendToSleep() async { }
@override
Future<void> stop(String packageName) async { }
@override
Future<void> tap(int x, int y) async { }
@override
Future<void> togglePower() async { }
@override
Future<void> unlock() async { }
@override
Future<void> wakeUp() async { }
@override
Future<void> awaitDevice() async {}
}
/// Fuchsia device.
class FuchsiaDevice extends Device {
const FuchsiaDevice({ required this.deviceId });
@override
final String deviceId;
// TODO(egarciad): Implement these for Fuchsia.
@override
Future<bool> isAwake() async => true;
@override
Future<bool> isAsleep() async => false;
@override
Future<void> wakeUp() async {}
@override
Future<void> sendToSleep() async {}
@override
Future<void> home() async {}
@override
Future<void> togglePower() async {}
@override
Future<void> unlock() async {}
@override
Future<void> tap(int x, int y) async {}
@override
Future<void> stop(String packageName) async {}
@override
Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
throw UnimplementedError();
}
@override
Stream<String> get logcat {
throw UnimplementedError();
}
@override
Future<void> clearLogs() async {}
@override
Future<void> reboot() async {
// Unsupported.
}
@override
Future<void> awaitDevice() async {}
}
/// Path to the `adb` executable.
String get adbPath {
final String? androidHome = Platform.environment['ANDROID_HOME'] ?? Platform.environment['ANDROID_SDK_ROOT'];
if (androidHome == null) {
throw const DeviceException(
'The ANDROID_HOME environment variable is '
'missing. The variable must point to the Android '
'SDK directory containing platform-tools.'
);
}
final String adbPath = path.join(androidHome, 'platform-tools/adb');
if (!canRun(adbPath)) {
throw DeviceException('adb not found at: $adbPath');
}
return path.absolute(adbPath);
}
class FakeDevice extends Device {
const FakeDevice({ required this.deviceId });
@override
final String deviceId;
@override
Future<bool> isAwake() async => true;
@override
Future<bool> isAsleep() async => false;
@override
Future<void> wakeUp() async {}
@override
Future<void> sendToSleep() async {}
@override
Future<void> home() async {}
@override
Future<void> togglePower() async {}
@override
Future<void> unlock() async {}
@override
Future<void> tap(int x, int y) async {
throw UnimplementedError();
}
@override
Future<Map<String, dynamic>> getMemoryStats(String packageName) async {
throw UnimplementedError();
}
@override
Stream<String> get logcat {
throw UnimplementedError();
}
@override
Future<void> clearLogs() async {}
@override
Future<void> stop(String packageName) async {}
@override
Future<void> reboot() async {
// Unsupported.
}
@override
Future<void> awaitDevice() async {}
}
class FakeDeviceDiscovery implements DeviceDiscovery {
factory FakeDeviceDiscovery() {
return _instance ??= FakeDeviceDiscovery._();
}
FakeDeviceDiscovery._();
static FakeDeviceDiscovery? _instance;
FakeDevice? _workingDevice;
@override
Future<FakeDevice> get workingDevice async {
if (_workingDevice == null) {
if (Platform.environment.containsKey(DeviceIdEnvName)) {
final String deviceId = Platform.environment[DeviceIdEnvName]!;
await chooseWorkingDeviceById(deviceId);
return _workingDevice!;
}
await chooseWorkingDevice();
}
return _workingDevice!;
}
/// The Fake is only available for by ID device discovery.
@override
Future<void> chooseWorkingDevice() async {
throw const DeviceException('No fake devices detected');
}
@override
Future<void> chooseWorkingDeviceById(String deviceId) async {
final String? matchedId = _findMatchId(await discoverDevices(), deviceId);
if (matchedId != null) {
_workingDevice = FakeDevice(deviceId: matchedId);
print('Choose device by ID: $matchedId');
return;
}
throw DeviceException(
'Device with ID $deviceId is not found for operating system: '
'$deviceOperatingSystem'
);
}
@override
Future<List<String>> discoverDevices() async {
return <String>['FAKE_SUCCESS', 'THIS_IS_A_FAKE'];
}
@override
Future<Map<String, HealthCheckResult>> checkDevices() async {
final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
for (final String deviceId in await discoverDevices()) {
results['fake-device-$deviceId'] = HealthCheckResult.success();
}
return results;
}
@override
Future<void> performPreflightTasks() async { }
}