simulators.dart 26.3 KB
Newer Older
1 2 3 4 5
// Copyright 2016 The Chromium 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';
6
import 'dart:convert';
7 8 9 10 11
import 'dart:io';

import 'package:path/path.dart' as path;

import '../application_package.dart';
12
import '../base/context.dart';
13
import '../base/process.dart';
14
import '../build_info.dart';
15
import '../device.dart';
16
import '../flx.dart' as flx;
17
import '../globals.dart';
18
import '../observatory.dart';
19
import '../protocol_discovery.dart';
20 21 22 23
import 'mac.dart';

const String _xcrunPath = '/usr/bin/xcrun';

24
/// Test device created by Flutter when no other device is available.
25
const String _kFlutterTestDeviceSuffix = '(Flutter)';
26

27 28 29
class IOSSimulators extends PollingDeviceDiscovery {
  IOSSimulators() : super('IOSSimulators');

30
  @override
31
  bool get supportsPlatform => Platform.isMacOS;
32 33

  @override
34 35 36 37 38
  List<Device> pollingGetDevices() => IOSSimulatorUtils.instance.getAttachedDevices();
}

class IOSSimulatorUtils {
  /// Returns [IOSSimulatorUtils] active in the current app context (i.e. zone).
39 40 41
  static IOSSimulatorUtils get instance {
    return context[IOSSimulatorUtils] ?? (context[IOSSimulatorUtils] = new IOSSimulatorUtils());
  }
42 43

  List<IOSSimulator> getAttachedDevices() {
44
    if (!XCode.instance.isInstalledAndMeetsVersionCheck)
45 46 47 48 49 50
      return <IOSSimulator>[];

    return SimControl.instance.getConnectedDevices().map((SimDevice device) {
      return new IOSSimulator(device.udid, name: device.name);
    }).toList();
  }
51 52 53 54
}

/// A wrapper around the `simctl` command line tool.
class SimControl {
55 56 57
  /// Returns [SimControl] active in the current app context (i.e. zone).
  static SimControl get instance => context[SimControl] ?? (context[SimControl] = new SimControl());

58
  Future<bool> boot({ String deviceName }) async {
59 60 61
    if (_isAnyConnected())
      return true;

62 63 64 65 66 67 68
    if (deviceName == null) {
      SimDevice testDevice = _createTestDevice();
      if (testDevice == null) {
        return false;
      }
      deviceName = testDevice.name;
    }
69 70

    // `xcrun instruments` requires a template (-t). @yjbanov has no idea what
71 72 73
    // "template" is but the built-in 'Blank' seems to work. -l causes xcrun to
    // quit after a time limit without killing the simulator. We quit after
    // 1 second.
74
    List<String> args = <String>[_xcrunPath, 'instruments', '-w', deviceName, '-t', 'Blank', '-l', '1'];
75 76 77 78 79 80 81
    printTrace(args.join(' '));
    runDetached(args);
    printStatus('Waiting for iOS Simulator to boot...');

    bool connected = false;
    int attempted = 0;
    while (!connected && attempted < 20) {
82
      connected = _isAnyConnected();
83 84
      if (!connected) {
        printStatus('Still waiting for iOS Simulator to boot...');
Ian Hickson's avatar
Ian Hickson committed
85
        await new Future<Null>.delayed(new Duration(seconds: 1));
86
      }
87
      attempted++;
88 89
    }

90 91 92 93 94 95 96
    if (connected) {
      printStatus('Connected to iOS Simulator.');
      return true;
    } else {
      printStatus('Timed out waiting for iOS Simulator to boot.');
      return false;
    }
97 98
  }

99
  SimDevice _createTestDevice() {
100
    SimDeviceType deviceType = _findSuitableDeviceType();
101
    if (deviceType == null)
102 103 104
      return null;

    String runtime = _findSuitableRuntime();
105
    if (runtime == null)
106 107 108 109
      return null;

    // Delete any old test devices
    getDevices()
110
      .where((SimDevice d) => d.name.endsWith(_kFlutterTestDeviceSuffix))
111 112 113
      .forEach(_deleteDevice);

    // Create new device
114
    String deviceName = '${deviceType.name} $_kFlutterTestDeviceSuffix';
115
    List<String> args = <String>[_xcrunPath, 'simctl', 'create', deviceName, deviceType.identifier, runtime];
116 117 118
    printTrace(args.join(' '));
    runCheckedSync(args);

119
    return getDevices().firstWhere((SimDevice d) => d.name == deviceName);
120 121
  }

122
  SimDeviceType _findSuitableDeviceType() {
123 124
    List<Map<String, dynamic>> allTypes = _list(SimControlListSection.devicetypes);
    List<Map<String, dynamic>> usableTypes = allTypes
Ian Hickson's avatar
Ian Hickson committed
125
      .where((Map<String, dynamic> info) => info['name'].startsWith('iPhone'))
126
      .toList()
Ian Hickson's avatar
Ian Hickson committed
127
      ..sort((Map<String, dynamic> r1, Map<String, dynamic> r2) => -compareIphoneVersions(r1['identifier'], r2['identifier']));
128 129 130

    if (usableTypes.isEmpty) {
      printError(
131 132
        'No suitable device type found.\n'
        'You may launch an iOS Simulator manually and Flutter will attempt to use it.'
133 134 135
      );
    }

136 137 138 139
    return new SimDeviceType(
      usableTypes.first['name'],
      usableTypes.first['identifier']
    );
140 141 142 143 144
  }

