blob: 524f5c337862900d7b1a34a5f288b7e17b4cc775 [file] [log] [blame]
// Copyright 2020 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' show LineSplitter, json;
import 'dart:io';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'device.dart';
import 'health.dart';
import 'mac.dart';
import 'utils.dart';
/// The minimum battery level to run a task with a scale of 100%.
const int _kBatteryMinLevel = 15;
/// Identifiers for devices that should never be rebooted.
final Set<String> noRebootList = <String>{
'822ef7958bba573829d85eef4df6cbdd86593730', // 32bit iPhone requires manual intervention on reboot.
};
/// IOS implementation of [DeviceDiscovery].
///
/// Discovers available ios devices and chooses one to work with.
class IosDeviceDiscovery implements DeviceDiscovery {
factory IosDeviceDiscovery(File? output) {
return _instance ??= IosDeviceDiscovery._(output);
}
final File? _outputFilePath;
IosDeviceDiscovery._(this._outputFilePath);
@visibleForTesting
IosDeviceDiscovery.testing(this._outputFilePath);
static IosDeviceDiscovery? _instance;
@override
Future<List<Device>> discoverDevices({Duration retryDuration = const Duration(seconds: 10)}) async {
final List<Device> discoveredDevices =
LineSplitter.split(await deviceListOutput()).map((String id) => IosDevice(deviceId: id)).toList();
stdout.write('ios devices discovered: ${discoveredDevices.map((e) => e.deviceId).toList()}');
return discoveredDevices;
}
Future<String> deviceListOutput({
ProcessManager processManager = const LocalProcessManager(),
}) async {
final String fullPathIdeviceId = await getMacBinaryPath('idevice_id', processManager: processManager);
stdout.write('idevice_id path $fullPathIdeviceId');
return eval(fullPathIdeviceId, <String>['-l'], processManager: processManager);
}
@override
Future<Map<String, List<HealthCheckResult>>> checkDevices({ProcessManager? processManager}) async {
processManager ??= LocalProcessManager();
final Map<String, List<HealthCheckResult>> results = <String, List<HealthCheckResult>>{};
for (Device device in await discoverDevices()) {
final List<HealthCheckResult> checks = <HealthCheckResult>[];
checks.add(HealthCheckResult.success(kDeviceAccessCheckKey));
checks.add(await keychainUnlockCheck(processManager: processManager));
checks.add(await certCheck(processManager: processManager));
checks.add(await devicePairCheck(processManager: processManager));
checks.add(await userAutoLoginCheck(processManager: processManager));
checks.add(await deviceProvisioningProfileCheck(device.deviceId, processManager: processManager));
checks.add(await batteryLevelCheck(processManager: processManager));
results['ios-device-${device.deviceId}'] = checks;
}
final Map<String, Map<String, dynamic>> healthCheckMap = await healthcheck(results);
writeToFile(json.encode(healthCheckMap), _outputFilePath!);
return results;
}
/// Checks and returns the device properties.
@override
Future<Map<String, String>> deviceProperties({ProcessManager? processManager}) async {
return <String, String>{};
}
@override
Future<void> recoverDevices() async {
for (Device device in await discoverDevices()) {
await device.recover();
}
}
@visibleForTesting
Future<HealthCheckResult> deviceProvisioningProfileCheck(String? deviceId, {ProcessManager? processManager}) async {
HealthCheckResult healthCheckResult;
try {
final String? homeDir = Platform.environment['HOME'];
final String out = await eval(
'ls',
<String>['$homeDir/Library/MobileDevice/Provisioning\ Profiles'],
processManager: processManager,
);
// Split filenames
final profiles = LineSplitter.split(out).toList();
// Check all provisioning profiles in the directory to
// to see if any contain a valid profile
bool validProfileFound = false;
for (var file in profiles) {
final String provisionFileContent = await eval(
'security',
<String>['cms', '-D', '-i', '$homeDir/Library/MobileDevice/Provisioning\ Profiles/$file'],
processManager: processManager,
);
if (provisionFileContent.contains(deviceId!)) {
validProfileFound = true;
break;
}
}
// If any file contained a valid profile, then set result accordingly
if (validProfileFound) {
healthCheckResult = HealthCheckResult.success(kDeviceProvisioningProfileCheckKey);
} else {
healthCheckResult = HealthCheckResult.failure(
kDeviceProvisioningProfileCheckKey,
'device does not exist in the provisioning profile',
);
}
} on BuildFailedError catch (error) {
healthCheckResult = HealthCheckResult.failure(kDeviceProvisioningProfileCheckKey, error.toString());
}
return healthCheckResult;
}
@visibleForTesting
Future<HealthCheckResult> keychainUnlockCheck({ProcessManager? processManager}) async {
HealthCheckResult healthCheckResult;
try {
await eval(kUnlockLoginKeychain, <String>[], processManager: processManager);
healthCheckResult = HealthCheckResult.success(kKeychainUnlockCheckKey);
} on BuildFailedError catch (error) {
healthCheckResult = HealthCheckResult.failure(kKeychainUnlockCheckKey, error.toString());
}
return healthCheckResult;
}
@visibleForTesting
Future<HealthCheckResult> batteryLevelCheck({ProcessManager? processManager}) async {
HealthCheckResult healthCheckResult;
try {
final String batteryCheckResult = await eval(
'ideviceinfo',
<String>['-q', 'com.apple.mobile.battery', '-k', 'BatteryCurrentCapacity'],
processManager: processManager,
);
final int level = int.parse(batteryCheckResult.isEmpty ? '0' : batteryCheckResult);
if (level < _kBatteryMinLevel) {
healthCheckResult =
HealthCheckResult.failure(kBatteryLevelCheckKey, 'Battery level ($level) is below $_kBatteryMinLevel');
} else {
healthCheckResult = HealthCheckResult.success(kBatteryLevelCheckKey);
}
} on BuildFailedError catch (error) {
healthCheckResult = HealthCheckResult.failure(kBatteryLevelCheckKey, error.toString());
}
return healthCheckResult;
}
@visibleForTesting
Future<HealthCheckResult> certCheck({ProcessManager? processManager}) async {
HealthCheckResult healthCheckResult;
try {
final String certCheckResult =
await eval('security', <String>['find-identity', '-p', 'codesigning', '-v'], processManager: processManager);
if (certCheckResult.contains('Apple Development: Flutter Devicelab') &&
certCheckResult.contains('1 valid identities found') &&
!certCheckResult.contains('CSSMERR_TP_CERT_REVOKED')) {
healthCheckResult = HealthCheckResult.success(kCertCheckKey);
} else {
healthCheckResult = HealthCheckResult.failure(kCertCheckKey, certCheckResult);
}
} on BuildFailedError catch (error) {
healthCheckResult = HealthCheckResult.failure(kCertCheckKey, error.toString());
}
return healthCheckResult;
}
@visibleForTesting
Future<HealthCheckResult> devicePairCheck({ProcessManager? processManager}) async {
HealthCheckResult healthCheckResult;
try {
final String devicePairCheckResult =
await eval('idevicepair', <String>['validate'], processManager: processManager);
if (devicePairCheckResult.contains('SUCCESS')) {
healthCheckResult = HealthCheckResult.success(kDevicePairCheckKey);
} else {
healthCheckResult = HealthCheckResult.failure(kDevicePairCheckKey, devicePairCheckResult);
}
} on BuildFailedError catch (error) {
healthCheckResult = HealthCheckResult.failure(kDevicePairCheckKey, error.toString());
}
return healthCheckResult;
}
@override
Future<void> prepareDevices() async {
for (Device device in await discoverDevices()) {
await device.prepare();
}
}
}
/// iOS device.
class IosDevice implements Device {
const IosDevice({@required this.deviceId});
@override
final String? deviceId;
@override
Future<void> recover() async {
await uninstall_applications();
await restart_device();
}
@override
Future<void> prepare() async {
return;
}
/// Restart iOS device.
@visibleForTesting
Future<bool> restart_device({ProcessManager? processManager}) async {
processManager ??= LocalProcessManager();
try {
if (noRebootList.contains(deviceId)) {
stdout.write('Device not marked for reboot.');
return true;
}
final String fullPathIdevicediagnostics =
await getMacBinaryPath('idevicediagnostics', processManager: processManager);
await eval(fullPathIdevicediagnostics, <String>['restart'], processManager: processManager);
} on BuildFailedError catch (error) {
stderr.write('device restart fails: $error');
return false;
}
stdout.write('Restart device complete.');
return true;
}
/// Uninstall applications from a device.
///
/// This is to prevent application installation failure caused by using different signing
/// certificate from previous installed application.
/// Issue: https://github.com/flutter/flutter/issues/76896
@visibleForTesting
Future<bool> uninstall_applications({ProcessManager? processManager}) async {
processManager ??= LocalProcessManager();
String result;
final String fullPathIdeviceInstaller = await getMacBinaryPath('ideviceinstaller', processManager: processManager);
try {
result = await eval(fullPathIdeviceInstaller, <String>['-l'], processManager: processManager);
} on BuildFailedError catch (error) {
stderr.write('list applications fails: $error');
return false;
}
// Skip uninstalling process when no device is available or no application exists.
if (result == 'No device found.' || result == 'CFBundleIdentifier, CFBundleVersion, CFBundleDisplayName') {
stdout.write('No device was found or no application to uninstall exists.');
return true;
}
final List<String> results = result.trim().split('\n');
final List<String> bundleIdentifiers = results.sublist(1).map((e) => e.split(',')[0].trim()).toList();
try {
for (String bundleIdentifier in bundleIdentifiers) {
await eval(fullPathIdeviceInstaller, <String>['-U', bundleIdentifier], processManager: processManager);
}
} on BuildFailedError catch (error) {
stderr.write('uninstall applications fails: $error');
return false;
}
stdout.write('Uninstall complete.');
return true;
}
}