xcode.dart 23.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

7 8
import 'package:meta/meta.dart';
import 'package:process/process.dart';
9
import 'package:vm_service/vm_service_io.dart' as vm_service_io;
10

11
import '../artifacts.dart';
12
import '../base/common.dart';
13
import '../base/file_system.dart';
14
import '../base/io.dart';
15
import '../base/logger.dart';
16
import '../base/platform.dart';
17
import '../base/process.dart';
18
import '../build_info.dart';
19
import '../cache.dart';
20
import '../convert.dart';
21
import '../globals.dart' as globals;
22
import '../ios/devices.dart';
23
import '../ios/ios_deploy.dart';
24
import '../ios/iproxy.dart';
25
import '../ios/mac.dart';
26
import '../ios/xcodeproj.dart';
27
import '../reporting/reporting.dart';
28

29 30
const int kXcodeRequiredVersionMajor = 11;
const int kXcodeRequiredVersionMinor = 0;
31
const int kXcodeRequiredVersionPatch = 0;
32

33 34 35 36 37 38 39 40 41 42 43
enum SdkType {
  iPhone,
  iPhoneSimulator,
  macOS,
}

/// SDK name passed to `xcrun --sdk`. Corresponds to undocumented Xcode
/// SUPPORTED_PLATFORMS values.
///
/// Usage: xcrun [options] <tool name> ... arguments ...
/// ...
44
/// --sdk <sdk name>            find the tool for the given SDK name.
45 46 47 48 49 50 51 52 53 54 55 56 57
String getNameForSdk(SdkType sdk) {
  switch (sdk) {
    case SdkType.iPhone:
      return 'iphoneos';
    case SdkType.iPhoneSimulator:
      return 'iphonesimulator';
    case SdkType.macOS:
      return 'macosx';
  }
  assert(false);
  return null;
}

58
/// A utility class for interacting with Xcode command line tools.
59
class Xcode {
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
  Xcode({
    @required Platform platform,
    @required ProcessManager processManager,
    @required Logger logger,
    @required FileSystem fileSystem,
    @required XcodeProjectInterpreter xcodeProjectInterpreter,
  }) : _platform = platform,
       _fileSystem = fileSystem,
       _xcodeProjectInterpreter = xcodeProjectInterpreter,
       _processUtils = ProcessUtils(logger: logger, processManager: processManager);

  final Platform _platform;
  final ProcessUtils _processUtils;
  final FileSystem _fileSystem;
  final XcodeProjectInterpreter _xcodeProjectInterpreter;

  bool get isInstalledAndMeetsVersionCheck => _platform.isMacOS && isInstalled && isVersionSatisfactory;
77 78 79 80 81

  String _xcodeSelectPath;
  String get xcodeSelectPath {
    if (_xcodeSelectPath == null) {
      try {
82
        _xcodeSelectPath = _processUtils.runSync(
83 84
          <String>['/usr/bin/xcode-select', '--print-path'],
        ).stdout.trim();
85 86
      } on ProcessException {
        // Ignored, return null below.
87 88
      } on ArgumentError {
        // Ignored, return null below.
89 90 91 92 93 94
      }
    }
    return _xcodeSelectPath;
  }

  bool get isInstalled {
95
    if (xcodeSelectPath == null || xcodeSelectPath.isEmpty) {
96
      return false;
97
    }
98
    return _xcodeProjectInterpreter.isInstalled;
99 100
  }

101 102
  int get majorVersion => _xcodeProjectInterpreter.majorVersion;
  int get minorVersion => _xcodeProjectInterpreter.minorVersion;
103
  int get patchVersion => _xcodeProjectInterpreter.patchVersion;
104

105
  String get versionText => _xcodeProjectInterpreter.versionText;
106 107 108 109 110 111

  bool _eulaSigned;
  /// Has the EULA been signed?
  bool get eulaSigned {
    if (_eulaSigned == null) {
      try {
112
        final RunResult result = _processUtils.runSync(
113 114 115
          <String>['/usr/bin/xcrun', 'clang'],
        );
        if (result.stdout != null && result.stdout.contains('license')) {
116
          _eulaSigned = false;
117
        } else if (result.stderr != null && result.stderr.contains('license')) {
118
          _eulaSigned = false;
119
        } else {
120
          _eulaSigned = true;
121
        }
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
      } on ProcessException {
        _eulaSigned = false;
      }
    }
    return _eulaSigned;
  }