  String _findSuitableRuntime() {
    List<Map<String, dynamic>> allRuntimes = _list(SimControlListSection.runtimes);
    List<Map<String, dynamic>> usableRuntimes = allRuntimes
Ian Hickson's avatar
Ian Hickson committed
145
      .where((Map<String, dynamic> info) => info['name'].startsWith('iOS'))
146
      .toList()
Ian Hickson's avatar
Ian Hickson committed
147
      ..sort((Map<String, dynamic> r1, Map<String, dynamic> r2) => -compareIosVersions(r1['version'], r2['version']));
148 149 150

    if (usableRuntimes.isEmpty) {
      printError(
151 152
        'No suitable iOS runtime found.\n'
        'You may launch an iOS Simulator manually and Flutter will attempt to use it.'
153 154 155 156 157 158 159 160
      );
    }

    return usableRuntimes.first['identifier'];
  }

  void _deleteDevice(SimDevice device) {
    try {
161
      List<String> args = <String>[_xcrunPath, 'simctl', 'delete', device.name];
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
      printTrace(args.join(' '));
      runCheckedSync(args);
    } catch(e) {
      printError(e);
    }
  }

  /// Runs `simctl list --json` and returns the JSON of the corresponding
  /// [section].
  ///
  /// The return type depends on the [section] being listed but is usually
  /// either a [Map] or a [List].
  dynamic _list(SimControlListSection section) {
    // Sample output from `simctl list --json`:
    //
177
    // {
178 179
    //   "devicetypes": { ... },
    //   "runtimes": { ... },
180 181 182 183 184 185 186 187 188
    //   "devices" : {
    //     "com.apple.CoreSimulator.SimRuntime.iOS-8-2" : [
    //       {
    //         "state" : "Shutdown",
    //         "availability" : " (unavailable, runtime profile not found)",
    //         "name" : "iPhone 4s",
    //         "udid" : "1913014C-6DCB-485D-AC6B-7CD76D322F5B"
    //       },
    //       ...
189 190
    //   },
    //   "pairs": { ... },
191

192
    List<String> args = <String>['simctl', 'list', '--json', section.name];
193 194 195 196
    printTrace('$_xcrunPath ${args.join(' ')}');
    ProcessResult results = Process.runSync(_xcrunPath, args);
    if (results.exitCode != 0) {
      printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
197
      return <String, Map<String, dynamic>>{};
198 199
    }

200 201 202 203 204
    return JSON.decode(results.stdout)[section.name];
  }

