Unverified Commit 25e98b54 authored by Victoria Ashworth's avatar Victoria Ashworth Committed by GitHub

Fix duplicate devices from xcdevice with iOS 17 (#128802)

This PR fixes issue of duplicate entries from `xcdevice list` cause devices to not show in `flutter devices`, `flutter run`, etc.

When a duplicate entry is found, use the entry without errors as the authority. If both have errors, use the one with the higher SDK as the authority.

Fixes https://github.com/flutter/flutter/issues/128719.
parent fadcaee8
...@@ -16,6 +16,7 @@ import '../base/logger.dart'; ...@@ -16,6 +16,7 @@ import '../base/logger.dart';
import '../base/os.dart'; import '../base/os.dart';
import '../base/platform.dart'; import '../base/platform.dart';
import '../base/utils.dart'; import '../base/utils.dart';
import '../base/version.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../convert.dart'; import '../convert.dart';
import '../device.dart'; import '../device.dart';
...@@ -295,10 +296,13 @@ class IOSDevice extends Device { ...@@ -295,10 +296,13 @@ class IOSDevice extends Device {
final IMobileDevice _iMobileDevice; final IMobileDevice _iMobileDevice;
final IProxy _iproxy; final IProxy _iproxy;
Version? get sdkVersion {
return Version.parse(_sdkVersion);
}
/// May be 0 if version cannot be parsed. /// May be 0 if version cannot be parsed.
int get majorSdkVersion { int get majorSdkVersion {
final String? majorVersionString = _sdkVersion?.split('.').first.trim(); return sdkVersion?.major ?? 0;
return majorVersionString != null ? int.tryParse(majorVersionString) ?? 0 : 0;
} }
@override @override
......
...@@ -12,6 +12,7 @@ import '../base/io.dart'; ...@@ -12,6 +12,7 @@ import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/platform.dart'; import '../base/platform.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../base/version.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cache.dart'; import '../cache.dart';
import '../convert.dart'; import '../convert.dart';
...@@ -493,7 +494,7 @@ class XCDevice { ...@@ -493,7 +494,7 @@ class XCDevice {
// }, // },
// ... // ...
final List<IOSDevice> devices = <IOSDevice>[]; final Map<String, IOSDevice> deviceMap = <String, IOSDevice>{};
for (final Object device in allAvailableDevices) { for (final Object device in allAvailableDevices) {
if (device is Map<String, Object?>) { if (device is Map<String, Object?>) {
// Only include iPhone, iPad, iPod, or other iOS devices. // Only include iPhone, iPad, iPod, or other iOS devices.
...@@ -531,33 +532,57 @@ class XCDevice { ...@@ -531,33 +532,57 @@ class XCDevice {
} }
} }
String? sdkVersion = _sdkVersion(device); String? sdkVersionString = _sdkVersion(device);
if (sdkVersion != null) { if (sdkVersionString != null) {
final String? buildVersion = _buildVersion(device); final String? buildVersion = _buildVersion(device);
if (buildVersion != null) { if (buildVersion != null) {
sdkVersion = '$sdkVersion $buildVersion'; sdkVersionString = '$sdkVersionString $buildVersion';
} }
} }
devices.add(IOSDevice( // Duplicate entries started appearing in Xcode 15, possibly due to
// Xcode's new device connectivity stack.
// If a duplicate entry is found in `xcdevice list`, don't overwrite
// existing entry when the existing entry indicates the device is
// connected and the current entry indicates the device is not connected.
// Don't overwrite if current entry's sdkVersion is null.
// Don't overwrite if both entries indicate the device is not
// connected and the existing entry has a higher sdkVersion.
if (deviceMap.containsKey(identifier)) {
final IOSDevice deviceInMap = deviceMap[identifier]!;
if ((deviceInMap.isConnected && !isConnected) || sdkVersionString == null) {
continue;
}
final Version? sdkVersion = Version.parse(sdkVersionString);
if (!deviceInMap.isConnected &&
!isConnected &&
sdkVersion != null &&
deviceInMap.sdkVersion != null &&
deviceInMap.sdkVersion!.compareTo(sdkVersion) > 0) {
continue;
}
}
deviceMap[identifier] = IOSDevice(
identifier, identifier,
name: name, name: name,
cpuArchitecture: _cpuArchitecture(device), cpuArchitecture: _cpuArchitecture(device),
connectionInterface: _interfaceType(device), connectionInterface: _interfaceType(device),
isConnected: isConnected, isConnected: isConnected,
sdkVersion: sdkVersion, sdkVersion: sdkVersionString,
iProxy: _iProxy, iProxy: _iProxy,
fileSystem: globals.fs, fileSystem: globals.fs,
logger: _logger, logger: _logger,
iosDeploy: _iosDeploy, iosDeploy: _iosDeploy,
iMobileDevice: _iMobileDevice, iMobileDevice: _iMobileDevice,
platform: globals.platform, platform: globals.platform,
devModeEnabled: devModeEnabled devModeEnabled: devModeEnabled,
)); );
} }
} }
return devices; return deviceMap.values.toList();
} }
/// Despite the name, com.apple.platform.iphoneos includes iPhone, iPads, and all iOS devices. /// Despite the name, com.apple.platform.iphoneos includes iPhone, iPads, and all iOS devices.
......
...@@ -13,6 +13,7 @@ import 'package:flutter_tools/src/base/io.dart'; ...@@ -13,6 +13,7 @@ import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
...@@ -177,6 +178,121 @@ void main() { ...@@ -177,6 +178,121 @@ void main() {
).majorSdkVersion, 0); ).majorSdkVersion, 0);
}); });
testWithoutContext('parses sdk version', () {
Version? sdkVersion = IOSDevice(
'device-123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: fileSystem,
logger: logger,
platform: macPlatform,
iosDeploy: iosDeploy,
iMobileDevice: iMobileDevice,
name: 'iPhone 1',
cpuArchitecture: DarwinArch.arm64,
sdkVersion: '13.3.1',
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
devModeEnabled: true,
).sdkVersion;
Version expectedVersion = Version(13, 3, 1, text: '13.3.1');
expect(sdkVersion, isNotNull);
expect(sdkVersion!.toString(), expectedVersion.toString());
expect(sdkVersion.compareTo(expectedVersion), 0);
sdkVersion = IOSDevice(
'device-123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: fileSystem,
logger: logger,
platform: macPlatform,
iosDeploy: iosDeploy,
iMobileDevice: iMobileDevice,
name: 'iPhone 1',
cpuArchitecture: DarwinArch.arm64,
sdkVersion: '13.3.1 (20ADBC)',
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
devModeEnabled: true,
).sdkVersion;
expectedVersion = Version(13, 3, 1, text: '13.3.1 (20ADBC)');
expect(sdkVersion, isNotNull);
expect(sdkVersion!.toString(), expectedVersion.toString());
expect(sdkVersion.compareTo(expectedVersion), 0);
sdkVersion = IOSDevice(
'device-123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: fileSystem,
logger: logger,
platform: macPlatform,
iosDeploy: iosDeploy,
iMobileDevice: iMobileDevice,
name: 'iPhone 1',
cpuArchitecture: DarwinArch.arm64,
sdkVersion: '16.4.1(a) (20ADBC)',
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
devModeEnabled: true,
).sdkVersion;
expectedVersion = Version(16, 4, 1, text: '16.4.1(a) (20ADBC)');
expect(sdkVersion, isNotNull);
expect(sdkVersion!.toString(), expectedVersion.toString());
expect(sdkVersion.compareTo(expectedVersion), 0);
sdkVersion = IOSDevice(
'device-123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: fileSystem,
logger: logger,
platform: macPlatform,
iosDeploy: iosDeploy,
iMobileDevice: iMobileDevice,
name: 'iPhone 1',
cpuArchitecture: DarwinArch.arm64,
sdkVersion: '0',
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
devModeEnabled: true,
).sdkVersion;
expectedVersion = Version(0, 0, 0, text: '0');
expect(sdkVersion, isNotNull);
expect(sdkVersion!.toString(), expectedVersion.toString());
expect(sdkVersion.compareTo(expectedVersion), 0);
sdkVersion = IOSDevice(
'device-123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: fileSystem,
logger: logger,
platform: macPlatform,
iosDeploy: iosDeploy,
iMobileDevice: iMobileDevice,
name: 'iPhone 1',
cpuArchitecture: DarwinArch.arm64,
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
devModeEnabled: true,
).sdkVersion;
expect(sdkVersion, isNull);
sdkVersion = IOSDevice(
'device-123',
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: fileSystem,
logger: logger,
platform: macPlatform,
iosDeploy: iosDeploy,
iMobileDevice: iMobileDevice,
name: 'iPhone 1',
cpuArchitecture: DarwinArch.arm64,
sdkVersion: 'bogus',
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
devModeEnabled: true,
).sdkVersion;
expect(sdkVersion, isNull);
});
testWithoutContext('has build number in sdkNameAndVersion', () async { testWithoutContext('has build number in sdkNameAndVersion', () async {
final IOSDevice device = IOSDevice( final IOSDevice device = IOSDevice(
'device-123', 'device-123',
......
...@@ -791,7 +791,7 @@ void main() { ...@@ -791,7 +791,7 @@ void main() {
"available" : true, "available" : true,
"platform" : "com.apple.platform.iphoneos", "platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1", "modelCode" : "iPhone8,1",
"identifier" : "d83d5bc53967baa0ee18626ba87b6254b2ab5418", "identifier" : "43ad2fda7991b34fe1acbda82f9e2fd3d6ddc9f7",
"architecture" : "BOGUS", "architecture" : "BOGUS",
"modelName" : "Future iPad", "modelName" : "Future iPad",
"name" : "iPad" "name" : "iPad"
...@@ -865,6 +865,190 @@ void main() { ...@@ -865,6 +865,190 @@ void main() {
Platform: () => macPlatform, Platform: () => macPlatform,
}); });
testUsingContext('use connected entry when filtering out duplicates', () async {
const String devicesOutput = '''
[
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone"
},
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone",
"error" : {
"code" : -13,
"failureReason" : "",
"description" : "iPhone iPad is not connected",
"recoverySuggestion" : "Xcode will continue when iPhone is connected and unlocked.",
"domain" : "com.apple.platform.iphoneos"
}
}
]
''';
fakeProcessManager.addCommand(const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput,
));
final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices, hasLength(1));
expect(devices[0].id, 'c4ca6f7a53027d1b7e4972e28478e7a28e2faee2');
expect(devices[0].name, 'iPhone');
expect(await devices[0].sdkNameAndVersion, 'iOS 13.3 17C54');
expect(devices[0].cpuArchitecture, DarwinArch.arm64);
expect(devices[0].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[0].isConnected, true);
expect(fakeProcessManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
Platform: () => macPlatform,
Artifacts: () => Artifacts.test(),
});
testUsingContext('use entry with sdk when filtering out duplicates', () async {
const String devicesOutput = '''
[
{
"simulator" : false,
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone_1",
"error" : {
"code" : -13,
"failureReason" : "",
"description" : "iPhone iPad is not connected",
"recoverySuggestion" : "Xcode will continue when iPhone is connected and unlocked.",
"domain" : "com.apple.platform.iphoneos"
}
},
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone_2",
"error" : {
"code" : -13,
"failureReason" : "",
"description" : "iPhone iPad is not connected",
"recoverySuggestion" : "Xcode will continue when iPhone is connected and unlocked.",
"domain" : "com.apple.platform.iphoneos"
}
}
]
''';
fakeProcessManager.addCommand(const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput,
));
final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices, hasLength(1));
expect(devices[0].id, 'c4ca6f7a53027d1b7e4972e28478e7a28e2faee2');
expect(devices[0].name, 'iPhone_2');
expect(await devices[0].sdkNameAndVersion, 'iOS 13.3 17C54');
expect(devices[0].cpuArchitecture, DarwinArch.arm64);
expect(devices[0].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[0].isConnected, false);
expect(fakeProcessManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
Platform: () => macPlatform,
Artifacts: () => Artifacts.test(),
});
testUsingContext('use entry with higher sdk when filtering out duplicates', () async {
const String devicesOutput = '''
[
{
"simulator" : false,
"operatingSystemVersion" : "14.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone_1",
"error" : {
"code" : -13,
"failureReason" : "",
"description" : "iPhone iPad is not connected",
"recoverySuggestion" : "Xcode will continue when iPhone is connected and unlocked.",
"domain" : "com.apple.platform.iphoneos"
}
},
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone_2",
"error" : {
"code" : -13,
"failureReason" : "",
"description" : "iPhone iPad is not connected",
"recoverySuggestion" : "Xcode will continue when iPhone is connected and unlocked.",
"domain" : "com.apple.platform.iphoneos"
}
}
]
''';
fakeProcessManager.addCommand(const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput,
));
final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices, hasLength(1));
expect(devices[0].id, 'c4ca6f7a53027d1b7e4972e28478e7a28e2faee2');
expect(devices[0].name, 'iPhone_1');
expect(await devices[0].sdkNameAndVersion, 'iOS 14.3 17C54');
expect(devices[0].cpuArchitecture, DarwinArch.arm64);
expect(devices[0].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[0].isConnected, false);
expect(fakeProcessManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{
Platform: () => macPlatform,
Artifacts: () => Artifacts.test(),
});
testUsingContext('handles bad output',() async { testUsingContext('handles bad output',() async {
fakeProcessManager.addCommand(const FakeCommand( fakeProcessManager.addCommand(const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'], command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
......
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