  bool _isSimctlInstalled;

  /// Verifies that simctl is installed by trying to run it.
  bool get isSimctlInstalled {
    if (_isSimctlInstalled == null) {
      try {
        // This command will error if additional components need to be installed in
        // xcode 9.2 and above.
137
        final RunResult result = _processUtils.runSync(
138 139
          <String>['/usr/bin/xcrun', 'simctl', 'list'],
        );
140 141 142 143 144 145 146 147 148
        _isSimctlInstalled = result.stderr == null || result.stderr == '';
      } on ProcessException {
        _isSimctlInstalled = false;
      }
    }
    return _isSimctlInstalled;
  }

  bool get isVersionSatisfactory {
149
    if (!_xcodeProjectInterpreter.isInstalled) {
150
      return false;
151 152
    }
    if (majorVersion > kXcodeRequiredVersionMajor) {
153
      return true;
154 155
    }
    if (majorVersion == kXcodeRequiredVersionMajor) {
156 157 158
      if (minorVersion == kXcodeRequiredVersionMinor) {
        return patchVersion >= kXcodeRequiredVersionPatch;
      }
159
      return minorVersion >= kXcodeRequiredVersionMinor;
160
    }
161 162 163 164
    return false;
  }

  Future<RunResult> cc(List<String> args) {
165
    return _processUtils.run(
166 167 168
      <String>['xcrun', 'cc', ...args],
      throwOnError: true,
    );
169 170 171
  }

  Future<RunResult> clang(List<String> args) {
172
    return _processUtils.run(
173 174 175
      <String>['xcrun', 'clang', ...args],
      throwOnError: true,
    );
176 177
  }

178
  Future<String> sdkLocation(SdkType sdk) async {
179
    assert(sdk != null);
180
    final RunResult runResult = await _processUtils.run(
181
      <String>['xcrun', '--sdk', getNameForSdk(sdk), '--show-sdk-path'],
182 183
    );
    if (runResult.exitCode != 0) {
184
      throwToolExit('Could not find SDK location: ${runResult.stderr}');
185 186
    }
    return runResult.stdout.trim();
187 188
  }

189
  String getSimulatorPath() {
190
    if (xcodeSelectPath == null) {
191
      return null;
192
    }
193
    final List<String> searchPaths = <String>[
194
      _fileSystem.path.join(xcodeSelectPath, 'Applications', 'Simulator.app'),
195 196
    ];
    return searchPaths.where((String p) => p != null).firstWhere(
197
      (String p) => _fileSystem.directory(p).existsSync(),
198 199 200 201
      orElse: () => null,
    );
  }
}
202

203 204 205 206 207
enum XCDeviceEvent {
  attach,
  detach,
}

208 209 210
/// A utility class for interacting with Xcode xcdevice command line tools.
class XCDevice {
  XCDevice({
211 212
    @required Artifacts artifacts,
    @required Cache cache,
213 214 215
    @required ProcessManager processManager,
    @required Logger logger,
    @required Xcode xcode,
216
    @required Platform platform,
217
    @required IProxy iproxy,
218
  }) : _processUtils = ProcessUtils(logger: logger, processManager: processManager),
219
      _logger = logger,
220 221 222 223 224 225 226 227 228 229 230 231 232
      _iMobileDevice = IMobileDevice(
        artifacts: artifacts,
        cache: cache,
        logger: logger,
        processManager: processManager,
      ),
      _iosDeploy = IOSDeploy(
        artifacts: artifacts,
        cache: cache,
        logger: logger,
        platform: platform,
        processManager: processManager,
      ),
233
      _iProxy = iproxy,
234 235 236 237 238 239 240 241
      _xcode = xcode {

    _setupDeviceIdentifierByEventStream();
  }

  void dispose() {
    _deviceObservationProcess?.kill();
  }
242 243 244