  /// Returns a list of all available devices, both potential and connected.
  List<SimDevice> getDevices() {
205 206
    List<SimDevice> devices = <SimDevice>[];

207
    Map<String, dynamic> devicesSection = _list(SimControlListSection.devices);
208 209

    for (String deviceCategory in devicesSection.keys) {
Ian Hickson's avatar
Ian Hickson committed
210
      List<Map<String, String>> devicesData = devicesSection[deviceCategory];
211 212 213 214 215 216 217 218 219 220

      for (Map<String, String> data in devicesData) {
        devices.add(new SimDevice(deviceCategory, data));
      }
    }

    return devices;
  }

  /// Returns all the connected simulator devices.
221
  List<SimDevice> getConnectedDevices() {
222 223 224
    return getDevices().where((SimDevice device) => device.isBooted).toList();
  }

225
  StreamController<List<SimDevice>> _trackDevicesControler;
226 227 228 229 230 231

  /// Listens to changes in the set of connected devices. The implementation
  /// currently uses polling. Callers should be careful to call cancel() on any
  /// stream subscription when finished.
  ///
  /// TODO(devoncarew): We could investigate using the usbmuxd protocol directly.
232
  Stream<List<SimDevice>> trackDevices() {
233 234 235
    if (_trackDevicesControler == null) {
      Timer timer;
      Set<String> deviceIds = new Set<String>();
Ian Hickson's avatar
Ian Hickson committed
236
      _trackDevicesControler = new StreamController<List<SimDevice>>.broadcast(
237 238 239
        onListen: () {
          timer = new Timer.periodic(new Duration(seconds: 4), (Timer timer) {
            List<SimDevice> devices = getConnectedDevices();
Ian Hickson's avatar
Ian Hickson committed
240
            if (_updateDeviceIds(devices, deviceIds))
241 242 243 244 245 246 247 248 249 250 251 252 253
              _trackDevicesControler.add(devices);
          });
        }, onCancel: () {
          timer?.cancel();
          deviceIds.clear();
        }
      );
    }

    return _trackDevicesControler.stream;
  }

  /// Update the cached set of device IDs and return whether there were any changes.
254
  bool _updateDeviceIds(List<SimDevice> devices, Set<String> deviceIds) {
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
    Set<String> newIds = new Set<String>.from(devices.map((SimDevice device) => device.udid));

    bool changed = false;

    for (String id in newIds) {
      if (!deviceIds.contains(id))
        changed = true;
    }

    for (String id in deviceIds) {
      if (!newIds.contains(id))
        changed = true;
    }

    deviceIds.clear();
    deviceIds.addAll(newIds);

    return changed;
  }

275
  bool _isAnyConnected() => getConnectedDevices().isNotEmpty;
276

277 278 279 280 281 282 283 284 285 286
  bool isInstalled(String appId) {
    return exitsHappy(<String>[
      _xcrunPath,
      'simctl',
      'get_app_container',
      'booted',
      appId,
    ]);
  }

287
  void install(String deviceId, String appPath) {
288
    runCheckedSync(<String>[_xcrunPath, 'simctl', 'install', deviceId, appPath]);
289 290
  }

291 292 293 294
  void uninstall(String deviceId, String appId) {
    runCheckedSync(<String>[_xcrunPath, 'simctl', 'uninstall', deviceId, appId]);
  }

295
  void launch(String deviceId, String appIdentifier, [List<String> launchArgs]) {
296
    List<String> args = <String>[_xcrunPath, 'simctl', 'launch', deviceId, appIdentifier];
297 298 299 300 301 302
    if (launchArgs != null)
      args.addAll(launchArgs);
    runCheckedSync(args);
  }
}

303 304
/// Enumerates all data sections of `xcrun simctl list --json` command.
class SimControlListSection {
Ian Hickson's avatar
Ian Hickson committed
305
  const SimControlListSection._(this.name);
306 307

  final String name;
Ian Hickson's avatar
Ian Hickson committed
308 309 310 311 312

