Unverified Commit 1d8deb1b authored by Todd Volkert's avatar Todd Volkert Committed by GitHub
parent 0b3f2f61
...@@ -20,6 +20,7 @@ import '../device.dart'; ...@@ -20,6 +20,7 @@ import '../device.dart';
import '../globals.dart'; import '../globals.dart';
import '../project.dart'; import '../project.dart';
import '../protocol_discovery.dart'; import '../protocol_discovery.dart';
import '../reporting/reporting.dart';
import 'code_signing.dart'; import 'code_signing.dart';
import 'ios_workflow.dart'; import 'ios_workflow.dart';
import 'mac.dart'; import 'mac.dart';
...@@ -185,6 +186,9 @@ class IOSDevice extends Device { ...@@ -185,6 +186,9 @@ class IOSDevice extends Device {
} on IOSDeviceNotFoundError catch (error) { } on IOSDeviceNotFoundError catch (error) {
// Unable to find device with given udid. Possibly a network device. // Unable to find device with given udid. Possibly a network device.
printTrace('Error getting attached iOS device: $error'); printTrace('Error getting attached iOS device: $error');
} on IOSDeviceNotTrustedError catch (error) {
printTrace('Error getting attached iOS device information: $error');
UsageEvent('device', 'ios-trust-failure').send();
} }
} }
return devices; return devices;
......
...@@ -34,7 +34,7 @@ IMobileDevice get iMobileDevice => context.get<IMobileDevice>(); ...@@ -34,7 +34,7 @@ IMobileDevice get iMobileDevice => context.get<IMobileDevice>();
/// Specialized exception for expected situations where the ideviceinfo /// Specialized exception for expected situations where the ideviceinfo
/// tool responds with exit code 255 / 'No device found' message /// tool responds with exit code 255 / 'No device found' message
class IOSDeviceNotFoundError implements Exception { class IOSDeviceNotFoundError implements Exception {
IOSDeviceNotFoundError(this.message); const IOSDeviceNotFoundError(this.message);
final String message; final String message;
...@@ -42,6 +42,56 @@ class IOSDeviceNotFoundError implements Exception { ...@@ -42,6 +42,56 @@ class IOSDeviceNotFoundError implements Exception {
String toString() => message; String toString() => message;
} }
/// Exception representing an attempt to find information on an iOS device
/// that failed because the user had not paired the device with the host yet.
class IOSDeviceNotTrustedError implements Exception {
const IOSDeviceNotTrustedError(this.message, this.lockdownCode);
/// The error message to show to the user.
final String message;
/// The associated `lockdownd` error code.
final LockdownReturnCode lockdownCode;
@override
String toString() => '$message (lockdownd error code ${lockdownCode.code})';
}
/// Class specifying possible return codes from `lockdownd`.
///
/// This contains only a subset of the return codes that `lockdownd` can return,
/// as we only care about a limited subset. These values should be kept in sync with
/// https://github.com/libimobiledevice/libimobiledevice/blob/26373b3/include/libimobiledevice/lockdown.h#L37
class LockdownReturnCode {
const LockdownReturnCode._(this.code);
/// Creates a new [LockdownReturnCode] from the specified OS exit code.
///
/// If the [code] maps to one of the known codes, a `const` instance will be
/// returned.
factory LockdownReturnCode.fromCode(int code) {
final Map<int, LockdownReturnCode> knownCodes = <int, LockdownReturnCode>{
pairingDialogResponsePending.code: pairingDialogResponsePending,
invalidHostId.code: invalidHostId,
};
return knownCodes.containsKey(code) ? knownCodes[code] : LockdownReturnCode._(code);
}
/// The OS exit code.
final int code;
/// Error code indicating that the pairing dialog has been shown to the user,
/// and the user has not yet responded as to whether to trust the host.
static const LockdownReturnCode pairingDialogResponsePending = LockdownReturnCode._(19);
/// Error code indicating that the host is not trusted.
///
/// This can happen if the user explicitly says "do not trust this computer"
/// or if they revoke all trusted computers in the device settings.
static const LockdownReturnCode invalidHostId = LockdownReturnCode._(21);
}
class IMobileDevice { class IMobileDevice {
IMobileDevice() IMobileDevice()
: _ideviceIdPath = artifacts.getArtifactPath(Artifact.ideviceId, platform: TargetPlatform.ios) : _ideviceIdPath = artifacts.getArtifactPath(Artifact.ideviceId, platform: TargetPlatform.ios)
...@@ -159,7 +209,21 @@ class IMobileDevice { ...@@ -159,7 +209,21 @@ class IMobileDevice {
), ),
); );
if (result.exitCode == 255 && result.stdout != null && result.stdout.contains('No device found')) if (result.exitCode == 255 && result.stdout != null && result.stdout.contains('No device found'))
throw IOSDeviceNotFoundError('ideviceinfo could not find device:\n${result.stdout}'); throw IOSDeviceNotFoundError('ideviceinfo could not find device:\n${result.stdout}. Try unlocking attached devices.');
if (result.exitCode == 255 && result.stderr != null && result.stderr.contains('Could not connect to lockdownd')) {
if (result.stderr.contains('error code -${LockdownReturnCode.pairingDialogResponsePending.code}')) {
throw const IOSDeviceNotTrustedError(
'Device info unavailable. Is the device asking to "Trust This Computer?"',
LockdownReturnCode.pairingDialogResponsePending,
);
}
if (result.stderr.contains('error code -${LockdownReturnCode.invalidHostId.code}')) {
throw const IOSDeviceNotTrustedError(
'Device info unavailable. Device pairing "trust" may have been revoked.',
LockdownReturnCode.invalidHostId,
);
}
}
if (result.exitCode != 0) if (result.exitCode != 0)
throw ToolExit('ideviceinfo returned an error:\n${result.stderr}'); throw ToolExit('ideviceinfo returned an error:\n${result.stderr}');
return result.stdout.trim(); return result.stdout.trim();
......
...@@ -371,7 +371,7 @@ f577a7903cc54959be2e34bc4f7f80b7009efcf4 ...@@ -371,7 +371,7 @@ f577a7903cc54959be2e34bc4f7f80b7009efcf4
when(iMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'DeviceName')) when(iMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'DeviceName'))
.thenAnswer((_) => Future<String>.value('La tele me regarde')); .thenAnswer((_) => Future<String>.value('La tele me regarde'));
when(iMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'DeviceName')) when(iMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'DeviceName'))
.thenThrow(IOSDeviceNotFoundError('Device not found')); .thenThrow(const IOSDeviceNotFoundError('Device not found'));
final List<IOSDevice> devices = await IOSDevice.getAttachedDevices(); final List<IOSDevice> devices = await IOSDevice.getAttachedDevices();
expect(devices, hasLength(1)); expect(devices, hasLength(1));
expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44'); expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44');
......
...@@ -113,6 +113,69 @@ void main() { ...@@ -113,6 +113,69 @@ void main() {
Artifacts: () => mockArtifacts, Artifacts: () => mockArtifacts,
}); });
testUsingContext('getInfoForDevice throws IOSDeviceNotFoundError when user has not yet trusted the host', () async {
when(mockArtifacts.getArtifactPath(Artifact.ideviceinfo, platform: anyNamed('platform'))).thenReturn(ideviceInfoPath);
when(mockProcessManager.run(
<String>[ideviceInfoPath, '-u', 'foo', '-k', 'bar'],
environment: <String, String>{'DYLD_LIBRARY_PATH': libimobiledevicePath},
)).thenAnswer((_) {
final ProcessResult result = ProcessResult(
1,
255,
'',
'ERROR: Could not connect to lockdownd, error code -${LockdownReturnCode.pairingDialogResponsePending.code}',
);
return Future<ProcessResult>.value(result);
});
expect(() async => await iMobileDevice.getInfoForDevice('foo', 'bar'), throwsA(isInstanceOf<IOSDeviceNotTrustedError>()));
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Cache: () => mockCache,
Artifacts: () => mockArtifacts,
});
testUsingContext('getInfoForDevice throws ToolExit lockdownd fails for unknown reason', () async {
when(mockArtifacts.getArtifactPath(Artifact.ideviceinfo, platform: anyNamed('platform'))).thenReturn(ideviceInfoPath);
when(mockProcessManager.run(
<String>[ideviceInfoPath, '-u', 'foo', '-k', 'bar'],
environment: <String, String>{'DYLD_LIBRARY_PATH': libimobiledevicePath},
)).thenAnswer((_) {
final ProcessResult result = ProcessResult(
1,
255,
'',
'ERROR: Could not connect to lockdownd, error code -12345',
);
return Future<ProcessResult>.value(result);
});
expect(() async => await iMobileDevice.getInfoForDevice('foo', 'bar'), throwsToolExit());
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Cache: () => mockCache,
Artifacts: () => mockArtifacts,
});
testUsingContext('getInfoForDevice throws IOSDeviceNotFoundError when host trust is revoked', () async {
when(mockArtifacts.getArtifactPath(Artifact.ideviceinfo, platform: anyNamed('platform'))).thenReturn(ideviceInfoPath);
when(mockProcessManager.run(
<String>[ideviceInfoPath, '-u', 'foo', '-k', 'bar'],
environment: <String, String>{'DYLD_LIBRARY_PATH': libimobiledevicePath},
)).thenAnswer((_) {
final ProcessResult result = ProcessResult(
1,
255,
'',
'ERROR: Could not connect to lockdownd, error code -${LockdownReturnCode.invalidHostId.code}',
);
return Future<ProcessResult>.value(result);
});
expect(() async => await iMobileDevice.getInfoForDevice('foo', 'bar'), throwsA(isInstanceOf<IOSDeviceNotTrustedError>()));
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Cache: () => mockCache,
Artifacts: () => mockArtifacts,
});
group('screenshot', () { group('screenshot', () {
final String outputPath = fs.path.join('some', 'test', 'path', 'image.png'); final String outputPath = fs.path.join('some', 'test', 'path', 'image.png');
MockProcessManager mockProcessManager; MockProcessManager mockProcessManager;
......
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