mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

Relates to tracker issue: - https://github.com/flutter/flutter/issues/128251 This PR includes 3 major updates: - Adding the `commandHasTerminal` parameter for `Event.flutterCommandResult` - In `packages/flutter_tools/lib/src/runner/flutter_command.dart` - Adding the new event for `sendException` from package:usage to be `Event.exception` (this event can be used by all dash tools) - In `packages/flutter_tools/lib/runner.dart` - Migrating the generic `UsageEvent` which was only used for Apple related workflows for iOS and macOS. I did an initial analysis in this [sheet](https://docs.google.com/spreadsheets/d/11KJLkHXFpECMX7tw-trNkYSr5MHDG15XNGv6TgLjfQs/edit?resourcekey=0-j4qdvsOEEg3wQW79YlY1-g#gid=0) to identify all the call sites - Found in several files, highlighted in the sheet above
841 lines
29 KiB
Dart
841 lines
29 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 'package:meta/meta.dart';
|
|
import 'package:process/process.dart';
|
|
import 'package:unified_analytics/unified_analytics.dart';
|
|
|
|
import '../artifacts.dart';
|
|
import '../base/file_system.dart';
|
|
import '../base/io.dart';
|
|
import '../base/logger.dart';
|
|
import '../base/platform.dart';
|
|
import '../base/process.dart';
|
|
import '../base/version.dart';
|
|
import '../build_info.dart';
|
|
import '../cache.dart';
|
|
import '../convert.dart';
|
|
import '../device.dart';
|
|
import '../globals.dart' as globals;
|
|
import '../ios/core_devices.dart';
|
|
import '../ios/devices.dart';
|
|
import '../ios/ios_deploy.dart';
|
|
import '../ios/iproxy.dart';
|
|
import '../ios/mac.dart';
|
|
import '../ios/xcode_debug.dart';
|
|
import '../reporting/reporting.dart';
|
|
import 'xcode.dart';
|
|
|
|
class XCDeviceEventNotification {
|
|
XCDeviceEventNotification(
|
|
this.eventType,
|
|
this.eventInterface,
|
|
this.deviceIdentifier,
|
|
);
|
|
|
|
final XCDeviceEvent eventType;
|
|
final XCDeviceEventInterface eventInterface;
|
|
final String deviceIdentifier;
|
|
}
|
|
|
|
enum XCDeviceEvent {
|
|
attach,
|
|
detach,
|
|
}
|
|
|
|
enum XCDeviceEventInterface {
|
|
usb(name: 'usb', connectionInterface: DeviceConnectionInterface.attached),
|
|
wifi(name: 'wifi', connectionInterface: DeviceConnectionInterface.wireless);
|
|
|
|
const XCDeviceEventInterface({
|
|
required this.name,
|
|
required this.connectionInterface,
|
|
});
|
|
|
|
final String name;
|
|
final DeviceConnectionInterface connectionInterface;
|
|
}
|
|
|
|
/// A utility class for interacting with Xcode xcdevice command line tools.
|
|
class XCDevice {
|
|
XCDevice({
|
|
required Artifacts artifacts,
|
|
required Cache cache,
|
|
required ProcessManager processManager,
|
|
required Logger logger,
|
|
required Xcode xcode,
|
|
required Platform platform,
|
|
required IProxy iproxy,
|
|
required FileSystem fileSystem,
|
|
required Analytics analytics,
|
|
@visibleForTesting
|
|
IOSCoreDeviceControl? coreDeviceControl,
|
|
XcodeDebug? xcodeDebug,
|
|
}) : _processUtils = ProcessUtils(logger: logger, processManager: processManager),
|
|
_logger = logger,
|
|
_iMobileDevice = IMobileDevice(
|
|
artifacts: artifacts,
|
|
cache: cache,
|
|
logger: logger,
|
|
processManager: processManager,
|
|
),
|
|
_iosDeploy = IOSDeploy(
|
|
artifacts: artifacts,
|
|
cache: cache,
|
|
logger: logger,
|
|
platform: platform,
|
|
processManager: processManager,
|
|
),
|
|
_coreDeviceControl = coreDeviceControl ?? IOSCoreDeviceControl(
|
|
logger: logger,
|
|
processManager: processManager,
|
|
xcode: xcode,
|
|
fileSystem: fileSystem,
|
|
),
|
|
_xcodeDebug = xcodeDebug ?? XcodeDebug(
|
|
logger: logger,
|
|
processManager: processManager,
|
|
xcode: xcode,
|
|
fileSystem: fileSystem,
|
|
),
|
|
_iProxy = iproxy,
|
|
_xcode = xcode,
|
|
_analytics = analytics {
|
|
|
|
_setupDeviceIdentifierByEventStream();
|
|
}
|
|
|
|
void dispose() {
|
|
_usbDeviceObserveProcess?.kill();
|
|
_wifiDeviceObserveProcess?.kill();
|
|
_usbDeviceWaitProcess?.kill();
|
|
_wifiDeviceWaitProcess?.kill();
|
|
}
|
|
|
|
final ProcessUtils _processUtils;
|
|
final Logger _logger;
|
|
final IMobileDevice _iMobileDevice;
|
|
final IOSDeploy _iosDeploy;
|
|
final Xcode _xcode;
|
|
final IProxy _iProxy;
|
|
final IOSCoreDeviceControl _coreDeviceControl;
|
|
final XcodeDebug _xcodeDebug;
|
|
final Analytics _analytics;
|
|
|
|
List<Object>? _cachedListResults;
|
|
|
|
Process? _usbDeviceObserveProcess;
|
|
Process? _wifiDeviceObserveProcess;
|
|
StreamController<XCDeviceEventNotification>? _observeStreamController;
|
|
|
|
@visibleForTesting
|
|
StreamController<XCDeviceEventNotification>? waitStreamController;
|
|
|
|
Process? _usbDeviceWaitProcess;
|
|
Process? _wifiDeviceWaitProcess;
|
|
|
|
void _setupDeviceIdentifierByEventStream() {
|
|
// _observeStreamController Should always be available for listeners
|
|
// in case polling needs to be stopped and restarted.
|
|
_observeStreamController = StreamController<XCDeviceEventNotification>.broadcast(
|
|
onListen: _startObservingTetheredIOSDevices,
|
|
onCancel: _stopObservingTetheredIOSDevices,
|
|
);
|
|
}
|
|
|
|
bool get isInstalled => _xcode.isInstalledAndMeetsVersionCheck;
|
|
|
|
Future<List<Object>?> _getAllDevices({
|
|
bool useCache = false,
|
|
required Duration timeout,
|
|
}) async {
|
|
if (!isInstalled) {
|
|
_logger.printTrace("Xcode not found. Run 'flutter doctor' for more information.");
|
|
return null;
|
|
}
|
|
if (useCache && _cachedListResults != null) {
|
|
return _cachedListResults;
|
|
}
|
|
try {
|
|
// USB-tethered devices should be found quickly. 1 second timeout is faster than the default.
|
|
final RunResult result = await _processUtils.run(
|
|
<String>[
|
|
..._xcode.xcrunCommand(),
|
|
'xcdevice',
|
|
'list',
|
|
'--timeout',
|
|
timeout.inSeconds.toString(),
|
|
],
|
|
throwOnError: true,
|
|
);
|
|
if (result.exitCode == 0) {
|
|
final String listOutput = result.stdout;
|
|
try {
|
|
final List<Object> listResults = (json.decode(result.stdout) as List<Object?>).whereType<Object>().toList();
|
|
_cachedListResults = listResults;
|
|
return listResults;
|
|
} on FormatException {
|
|
// xcdevice logs errors and crashes to stdout.
|
|
_logger.printError('xcdevice returned non-JSON response: $listOutput');
|
|
return null;
|
|
}
|
|
}
|
|
_logger.printTrace('xcdevice returned an error:\n${result.stderr}');
|
|
} on ProcessException catch (exception) {
|
|
_logger.printTrace('Process exception running xcdevice list:\n$exception');
|
|
} on ArgumentError catch (exception) {
|
|
_logger.printTrace('Argument exception running xcdevice list:\n$exception');
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Observe identifiers (UDIDs) of devices as they attach and detach.
|
|
///
|
|
/// Each attach and detach event contains information on the event type,
|
|
/// the event interface, and the device identifer.
|
|
Stream<XCDeviceEventNotification>? observedDeviceEvents() {
|
|
if (!isInstalled) {
|
|
_logger.printTrace("Xcode not found. Run 'flutter doctor' for more information.");
|
|
return null;
|
|
}
|
|
return _observeStreamController?.stream;
|
|
}
|
|
|
|
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
|
|
// Attach: 00008027-00192736010F802E
|
|
// Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
|
|
final RegExp _observationIdentifierPattern = RegExp(r'^(\w*): ([\w-]*)$');
|
|
|
|
Future<void> _startObservingTetheredIOSDevices() async {
|
|
try {
|
|
if (_usbDeviceObserveProcess != null || _wifiDeviceObserveProcess != null) {
|
|
throw Exception('xcdevice observe restart failed');
|
|
}
|
|
|
|
_usbDeviceObserveProcess = await _startObserveProcess(
|
|
XCDeviceEventInterface.usb,
|
|
);
|
|
|
|
_wifiDeviceObserveProcess = await _startObserveProcess(
|
|
XCDeviceEventInterface.wifi,
|
|
);
|
|
|
|
final Future<void> usbProcessExited = _usbDeviceObserveProcess!.exitCode.then((int status) {
|
|
_logger.printTrace('xcdevice observe --usb exited with code $exitCode');
|
|
// Kill other process in case only one was killed.
|
|
_wifiDeviceObserveProcess?.kill();
|
|
});
|
|
|
|
final Future<void> wifiProcessExited = _wifiDeviceObserveProcess!.exitCode.then((int status) {
|
|
_logger.printTrace('xcdevice observe --wifi exited with code $exitCode');
|
|
// Kill other process in case only one was killed.
|
|
_usbDeviceObserveProcess?.kill();
|
|
});
|
|
|
|
unawaited(Future.wait(<Future<void>>[
|
|
usbProcessExited,
|
|
wifiProcessExited,
|
|
]).whenComplete(() async {
|
|
if (_observeStreamController?.hasListener ?? false) {
|
|
// Tell listeners the process died.
|
|
await _observeStreamController?.close();
|
|
}
|
|
_usbDeviceObserveProcess = null;
|
|
_wifiDeviceObserveProcess = null;
|
|
|
|
// Reopen it so new listeners can resume polling.
|
|
_setupDeviceIdentifierByEventStream();
|
|
}));
|
|
} on ProcessException catch (exception, stackTrace) {
|
|
_observeStreamController?.addError(exception, stackTrace);
|
|
} on ArgumentError catch (exception, stackTrace) {
|
|
_observeStreamController?.addError(exception, stackTrace);
|
|
}
|
|
}
|
|
|
|
Future<Process> _startObserveProcess(XCDeviceEventInterface eventInterface) {
|
|
// Run in interactive mode (via script) to convince
|
|
// xcdevice it has a terminal attached in order to redirect stdout.
|
|
return _streamXCDeviceEventCommand(
|
|
<String>[
|
|
'script',
|
|
'-t',
|
|
'0',
|
|
'/dev/null',
|
|
..._xcode.xcrunCommand(),
|
|
'xcdevice',
|
|
'observe',
|
|
'--${eventInterface.name}',
|
|
],
|
|
prefix: 'xcdevice observe --${eventInterface.name}: ',
|
|
mapFunction: (String line) {
|
|
final XCDeviceEventNotification? event = _processXCDeviceStdOut(
|
|
line,
|
|
eventInterface,
|
|
);
|
|
if (event != null) {
|
|
_observeStreamController?.add(event);
|
|
}
|
|
return line;
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Starts the command and streams stdout/stderr from the child process to
|
|
/// this process' stdout/stderr.
|
|
///
|
|
/// If [mapFunction] is present, all lines are forwarded to [mapFunction] for
|
|
/// further processing.
|
|
Future<Process> _streamXCDeviceEventCommand(
|
|
List<String> cmd, {
|
|
String prefix = '',
|
|
StringConverter? mapFunction,
|
|
}) async {
|
|
final Process process = await _processUtils.start(cmd);
|
|
|
|
final StreamSubscription<String> stdoutSubscription = process.stdout
|
|
.transform<String>(utf8.decoder)
|
|
.transform<String>(const LineSplitter())
|
|
.listen((String line) {
|
|
String? mappedLine = line;
|
|
if (mapFunction != null) {
|
|
mappedLine = mapFunction(line);
|
|
}
|
|
if (mappedLine != null) {
|
|
final String message = '$prefix$mappedLine';
|
|
_logger.printTrace(message);
|
|
}
|
|
});
|
|
final StreamSubscription<String> stderrSubscription = process.stderr
|
|
.transform<String>(utf8.decoder)
|
|
.transform<String>(const LineSplitter())
|
|
.listen((String line) {
|
|
String? mappedLine = line;
|
|
if (mapFunction != null) {
|
|
mappedLine = mapFunction(line);
|
|
}
|
|
if (mappedLine != null) {
|
|
_logger.printError('$prefix$mappedLine', wrap: false);
|
|
}
|
|
});
|
|
|
|
unawaited(process.exitCode.whenComplete(() {
|
|
stdoutSubscription.cancel();
|
|
stderrSubscription.cancel();
|
|
}));
|
|
|
|
return process;
|
|
}
|
|
|
|
void _stopObservingTetheredIOSDevices() {
|
|
_usbDeviceObserveProcess?.kill();
|
|
_wifiDeviceObserveProcess?.kill();
|
|
}
|
|
|
|
XCDeviceEventNotification? _processXCDeviceStdOut(
|
|
String line,
|
|
XCDeviceEventInterface eventInterface,
|
|
) {
|
|
// xcdevice observe example output of UDIDs:
|
|
//
|
|
// Listening for all devices, on both interfaces.
|
|
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
|
|
// Attach: 00008027-00192736010F802E
|
|
// Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
|
|
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
|
|
final RegExpMatch? match = _observationIdentifierPattern.firstMatch(line);
|
|
if (match != null && match.groupCount == 2) {
|
|
final String verb = match.group(1)!.toLowerCase();
|
|
final String identifier = match.group(2)!;
|
|
if (verb.startsWith('attach')) {
|
|
return XCDeviceEventNotification(
|
|
XCDeviceEvent.attach,
|
|
eventInterface,
|
|
identifier,
|
|
);
|
|
} else if (verb.startsWith('detach')) {
|
|
return XCDeviceEventNotification(
|
|
XCDeviceEvent.detach,
|
|
eventInterface,
|
|
identifier,
|
|
);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Wait for a connect event for a specific device. Must use device's exact UDID.
|
|
///
|
|
/// To cancel this process, call [cancelWaitForDeviceToConnect].
|
|
Future<XCDeviceEventNotification?> waitForDeviceToConnect(
|
|
String deviceId,
|
|
) async {
|
|
try {
|
|
if (_usbDeviceWaitProcess != null || _wifiDeviceWaitProcess != null) {
|
|
throw Exception('xcdevice wait restart failed');
|
|
}
|
|
|
|
waitStreamController = StreamController<XCDeviceEventNotification>();
|
|
|
|
_usbDeviceWaitProcess = await _startWaitProcess(
|
|
deviceId,
|
|
XCDeviceEventInterface.usb,
|
|
);
|
|
|
|
_wifiDeviceWaitProcess = await _startWaitProcess(
|
|
deviceId,
|
|
XCDeviceEventInterface.wifi,
|
|
);
|
|
|
|
final Future<void> usbProcessExited = _usbDeviceWaitProcess!.exitCode.then((int status) {
|
|
_logger.printTrace('xcdevice wait --usb exited with code $exitCode');
|
|
// Kill other process in case only one was killed.
|
|
_wifiDeviceWaitProcess?.kill();
|
|
});
|
|
|
|
final Future<void> wifiProcessExited = _wifiDeviceWaitProcess!.exitCode.then((int status) {
|
|
_logger.printTrace('xcdevice wait --wifi exited with code $exitCode');
|
|
// Kill other process in case only one was killed.
|
|
_usbDeviceWaitProcess?.kill();
|
|
});
|
|
|
|
final Future<void> allProcessesExited = Future.wait(
|
|
<Future<void>>[
|
|
usbProcessExited,
|
|
wifiProcessExited,
|
|
]).whenComplete(() async {
|
|
_usbDeviceWaitProcess = null;
|
|
_wifiDeviceWaitProcess = null;
|
|
await waitStreamController?.close();
|
|
});
|
|
|
|
return await Future.any(
|
|
<Future<XCDeviceEventNotification?>>[
|
|
allProcessesExited.then((_) => null),
|
|
waitStreamController!.stream.first.whenComplete(() async {
|
|
cancelWaitForDeviceToConnect();
|
|
}),
|
|
],
|
|
);
|
|
} on ProcessException catch (exception, stackTrace) {
|
|
_logger.printTrace('Process exception running xcdevice wait:\n$exception\n$stackTrace');
|
|
} on ArgumentError catch (exception, stackTrace) {
|
|
_logger.printTrace('Process exception running xcdevice wait:\n$exception\n$stackTrace');
|
|
} on StateError {
|
|
_logger.printTrace('Stream broke before first was found');
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<Process> _startWaitProcess(String deviceId, XCDeviceEventInterface eventInterface) {
|
|
// Run in interactive mode (via script) to convince
|
|
// xcdevice it has a terminal attached in order to redirect stdout.
|
|
return _streamXCDeviceEventCommand(
|
|
<String>[
|
|
'script',
|
|
'-t',
|
|
'0',
|
|
'/dev/null',
|
|
..._xcode.xcrunCommand(),
|
|
'xcdevice',
|
|
'wait',
|
|
'--${eventInterface.name}',
|
|
deviceId,
|
|
],
|
|
prefix: 'xcdevice wait --${eventInterface.name}: ',
|
|
mapFunction: (String line) {
|
|
final XCDeviceEventNotification? event = _processXCDeviceStdOut(
|
|
line,
|
|
eventInterface,
|
|
);
|
|
if (event != null && event.eventType == XCDeviceEvent.attach) {
|
|
waitStreamController?.add(event);
|
|
}
|
|
return line;
|
|
},
|
|
);
|
|
}
|
|
|
|
void cancelWaitForDeviceToConnect() {
|
|
_usbDeviceWaitProcess?.kill();
|
|
_wifiDeviceWaitProcess?.kill();
|
|
}
|
|
|
|
/// A list of [IOSDevice]s. This list includes connected devices and
|
|
/// disconnected wireless devices.
|
|
///
|
|
/// Sometimes devices may have incorrect connection information
|
|
/// (`isConnected`, `connectionInterface`) if it timed out before it could get the
|
|
/// information. Wireless devices can take longer to get the correct
|
|
/// information.
|
|
///
|
|
/// [timeout] defaults to 2 seconds.
|
|
Future<List<IOSDevice>> getAvailableIOSDevices({ Duration? timeout }) async {
|
|
final List<Object>? allAvailableDevices = await _getAllDevices(timeout: timeout ?? const Duration(seconds: 2));
|
|
|
|
if (allAvailableDevices == null) {
|
|
return const <IOSDevice>[];
|
|
}
|
|
|
|
final Map<String, IOSCoreDevice> coreDeviceMap = <String, IOSCoreDevice>{};
|
|
if (_xcode.isDevicectlInstalled) {
|
|
final List<IOSCoreDevice> coreDevices = await _coreDeviceControl.getCoreDevices();
|
|
for (final IOSCoreDevice device in coreDevices) {
|
|
if (device.udid == null) {
|
|
continue;
|
|
}
|
|
coreDeviceMap[device.udid!] = device;
|
|
}
|
|
}
|
|
|
|
// [
|
|
// {
|
|
// "simulator" : true,
|
|
// "operatingSystemVersion" : "13.3 (17K446)",
|
|
// "available" : true,
|
|
// "platform" : "com.apple.platform.appletvsimulator",
|
|
// "modelCode" : "AppleTV5,3",
|
|
// "identifier" : "CBB5E1ED-2172-446E-B4E7-F2B5823DBBA6",
|
|
// "architecture" : "x86_64",
|
|
// "modelName" : "Apple TV",
|
|
// "name" : "Apple TV"
|
|
// },
|
|
// {
|
|
// "simulator" : false,
|
|
// "operatingSystemVersion" : "13.3 (17C54)",
|
|
// "interface" : "usb",
|
|
// "available" : true,
|
|
// "platform" : "com.apple.platform.iphoneos",
|
|
// "modelCode" : "iPhone8,1",
|
|
// "identifier" : "d83d5bc53967baa0ee18626ba87b6254b2ab5418",
|
|
// "architecture" : "arm64",
|
|
// "modelName" : "iPhone 6s",
|
|
// "name" : "iPhone"
|
|
// },
|
|
// {
|
|
// "simulator" : true,
|
|
// "operatingSystemVersion" : "6.1.1 (17S445)",
|
|
// "available" : true,
|
|
// "platform" : "com.apple.platform.watchsimulator",
|
|
// "modelCode" : "Watch5,4",
|
|
// "identifier" : "2D74FB11-88A0-44D0-B81E-C0C142B1C94A",
|
|
// "architecture" : "i386",
|
|
// "modelName" : "Apple Watch Series 5 - 44mm",
|
|
// "name" : "Apple Watch Series 5 - 44mm"
|
|
// },
|
|
// ...
|
|
|
|
final Map<String, IOSDevice> deviceMap = <String, IOSDevice>{};
|
|
for (final Object device in allAvailableDevices) {
|
|
if (device is Map<String, Object?>) {
|
|
// Only include iPhone, iPad, iPod, or other iOS devices.
|
|
if (!_isIPhoneOSDevice(device)) {
|
|
continue;
|
|
}
|
|
final String? identifier = device['identifier'] as String?;
|
|
final String? name = device['name'] as String?;
|
|
if (identifier == null || name == null) {
|
|
continue;
|
|
}
|
|
bool devModeEnabled = true;
|
|
bool isConnected = true;
|
|
final Map<String, Object?>? errorProperties = _errorProperties(device);
|
|
if (errorProperties != null) {
|
|
final String? errorMessage = _parseErrorMessage(errorProperties);
|
|
if (errorMessage != null) {
|
|
if (errorMessage.contains('not paired')) {
|
|
UsageEvent('device', 'ios-trust-failure', flutterUsage: globals.flutterUsage).send();
|
|
_analytics.send(Event.appleUsageEvent(workflow: 'device', parameter: 'ios-trust-failure'));
|
|
|
|
}
|
|
_logger.printTrace(errorMessage);
|
|
}
|
|
|
|
final int? code = _errorCode(errorProperties);
|
|
|
|
// Temporary error -10: iPhone is busy: Preparing debugger support for iPhone.
|
|
// Sometimes the app launch will fail on these devices until Xcode is done setting up the device.
|
|
// Other times this is a false positive and the app will successfully launch despite the error.
|
|
if (code != -10) {
|
|
isConnected = false;
|
|
}
|
|
|
|
if (code == 6) {
|
|
devModeEnabled = false;
|
|
}
|
|
}
|
|
|
|
String? sdkVersionString = _sdkVersion(device);
|
|
|
|
if (sdkVersionString != null) {
|
|
final String? buildVersion = _buildVersion(device);
|
|
if (buildVersion != null) {
|
|
sdkVersionString = '$sdkVersionString $buildVersion';
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
DeviceConnectionInterface connectionInterface = _interfaceType(device);
|
|
|
|
// CoreDevices (devices with iOS 17 and greater) no longer reflect the
|
|
// correct connection interface or developer mode status in `xcdevice`.
|
|
// Use `devicectl` to get that information for CoreDevices.
|
|
final IOSCoreDevice? coreDevice = coreDeviceMap[identifier];
|
|
if (coreDevice != null) {
|
|
if (coreDevice.connectionInterface != null) {
|
|
connectionInterface = coreDevice.connectionInterface!;
|
|
}
|
|
|
|
if (coreDevice.deviceProperties?.developerModeStatus != 'enabled') {
|
|
devModeEnabled = false;
|
|
}
|
|
}
|
|
|
|
deviceMap[identifier] = IOSDevice(
|
|
identifier,
|
|
name: name,
|
|
cpuArchitecture: _cpuArchitecture(device),
|
|
connectionInterface: connectionInterface,
|
|
isConnected: isConnected,
|
|
sdkVersion: sdkVersionString,
|
|
iProxy: _iProxy,
|
|
fileSystem: globals.fs,
|
|
logger: _logger,
|
|
iosDeploy: _iosDeploy,
|
|
iMobileDevice: _iMobileDevice,
|
|
coreDeviceControl: _coreDeviceControl,
|
|
xcodeDebug: _xcodeDebug,
|
|
platform: globals.platform,
|
|
devModeEnabled: devModeEnabled,
|
|
isCoreDevice: coreDevice != null,
|
|
);
|
|
}
|
|
}
|
|
return deviceMap.values.toList();
|
|
}
|
|
|
|
/// Despite the name, com.apple.platform.iphoneos includes iPhone, iPads, and all iOS devices.
|
|
/// Excludes simulators.
|
|
static bool _isIPhoneOSDevice(Map<String, Object?> deviceProperties) {
|
|
final Object? platform = deviceProperties['platform'];
|
|
if (platform is String) {
|
|
return platform == 'com.apple.platform.iphoneos';
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static Map<String, Object?>? _errorProperties(Map<String, Object?> deviceProperties) {
|
|
final Object? error = deviceProperties['error'];
|
|
return error is Map<String, Object?> ? error : null;
|
|
}
|
|
|
|
static int? _errorCode(Map<String, Object?>? errorProperties) {
|
|
if (errorProperties == null) {
|
|
return null;
|
|
}
|
|
final Object? code = errorProperties['code'];
|
|
return code is int ? code : null;
|
|
}
|
|
|
|
static DeviceConnectionInterface _interfaceType(Map<String, Object?> deviceProperties) {
|
|
// Interface can be "usb" or "network". It can also be missing
|
|
// (e.g. simulators do not have an interface property).
|
|
// If the interface is "network", use `DeviceConnectionInterface.wireless`,
|
|
// otherwise use `DeviceConnectionInterface.attached.
|
|
final Object? interface = deviceProperties['interface'];
|
|
if (interface is String && interface.toLowerCase() == 'network') {
|
|
return DeviceConnectionInterface.wireless;
|
|
}
|
|
return DeviceConnectionInterface.attached;
|
|
}
|
|
|
|
static String? _sdkVersion(Map<String, Object?> deviceProperties) {
|
|
final Object? operatingSystemVersion = deviceProperties['operatingSystemVersion'];
|
|
if (operatingSystemVersion is String) {
|
|
// Parse out the OS version, ignore the build number in parentheses.
|
|
// "13.3 (17C54)"
|
|
final RegExp operatingSystemRegex = RegExp(r'(.*) \(.*\)$');
|
|
if (operatingSystemRegex.hasMatch(operatingSystemVersion.trim())) {
|
|
return operatingSystemRegex.firstMatch(operatingSystemVersion.trim())?.group(1);
|
|
}
|
|
return operatingSystemVersion;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static String? _buildVersion(Map<String, Object?> deviceProperties) {
|
|
final Object? operatingSystemVersion = deviceProperties['operatingSystemVersion'];
|
|
if (operatingSystemVersion is String) {
|
|
// Parse out the build version, for example 17C54 from "13.3 (17C54)".
|
|
final RegExp buildVersionRegex = RegExp(r'\(.*\)$');
|
|
return buildVersionRegex.firstMatch(operatingSystemVersion)?.group(0)?.replaceAll(RegExp('[()]'), '');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
DarwinArch _cpuArchitecture(Map<String, Object?> deviceProperties) {
|
|
DarwinArch? cpuArchitecture;
|
|
final Object? architecture = deviceProperties['architecture'];
|
|
if (architecture is String) {
|
|
try {
|
|
cpuArchitecture = getIOSArchForName(architecture);
|
|
} on Exception {
|
|
// Fallback to default iOS architecture. Future-proof against a
|
|
// theoretical version of Xcode that changes this string to something
|
|
// slightly different like "ARM64", or armv7 variations like
|
|
// armv7s and armv7f.
|
|
if (architecture.startsWith('armv7')) {
|
|
cpuArchitecture = DarwinArch.armv7;
|
|
} else {
|
|
cpuArchitecture = DarwinArch.arm64;
|
|
}
|
|
_logger.printWarning(
|
|
'Unknown architecture $architecture, defaulting to '
|
|
'${cpuArchitecture.name}',
|
|
);
|
|
}
|
|
}
|
|
return cpuArchitecture ?? DarwinArch.arm64;
|
|
}
|
|
|
|
/// Error message parsed from xcdevice. null if no error.
|
|
static String? _parseErrorMessage(Map<String, Object?>? errorProperties) {
|
|
// {
|
|
// "simulator" : false,
|
|
// "operatingSystemVersion" : "13.3 (17C54)",
|
|
// "interface" : "usb",
|
|
// "available" : false,
|
|
// "platform" : "com.apple.platform.iphoneos",
|
|
// "modelCode" : "iPhone8,1",
|
|
// "identifier" : "98206e7a4afd4aedaff06e687594e089dede3c44",
|
|
// "architecture" : "arm64",
|
|
// "modelName" : "iPhone 6s",
|
|
// "name" : "iPhone",
|
|
// "error" : {
|
|
// "code" : -9,
|
|
// "failureReason" : "",
|
|
// "underlyingErrors" : [
|
|
// {
|
|
// "code" : 5,
|
|
// "failureReason" : "allowsSecureServices: 1. isConnected: 0. Platform: <DVTPlatform:0x7f804ce32880:'com.apple.platform.iphoneos':<DVTFilePath:0x7f804ce32800:'\/Users\/magder\/Applications\/Xcode_11-3-1.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform'>>. DTDKDeviceIdentifierIsIDID: 0",
|
|
// "description" : "📱<DVTiOSDevice (0x7f801f190450), iPhone, iPhone, 13.3 (17C54), d83d5bc53967baa0ee18626ba87b6254b2ab5418> -- Failed _shouldMakeReadyForDevelopment check even though device is not locked by passcode.",
|
|
// "recoverySuggestion" : "",
|
|
// "domain" : "com.apple.platform.iphoneos"
|
|
// }
|
|
// ],
|
|
// "description" : "iPhone is not paired with your computer.",
|
|
// "recoverySuggestion" : "To use iPhone with Xcode, unlock it and choose to trust this computer when prompted.",
|
|
// "domain" : "com.apple.platform.iphoneos"
|
|
// }
|
|
// },
|
|
// {
|
|
// "simulator" : false,
|
|
// "operatingSystemVersion" : "13.3 (17C54)",
|
|
// "interface" : "usb",
|
|
// "available" : false,
|
|
// "platform" : "com.apple.platform.iphoneos",
|
|
// "modelCode" : "iPhone8,1",
|
|
// "identifier" : "d83d5bc53967baa0ee18626ba87b6254b2ab5418",
|
|
// "architecture" : "arm64",
|
|
// "modelName" : "iPhone 6s",
|
|
// "name" : "iPhone",
|
|
// "error" : {
|
|
// "code" : -9,
|
|
// "failureReason" : "",
|
|
// "description" : "iPhone is not paired with your computer.",
|
|
// "domain" : "com.apple.platform.iphoneos"
|
|
// }
|
|
// }
|
|
// ...
|
|
|
|
if (errorProperties == null) {
|
|
return null;
|
|
}
|
|
|
|
final StringBuffer errorMessage = StringBuffer('Error: ');
|
|
|
|
final Object? description = errorProperties['description'];
|
|
if (description is String) {
|
|
errorMessage.write(description);
|
|
if (!description.endsWith('.')) {
|
|
errorMessage.write('.');
|
|
}
|
|
} else {
|
|
errorMessage.write('Xcode pairing error.');
|
|
}
|
|
|
|
final Object? recoverySuggestion = errorProperties['recoverySuggestion'];
|
|
if (recoverySuggestion is String) {
|
|
errorMessage.write(' $recoverySuggestion');
|
|
}
|
|
|
|
final int? code = _errorCode(errorProperties);
|
|
if (code != null) {
|
|
errorMessage.write(' (code $code)');
|
|
}
|
|
|
|
return errorMessage.toString();
|
|
}
|
|
|
|
/// List of all devices reporting errors.
|
|
Future<List<String>> getDiagnostics() async {
|
|
final List<Object>? allAvailableDevices = await _getAllDevices(
|
|
useCache: true,
|
|
timeout: const Duration(seconds: 2)
|
|
);
|
|
|
|
if (allAvailableDevices == null) {
|
|
return const <String>[];
|
|
}
|
|
|
|
final List<String> diagnostics = <String>[];
|
|
for (final Object deviceProperties in allAvailableDevices) {
|
|
if (deviceProperties is! Map<String, Object?>) {
|
|
continue;
|
|
}
|
|
final Map<String, Object?>? errorProperties = _errorProperties(deviceProperties);
|
|
final String? errorMessage = _parseErrorMessage(errorProperties);
|
|
if (errorMessage != null) {
|
|
final int? code = _errorCode(errorProperties);
|
|
// Error -13: iPhone is not connected. Xcode will continue when iPhone is connected.
|
|
// This error is confusing since the device is not connected and maybe has not been connected
|
|
// for a long time. Avoid showing it.
|
|
if (code == -13 && errorMessage.contains('not connected')) {
|
|
continue;
|
|
}
|
|
|
|
diagnostics.add(errorMessage);
|
|
}
|
|
}
|
|
return diagnostics;
|
|
}
|
|
}
|