  static const SimControlListSection devices = const SimControlListSection._('devices');
  static const SimControlListSection devicetypes = const SimControlListSection._('devicetypes');
  static const SimControlListSection runtimes = const SimControlListSection._('runtimes');
  static const SimControlListSection pairs = const SimControlListSection._('pairs');
313 314
}

315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
/// A simulated device type.
///
/// Simulated device types can be listed using the command
/// `xcrun simctl list devicetypes`.
class SimDeviceType {
  SimDeviceType(this.name, this.identifier);

  /// The name of the device type.
  ///
  /// Examples:
  ///
  ///     "iPhone 6s"
  ///     "iPhone 6 Plus"
  final String name;

  /// The identifier of the device type.
  ///
  /// Examples:
  ///
  ///     "com.apple.CoreSimulator.SimDeviceType.iPhone-6s"
  ///     "com.apple.CoreSimulator.SimDeviceType.iPhone-6-Plus"
  final String identifier;
}

339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
class SimDevice {
  SimDevice(this.category, this.data);

  final String category;
  final Map<String, String> data;

  String get state => data['state'];
  String get availability => data['availability'];
  String get name => data['name'];
  String get udid => data['udid'];

  bool get isBooted => state == 'Booted';
}

class IOSSimulator extends Device {
  IOSSimulator(String id, { this.name }) : super(id);

356
  @override
357 358
  final String name;

359
  @override
360 361
  bool get isLocalEmulator => true;

362
  _IOSSimulatorLogReader _logReader;
363
  _IOSSimulatorDevicePortForwarder _portForwarder;
364

365 366 367 368 369 370 371 372 373 374 375 376 377
  String get xcrunPath => path.join('/usr', 'bin', 'xcrun');

