// 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. // @dart = 2.8 import 'dart:async'; import 'package:meta/meta.dart'; import 'package:process/process.dart'; import '../artifacts.dart'; import '../base/common.dart'; import '../base/io.dart'; import '../base/logger.dart'; import '../base/platform.dart'; import '../base/process.dart'; import '../build_info.dart'; import '../cache.dart'; import '../convert.dart'; import '../globals.dart' as globals; import '../ios/devices.dart'; import '../ios/ios_deploy.dart'; import '../ios/iproxy.dart'; import '../ios/mac.dart'; import '../reporting/reporting.dart'; import 'xcode.dart'; enum XCDeviceEvent { attach, detach, } /// 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, }) : _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, ), _iProxy = iproxy, _xcode = xcode { _setupDeviceIdentifierByEventStream(); } void dispose() { _deviceObservationProcess?.kill(); } final ProcessUtils _processUtils; final Logger _logger; final IMobileDevice _iMobileDevice; final IOSDeploy _iosDeploy; final Xcode _xcode; final IProxy _iProxy; List<dynamic> _cachedListResults; Process _deviceObservationProcess; StreamController<Map<XCDeviceEvent, String>> _deviceIdentifierByEvent; void _setupDeviceIdentifierByEventStream() { // _deviceIdentifierByEvent Should always be available for listeners // in case polling needs to be stopped and restarted. _deviceIdentifierByEvent = StreamController<Map<XCDeviceEvent, String>>.broadcast( onListen: _startObservingTetheredIOSDevices, onCancel: _stopObservingTetheredIOSDevices, ); } bool get isInstalled => _xcode.isInstalledAndMeetsVersionCheck; Future<List<dynamic>> _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 List<dynamic> listResults = json.decode(result.stdout) as List<dynamic>; _cachedListResults = listResults; return listResults; } _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 is a tuple of one event type /// and identifier. Stream<Map<XCDeviceEvent, String>> observedDeviceEvents() { if (!isInstalled) { _logger.printTrace("Xcode not found. Run 'flutter doctor' for more information."); return null; } return _deviceIdentifierByEvent.stream; } // Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418 // Attach: 00008027-00192736010F802E // Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418 final RegExp _observationIdentifierPattern = RegExp(r'^(\w*): ([\w-]*)$'); Future<void> _startObservingTetheredIOSDevices() async { try { if (_deviceObservationProcess != null) { throw Exception('xcdevice observe restart failed'); } // Run in interactive mode (via script) to convince // xcdevice it has a terminal attached in order to redirect stdout. _deviceObservationProcess = await _processUtils.start( <String>[ 'script', '-t', '0', '/dev/null', ..._xcode.xcrunCommand(), 'xcdevice', 'observe', '--both', ], ); final StreamSubscription<String> stdoutSubscription = _deviceObservationProcess.stdout .transform<String>(utf8.decoder) .transform<String>(const LineSplitter()) .listen((String line) { // 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')) { _deviceIdentifierByEvent.add(<XCDeviceEvent, String>{ XCDeviceEvent.attach: identifier }); } else if (verb.startsWith('detach')) { _deviceIdentifierByEvent.add(<XCDeviceEvent, String>{ XCDeviceEvent.detach: identifier }); } } }); final StreamSubscription<String> stderrSubscription = _deviceObservationProcess.stderr .transform<String>(utf8.decoder) .transform<String>(const LineSplitter()) .listen((String line) { _logger.printTrace('xcdevice observe error: $line'); }); unawaited(_deviceObservationProcess.exitCode.then((int status) { _logger.printTrace('xcdevice exited with code $exitCode'); unawaited(stdoutSubscription.cancel()); unawaited(stderrSubscription.cancel()); }).whenComplete(() async { if (_deviceIdentifierByEvent.hasListener) { // Tell listeners the process died. await _deviceIdentifierByEvent.close(); } _deviceObservationProcess = null; // Reopen it so new listeners can resume polling. _setupDeviceIdentifierByEventStream(); })); } on ProcessException catch (exception, stackTrace) { _deviceIdentifierByEvent.addError(exception, stackTrace); } on ArgumentError catch (exception, stackTrace) { _deviceIdentifierByEvent.addError(exception, stackTrace); } } void _stopObservingTetheredIOSDevices() { _deviceObservationProcess?.kill(); } /// [timeout] defaults to 2 seconds. Future<List<IOSDevice>> getAvailableIOSDevices({ Duration timeout }) async { final List<dynamic> allAvailableDevices = await _getAllDevices(timeout: timeout ?? const Duration(seconds: 2)); if (allAvailableDevices == null) { return const <IOSDevice>[]; } // [ // { // "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 List<IOSDevice> devices = <IOSDevice>[]; for (final dynamic device in allAvailableDevices) { if (device is! Map) { continue; } final Map<String, dynamic> deviceProperties = device as Map<String, dynamic>; // Only include iPhone, iPad, iPod, or other iOS devices. if (!_isIPhoneOSDevice(deviceProperties)) { continue; } final Map<String, dynamic> errorProperties = _errorProperties(deviceProperties); if (errorProperties != null) { final String errorMessage = _parseErrorMessage(errorProperties); if (errorMessage.contains('not paired')) { UsageEvent('device', 'ios-trust-failure', flutterUsage: globals.flutterUsage).send(); } _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) { continue; } } final IOSDeviceInterface interface = _interfaceType(deviceProperties); // Only support USB devices, skip "network" interface (Xcode > Window > Devices and Simulators > Connect via network). // TODO(jmagman): Remove this check once wirelessly detected devices can be observed and attached, https://github.com/flutter/flutter/issues/15072. if (interface != IOSDeviceInterface.usb) { continue; } devices.add(IOSDevice( device['identifier'] as String, name: device['name'] as String, cpuArchitecture: _cpuArchitecture(deviceProperties), interfaceType: interface, sdkVersion: _sdkVersion(deviceProperties), iProxy: _iProxy, fileSystem: globals.fs, logger: _logger, iosDeploy: _iosDeploy, iMobileDevice: _iMobileDevice, platform: globals.platform, )); } return devices; } /// Despite the name, com.apple.platform.iphoneos includes iPhone, iPads, and all iOS devices. /// Excludes simulators. static bool _isIPhoneOSDevice(Map<String, dynamic> deviceProperties) { if (deviceProperties.containsKey('platform')) { final String platform = deviceProperties['platform'] as String; return platform == 'com.apple.platform.iphoneos'; } return false; } static Map<String, dynamic> _errorProperties(Map<String, dynamic> deviceProperties) { if (deviceProperties.containsKey('error')) { return deviceProperties['error'] as Map<String, dynamic>; } return null; } static int _errorCode(Map<String, dynamic> errorProperties) { if (errorProperties.containsKey('code') && errorProperties['code'] is int) { return errorProperties['code'] as int; } return null; } static IOSDeviceInterface _interfaceType(Map<String, dynamic> deviceProperties) { // Interface can be "usb", "network", or "none" for simulators // and unknown future interfaces. if (deviceProperties.containsKey('interface')) { if ((deviceProperties['interface'] as String).toLowerCase() == 'network') { return IOSDeviceInterface.network; } else { return IOSDeviceInterface.usb; } } return IOSDeviceInterface.none; } static String _sdkVersion(Map<String, dynamic> deviceProperties) { if (deviceProperties.containsKey('operatingSystemVersion')) { // Parse out the OS version, ignore the build number in parentheses. // "13.3 (17C54)" final RegExp operatingSystemRegex = RegExp(r'(.*) \(.*\)$'); final String operatingSystemVersion = deviceProperties['operatingSystemVersion'] as String; return operatingSystemRegex.firstMatch(operatingSystemVersion.trim())?.group(1); } return null; } DarwinArch _cpuArchitecture(Map<String, dynamic> deviceProperties) { DarwinArch cpuArchitecture; if (deviceProperties.containsKey('architecture')) { final String architecture = deviceProperties['architecture'] as 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.printError( 'Unknown architecture $architecture, defaulting to ' '${getNameForDarwinArch(cpuArchitecture)}', ); } } return cpuArchitecture; } /// Error message parsed from xcdevice. null if no error. static String _parseErrorMessage(Map<String, dynamic> 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: '); if (errorProperties.containsKey('description')) { final String description = errorProperties['description'] as String; errorMessage.write(description); if (!description.endsWith('.')) { errorMessage.write('.'); } } else { errorMessage.write('Xcode pairing error.'); } if (errorProperties.containsKey('recoverySuggestion')) { final String recoverySuggestion = errorProperties['recoverySuggestion'] as 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<dynamic> allAvailableDevices = await _getAllDevices( useCache: true, timeout: const Duration(seconds: 2) ); if (allAvailableDevices == null) { return const <String>[]; } final List<String> diagnostics = <String>[]; for (final dynamic device in allAvailableDevices) { if (device is! Map) { continue; } final Map<String, dynamic> deviceProperties = device as Map<String, dynamic>; final Map<String, dynamic> errorProperties = _errorProperties(deviceProperties); final String errorMessage = _parseErrorMessage(errorProperties); if (errorMessage != null) { diagnostics.add(errorMessage); } } return diagnostics; } }