  final ProcessUtils _processUtils;
  final Logger _logger;
245
  final IMobileDevice _iMobileDevice;
246
  final IOSDeploy _iosDeploy;
247
  final Xcode _xcode;
248
  final IProxy _iProxy;
249

250 251 252 253 254 255 256 257 258 259 260 261 262
  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,
    );
  }

263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
  bool get isInstalled => _xcode.isInstalledAndMeetsVersionCheck && xcdevicePath != null;

  String _xcdevicePath;
  String get xcdevicePath {
    if (_xcdevicePath == null) {
      try {
        _xcdevicePath = _processUtils.runSync(
          <String>[
            'xcrun',
            '--find',
            'xcdevice'
          ],
          throwOnError: true,
        ).stdout.trim();
      } on ProcessException catch (exception) {
        _logger.printTrace('Process exception finding xcdevice:\n$exception');
      } on ArgumentError catch (exception) {
        _logger.printTrace('Argument exception finding xcdevice:\n$exception');
      }
    }
    return _xcdevicePath;
  }

286 287 288 289
  Future<List<dynamic>> _getAllDevices({
    bool useCache = false,
    @required Duration timeout
  }) async {
290
    if (!isInstalled) {
291
      _logger.printTrace("Xcode not found. Run 'flutter doctor' for more information.");
292 293 294 295 296 297 298 299 300 301 302 303 304
      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>[
          'xcrun',
          'xcdevice',
          'list',
          '--timeout',
305
          timeout.inSeconds.toString(),
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
        ],
        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;
  }

324 325 326 327 328 329 330 331 332 333 334 335 336
  /// 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
337
  // Attach: 00008027-00192736010F802E
338
  // Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
339
  final RegExp _observationIdentifierPattern = RegExp(r'^(\w*): ([\w-]*)$');
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370

  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',
          'xcrun',
          '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
371
        // Attach: 00008027-00192736010F802E
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
        // 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();
  }
419

420
  /// [timeout] defaults to 2 seconds.
421
  Future<List<IOSDevice>> getAvailableIOSDevices({ Duration timeout }) async {
422
    final List<dynamic> allAvailableDevices = await _getAllDevices(timeout: timeout ?? const Duration(seconds: 2));
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480

    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')) {
481
          UsageEvent('device', 'ios-trust-failure', flutterUsage: globals.flutterUsage).send();
482 483 484 485 486 487 488 489 490 491 492 493 494
        }
        _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;
        }
      }

495 496
      final IOSDeviceInterface interface = _interfaceType(deviceProperties);

497
      // Only support USB devices, skip "network" interface (Xcode > Window > Devices and Simulators > Connect via network).
498 499
      // 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) {
500 501 502 503 504 505 506
        continue;
      }

      devices.add(IOSDevice(
        device['identifier'] as String,
        name: device['name'] as String,
        cpuArchitecture: _cpuArchitecture(deviceProperties),
507
        interfaceType: interface,
508
        sdkVersion: _sdkVersion(deviceProperties),
509
        iProxy: _iProxy,
510
        fileSystem: globals.fs,
511
        logger: _logger,
512
        iosDeploy: _iosDeploy,
513
        iMobileDevice: _iMobileDevice,
514
        platform: globals.platform,
515
        vmServiceConnectUri: vm_service_io.vmServiceConnectUri,
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544
      ));
    }
    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;
  }

545 546 547 548 549 550 551 552 553 554 555 556
  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;
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575
  }

  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);
576 577 578
      } on Exception {
        // Fallback to default iOS architecture. Future-proof against a
        // theoretical version of Xcode that changes this string to something
579 580 581 582 583 584 585
        // slightly different like "ARM64", or armv7 variations like
        // armv7s and armv7f.
        if (architecture.startsWith('armv7')) {
          cpuArchitecture = DarwinArch.armv7;
        } else {
          cpuArchitecture = defaultIOSArchs.first;
        }
586 587 588 589
        _logger.printError(
          'Unknown architecture $architecture, defaulting to '
          '${getNameForDarwinArch(cpuArchitecture)}',
        );
590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675
      }
    }
    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 {
676 677
    final List<dynamic> allAvailableDevices = await _getAllDevices(
      useCache: true,
678
      timeout: const Duration(seconds: 2)
679
    );
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699

    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;
  }
}