  String _getSimulatorPath() {
    return path.join(homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', id);
  }

  String _getSimulatorAppHomeDirectory(ApplicationPackage app) {
    String simulatorPath = _getSimulatorPath();
    if (simulatorPath == null)
      return null;
    return path.join(simulatorPath, 'data');
  }

378 379 380 381 382
  @override
  bool isAppInstalled(ApplicationPackage app) {
    return SimControl.instance.isInstalled(app.id);
  }

383 384 385
  @override
  bool installApp(ApplicationPackage app) {
    try {
386 387
      IOSApp iosApp = app;
      SimControl.instance.install(id, iosApp.simulatorBundlePath);
388 389 390 391 392 393
      return true;
    } catch (e) {
      return false;
    }
  }

394 395 396 397 398 399 400 401 402 403
  @override
  bool uninstallApp(ApplicationPackage app) {
    try {
      SimControl.instance.uninstall(id, app.id);
      return true;
    } catch (e) {
      return false;
    }
  }

404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
  @override
  bool isSupported() {
    if (!Platform.isMacOS) {
      _supportMessage = "Not supported on a non Mac host";
      return false;
    }

    // Step 1: Check if the device is part of a blacklisted category.
    //         We do not support WatchOS or tvOS devices.

    RegExp blacklist = new RegExp(r'Apple (TV|Watch)', caseSensitive: false);

    if (blacklist.hasMatch(name)) {
      _supportMessage = "Flutter does not support either the Apple TV or Watch. Choose an iPhone 5s or above.";
      return false;
    }

    // Step 2: Check if the device must be rejected because of its version.
    //         There is an artitifical check on older simulators where arm64
    //         targetted applications cannot be run (even though the
    //         Flutter runner on the simulator is completely different).

    RegExp versionExp = new RegExp(r'iPhone ([0-9])+');
    Match match = versionExp.firstMatch(name);

429 430
    // Not an iPhone. All available non-iPhone simulators are compatible.
    if (match == null)
431 432
      return true;

433 434
    // iPhones 6 and above are always fine.
    if (int.parse(match.group(1)) > 5)
435 436 437
      return true;

    // The 's' subtype of 5 is compatible.
438
    if (name.contains('iPhone 5s'))
439 440 441 442 443 444 445 446 447 448
      return true;

    _supportMessage = "The simulator version is too old. Choose an iPhone 5s or above.";
    return false;
  }

  String _supportMessage;

  @override
  String supportMessage() {
449
    if (isSupported())
450 451 452 453 454 455
      return "Supported";

    return _supportMessage != null ? _supportMessage : "Unknown";
  }

  @override
Devon Carew's avatar
Devon Carew committed
456
  Future<LaunchResult> startApp(
457 458
    ApplicationPackage app,
    BuildMode mode, {
459 460
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
461
    DebuggingOptions debuggingOptions,
462 463 464 465
    Map<String, dynamic> platformArgs
  }) async {
    printTrace('Building ${app.name} for $id.');

466
    if (!(await _setupUpdatedApplicationBundle(app)))
Devon Carew's avatar
Devon Carew committed
467
      return new LaunchResult.failed();
468

469
    ProtocolDiscovery observatoryDiscovery;
470

471 472
    if (debuggingOptions.debuggingEnabled)
      observatoryDiscovery = new ProtocolDiscovery(logReader, ProtocolDiscovery.kObservatoryService);
473

474 475 476 477
    // Prepare launch arguments.
    List<String> args = <String>[
      "--flx=${path.absolute(path.join('build', 'app.flx'))}",
      "--dart-main=${path.absolute(mainPath)}",
478
      "--packages=${path.absolute('.packages')}",
479
    ];
480

Devon Carew's avatar
Devon Carew committed
481
    if (debuggingOptions.debuggingEnabled) {
482
      if (debuggingOptions.buildMode == BuildMode.debug)
Devon Carew's avatar
Devon Carew committed
483 484 485
        args.add("--enable-checked-mode");
      if (debuggingOptions.startPaused)
        args.add("--start-paused");
486

Devon Carew's avatar
Devon Carew committed
487
      int observatoryPort = await debuggingOptions.findBestObservatoryPort();
488
      args.add("--observatory-port=$observatoryPort");
Devon Carew's avatar
Devon Carew committed
489
    }
490

491
    // Launch the updated application in the simulator.
492
    try {
493
      SimControl.instance.launch(id, app.id, args);
494 495
    } catch (error) {
      printError('$error');
Devon Carew's avatar
Devon Carew committed
496
      return new LaunchResult.failed();
497 498
    }

Devon Carew's avatar
Devon Carew committed
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
    if (!debuggingOptions.debuggingEnabled) {
      return new LaunchResult.succeeded();
    } else {
      // Wait for the service protocol port here. This will complete once the
      // device has printed "Observatory is listening on..."
      printTrace('Waiting for observatory port to be available...');

      try {
        int devicePort = await observatoryDiscovery
          .nextPort()
          .timeout(new Duration(seconds: 20));
        printTrace('service protocol port = $devicePort');
        printStatus('Observatory listening on http://127.0.0.1:$devicePort');
        return new LaunchResult.succeeded(observatoryPort: devicePort);
      } catch (error) {
        if (error is TimeoutException)
          printError('Timed out while waiting for a debug connection.');
        else
          printError('Error waiting for a debug connection: $error');
        return new LaunchResult.failed();
      } finally {
        observatoryDiscovery.cancel();
      }
Devon Carew's avatar
Devon Carew committed
522
    }
523 524
  }

525
  bool _applicationIsInstalledAndRunning(ApplicationPackage app) {
526
    bool isInstalled = isAppInstalled(app);
527

528
    bool isRunning = exitsHappy(<String>[
529 530 531 532 533 534 535
      '/usr/bin/killall',
      'Runner',
    ]);

    return isInstalled && isRunning;
  }

536 537
  Future<bool> _setupUpdatedApplicationBundle(ApplicationPackage app) async {
    bool sideloadResult = await _sideloadUpdatedAssetsForInstalledApplicationBundle(app);
538 539 540 541 542

    if (!sideloadResult)
      return false;

    if (!_applicationIsInstalledAndRunning(app))
543
      return _buildAndInstallApplicationBundle(app);
544 545

    return true;
546 547 548 549
  }

  Future<bool> _buildAndInstallApplicationBundle(ApplicationPackage app) async {
    // Step 1: Build the Xcode project.
550 551
    // The build mode for the simulator is always debug.
    bool buildResult = await buildIOSXcodeProject(app, BuildMode.debug, buildForDevice: false);
552 553 554 555 556 557
    if (!buildResult) {
      printError('Could not build the application for the simulator.');
      return false;
    }

    // Step 2: Assert that the Xcode project was successfully built.
558 559
    IOSApp iosApp = app;
    Directory bundle = new Directory(iosApp.simulatorBundlePath);
560 561 562 563 564 565 566 567 568 569 570 571
    bool bundleExists = await bundle.exists();
    if (!bundleExists) {
      printError('Could not find the built application bundle at ${bundle.path}.');
      return false;
    }

    // Step 3: Install the updated bundle to the simulator.
    SimControl.instance.install(id, path.absolute(bundle.path));
    return true;
  }

  Future<bool> _sideloadUpdatedAssetsForInstalledApplicationBundle(
572 573
      ApplicationPackage app) async {
    return (await flx.build(precompiledSnapshot: true)) == 0;
574 575
  }

576 577 578 579 580 581 582 583 584 585 586 587 588 589 590
  @override
  Future<bool> restartApp(
    ApplicationPackage package,
    LaunchResult result, {
    String mainPath,
    Observatory observatory
  }) {
    return observatory.isolateReload(observatory.firstIsolateId).then((Response response) {
      return true;
    }).catchError((dynamic error) {
      printError('Error restarting app: $error');
      return false;
    });
  }

591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611
  @override
  Future<bool> stopApp(ApplicationPackage app) async {
    // Currently we don't have a way to stop an app running on iOS.
    return false;
  }

  Future<bool> pushFile(
      ApplicationPackage app, String localFile, String targetFile) async {
    if (Platform.isMacOS) {
      String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
      runCheckedSync(<String>['cp', localFile, path.join(simulatorHomeDirectory, targetFile)]);
      return true;
    }
    return false;
  }

  String get logFilePath {
    return path.join(homeDirectory, 'Library', 'Logs', 'CoreSimulator', id, 'system.log');
  }

  @override
612
  TargetPlatform get platform => TargetPlatform.ios;
613

614
  @override
615 616 617 618 619 620
  DeviceLogReader get logReader {
    if (_logReader == null)
      _logReader = new _IOSSimulatorLogReader(this);

    return _logReader;
  }
621

622
  @override
623 624 625 626 627 628 629
  DevicePortForwarder get portForwarder {
    if (_portForwarder == null)
      _portForwarder = new _IOSSimulatorDevicePortForwarder(this);

    return _portForwarder;
  }

630
  @override
631 632 633 634 635 636 637 638 639 640 641 642 643 644
  void clearLogs() {
    File logFile = new File(logFilePath);
    if (logFile.existsSync()) {
      RandomAccessFile randomFile = logFile.openSync(mode: FileMode.WRITE);
      randomFile.truncateSync(0);
      randomFile.closeSync();
    }
  }

  void ensureLogsExists() {
    File logFile = new File(logFilePath);
    if (!logFile.existsSync())
      logFile.writeAsBytesSync(<int>[]);
  }
Devon Carew's avatar
Devon Carew committed
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 676 677 678 679 680 681 682 683 684 685 686 687

  @override
  bool get supportsScreenshot => true;

  @override
  Future<bool> takeScreenshot(File outputFile) async {
    String homeDirPath = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
    Directory desktopDir = new Directory(path.join(homeDirPath, 'Desktop'));

    // 'Simulator Screen Shot Mar 25, 2016, 2.59.43 PM.png'

    Set<File> getScreenshots() {
      return new Set<File>.from(desktopDir.listSync().where((FileSystemEntity entity) {
        String name = path.basename(entity.path);
        return entity is File && name.startsWith('Simulator') && name.endsWith('.png');
      }));
    };

    Set<File> existingScreenshots = getScreenshots();

    runSync(<String>[
      'osascript',
      '-e',
      'activate application "Simulator"\n'
        'tell application "System Events" to keystroke "s" using command down'
    ]);

    // There is some latency here from the applescript call.
    await new Future<Null>.delayed(new Duration(seconds: 1));

    Set<File> shots = getScreenshots().difference(existingScreenshots);

    if (shots.isEmpty) {
      printError('Unable to locate the screenshot file.');
      return false;
    }

    File shot = shots.first;
    outputFile.writeAsBytesSync(shot.readAsBytesSync());
    shot.delete();

    return true;
  }
688 689 690
}

class _IOSSimulatorLogReader extends DeviceLogReader {
Devon Carew's avatar
Devon Carew committed
691 692 693 694 695 696 697 698
  _IOSSimulatorLogReader(this.device) {
    _linesController = new StreamController<String>.broadcast(
      onListen: () {
        _start();
      },
      onCancel: _stop
    );
  }
699 700 701

  final IOSSimulator device;

Devon Carew's avatar
Devon Carew committed
702
  StreamController<String> _linesController;
703 704
  bool _lastWasFiltered = false;

Devon Carew's avatar
Devon Carew committed
705
  // We log from two files: the device and the system log.
706 707 708
  Process _deviceProcess;
  Process _systemProcess;

709
  @override
Devon Carew's avatar
Devon Carew committed
710
  Stream<String> get logLines => _linesController.stream;
711

712
  @override
713 714
  String get name => device.name;

Devon Carew's avatar
Devon Carew committed
715
  Future<Null> _start() async {
716 717
    // Device log.
    device.ensureLogsExists();
Devon Carew's avatar
Devon Carew committed
718 719 720
    _deviceProcess = await runCommand(<String>['tail', '-n', '0', '-F', device.logFilePath]);
    _deviceProcess.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onDeviceLine);
    _deviceProcess.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onDeviceLine);
721 722 723

    // Track system.log crashes.
    // ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
Devon Carew's avatar
Devon Carew committed
724 725 726
    _systemProcess = await runCommand(<String>['tail', '-n', '0', '-F', '/private/var/log/system.log']);
    _systemProcess.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onSystemLine);
    _systemProcess.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onSystemLine);
727

Devon Carew's avatar
Devon Carew committed
728 729 730 731
    _deviceProcess.exitCode.then((int code) {
      if (_linesController.hasListener)
        _linesController.close();
    });
732 733 734 735
  }

  // Match the log prefix (in order to shorten it):
  //   'Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: ...'
Devon Carew's avatar
Devon Carew committed
736
  static final RegExp _mapRegex = new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$');
737 738

  // Jan 31 19:23:28 --- last message repeated 1 time ---
Devon Carew's avatar
Devon Carew committed
739
  static final RegExp _lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ --- (.*) ---$');
740

Devon Carew's avatar
Devon Carew committed
741
  static final RegExp _flutterRunnerRegex = new RegExp(r' FlutterRunner\[\d+\] ');
742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759

  String _filterDeviceLine(String string) {
    Match match = _mapRegex.matchAsPrefix(string);
    if (match != null) {
      _lastWasFiltered = true;

      // Filter out some messages that clearly aren't related to Flutter.
      if (string.contains(': could not find icon for representation -> com.apple.'))
        return null;

      String category = match.group(1);
      String content = match.group(2);
      if (category == 'Game Center' || category == 'itunesstored' ||
          category == 'nanoregistrylaunchd' || category == 'mstreamd' ||
          category == 'syncdefaultsd' || category == 'companionappd' ||
          category == 'searchd')
        return null;

760 761 762 763 764
      // assertiond: assertion failed: 15E65 13E230: assertiond + 15801 [3C808658-78EC-3950-A264-79A64E0E463B]: 0x1
      if (category == 'assertiond' && content.startsWith('assertion failed: ')
           && content.endsWith(']: 0x1'))
         return null;

765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780
      _lastWasFiltered = false;

      if (category == 'Runner')
        return content;
      return '$category: $content';
    }
    match = _lastMessageRegex.matchAsPrefix(string);
    if (match != null && !_lastWasFiltered)
      return '(${match.group(1)})';
    return string;
  }

  void _onDeviceLine(String line) {
    String filteredLine = _filterDeviceLine(line);
    if (filteredLine == null)
      return;
Devon Carew's avatar
Devon Carew committed
781
    _linesController.add(filteredLine);
782 783 784 785 786 787 788 789 790 791 792 793 794 795
  }

  String _filterSystemLog(String string) {
    Match match = _mapRegex.matchAsPrefix(string);
    return match == null ? string : '${match.group(1)}: ${match.group(2)}';
  }

  void _onSystemLine(String line) {
    if (!_flutterRunnerRegex.hasMatch(line))
      return;

    String filteredLine = _filterSystemLog(line);
    if (filteredLine == null)
      return;
796

Devon Carew's avatar
Devon Carew committed
797
    _linesController.add(filteredLine);
798 799
  }

Devon Carew's avatar
Devon Carew committed
800 801 802
  void _stop() {
    _deviceProcess?.kill();
    _systemProcess?.kill();
803 804
  }
}
805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842

int compareIosVersions(String v1, String v2) {
  List<int> v1Fragments = v1.split('.').map(int.parse).toList();
  List<int> v2Fragments = v2.split('.').map(int.parse).toList();

  int i = 0;
  while(i < v1Fragments.length && i < v2Fragments.length) {
    int v1Fragment = v1Fragments[i];
    int v2Fragment = v2Fragments[i];
    if (v1Fragment != v2Fragment)
      return v1Fragment.compareTo(v2Fragment);
    i++;
  }
  return v1Fragments.length.compareTo(v2Fragments.length);
}

/// Matches on device type given an identifier.
///
/// Example device type identifiers:
///   ✓ com.apple.CoreSimulator.SimDeviceType.iPhone-5
///   ✓ com.apple.CoreSimulator.SimDeviceType.iPhone-6
///   ✓ com.apple.CoreSimulator.SimDeviceType.iPhone-6s-Plus
///   ✗ com.apple.CoreSimulator.SimDeviceType.iPad-2
///   ✗ com.apple.CoreSimulator.SimDeviceType.Apple-Watch-38mm
final RegExp _iosDeviceTypePattern =
    new RegExp(r'com.apple.CoreSimulator.SimDeviceType.iPhone-(\d+)(.*)');

int compareIphoneVersions(String id1, String id2) {
  Match m1 = _iosDeviceTypePattern.firstMatch(id1);
  Match m2 = _iosDeviceTypePattern.firstMatch(id2);

  int v1 = int.parse(m1[1]);
  int v2 = int.parse(m2[1]);

  if (v1 != v2)
    return v1.compareTo(v2);

  // Sorted in the least preferred first order.
Ian Hickson's avatar
Ian Hickson committed
843
  const List<String> qualifiers = const <String>['-Plus', '', 's-Plus', 's'];
844 845 846 847 848

  int q1 = qualifiers.indexOf(m1[2]);
  int q2 = qualifiers.indexOf(m2[2]);
  return q1.compareTo(q2);
}
849 850 851 852 853 854 855 856

class _IOSSimulatorDevicePortForwarder extends DevicePortForwarder {
  _IOSSimulatorDevicePortForwarder(this.device);

  final IOSSimulator device;

  final List<ForwardedPort> _ports = <ForwardedPort>[];

857
  @override
858 859 860 861
  List<ForwardedPort> get forwardedPorts {
    return _ports;
  }

862
  @override
863 864 865 866 867 868 869 870 871
  Future<int> forward(int devicePort, {int hostPort: null}) async {
    if ((hostPort == null) || (hostPort == 0)) {
      hostPort = devicePort;
    }
    assert(devicePort == hostPort);
    _ports.add(new ForwardedPort(devicePort, hostPort));
    return hostPort;
  }

872
  @override
Ian Hickson's avatar
Ian Hickson committed
873
  Future<Null> unforward(ForwardedPort forwardedPort) async {
874 875 876
    _ports.remove(forwardedPort);
  }
}