simulators.dart 25.6 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
import 'dart:math' as math;
8 9 10 11

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

import '../application_package.dart';
12
import '../base/common.dart';
13
import '../base/context.dart';
14
import '../base/file_system.dart';
15
import '../base/io.dart';
16
import '../base/process.dart';
17
import '../base/process_manager.dart';
18
import '../build_info.dart';
19
import '../device.dart';
20
import '../flx.dart' as flx;
21
import '../globals.dart';
22
import '../protocol_discovery.dart';
23 24 25 26
import 'mac.dart';

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

27
/// Test device created by Flutter when no other device is available.
28
const String _kFlutterTestDeviceSuffix = '(Flutter)';
29

30 31 32
class IOSSimulators extends PollingDeviceDiscovery {
  IOSSimulators() : super('IOSSimulators');

33
  @override
34
  bool get supportsPlatform => Platform.isMacOS;
35 36

  @override
37 38 39 40 41
  List<Device> pollingGetDevices() => IOSSimulatorUtils.instance.getAttachedDevices();
}

class IOSSimulatorUtils {
  /// Returns [IOSSimulatorUtils] active in the current app context (i.e. zone).
42
  static IOSSimulatorUtils get instance => context[IOSSimulatorUtils];
43 44

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

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

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

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

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

    // `xcrun instruments` requires a template (-t). @yjbanov has no idea what
72 73 74
    // "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.
75
    List<String> args = <String>[_xcrunPath, 'instruments', '-w', deviceName, '-t', 'Blank', '-l', '1'];
76 77 78 79 80 81 82
    printTrace(args.join(' '));
    runDetached(args);
    printStatus('Waiting for iOS Simulator to boot...');

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

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

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

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

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

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

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

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

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

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

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

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

    return usableRuntimes.first['identifier'];
  }

  void _deleteDevice(SimDevice device) {
    try {
162
      List<String> args = <String>[_xcrunPath, 'simctl', 'delete', device.name];
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
      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`:
    //
178
    // {
179 180
    //   "devicetypes": { ... },
    //   "runtimes": { ... },
181 182 183 184 185 186 187 188 189
    //   "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"
    //       },
    //       ...
190 191
    //   },
    //   "pairs": { ... },
192

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

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

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

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

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

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

    return devices;
  }

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

226
  bool _isAnyConnected() => getConnectedDevices().isNotEmpty;
227

228 229 230 231 232 233 234 235 236 237
  bool isInstalled(String appId) {
    return exitsHappy(<String>[
      _xcrunPath,
      'simctl',
      'get_app_container',
      'booted',
      appId,
    ]);
  }

238
  void install(String deviceId, String appPath) {
239
    runCheckedSync(<String>[_xcrunPath, 'simctl', 'install', deviceId, appPath]);
240 241
  }

242 243 244 245
  void uninstall(String deviceId, String appId) {
    runCheckedSync(<String>[_xcrunPath, 'simctl', 'uninstall', deviceId, appId]);
  }

246
  void launch(String deviceId, String appIdentifier, [List<String> launchArgs]) {
247
    List<String> args = <String>[_xcrunPath, 'simctl', 'launch', deviceId, appIdentifier];
248 249 250 251 252 253
    if (launchArgs != null)
      args.addAll(launchArgs);
    runCheckedSync(args);
  }
}

254 255
/// Enumerates all data sections of `xcrun simctl list --json` command.
class SimControlListSection {
Ian Hickson's avatar
Ian Hickson committed
256
  const SimControlListSection._(this.name);
257 258

  final String name;
Ian Hickson's avatar
Ian Hickson committed
259 260 261 262 263

  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');
264 265
}

266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
/// 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;
}

290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
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 {
305
  IOSSimulator(String id, { this.name, this.category }) : super(id);
306

307
  @override
308 309
  final String name;

310 311
  final String category;

312
  @override
313 314
  bool get isLocalEmulator => true;

315 316 317
  @override
  bool get supportsHotMode => true;

318
  Map<ApplicationPackage, _IOSSimulatorLogReader> _logReaders;
319
  _IOSSimulatorDevicePortForwarder _portForwarder;
320

321 322 323
  String get xcrunPath => path.join('/usr', 'bin', 'xcrun');

  String _getSimulatorPath() {
324
    return path.join(homeDirPath, 'Library', 'Developer', 'CoreSimulator', 'Devices', id);
325 326 327 328 329 330 331 332 333
  }

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

334 335 336 337 338
  @override
  bool isAppInstalled(ApplicationPackage app) {
    return SimControl.instance.isInstalled(app.id);
  }

339 340 341
  @override
  bool installApp(ApplicationPackage app) {
    try {
342 343
      IOSApp iosApp = app;
      SimControl.instance.install(id, iosApp.simulatorBundlePath);
344 345 346 347 348 349
      return true;
    } catch (e) {
      return false;
    }
  }

350 351 352 353 354 355 356 357 358 359
  @override
  bool uninstallApp(ApplicationPackage app) {
    try {
      SimControl.instance.uninstall(id, app.id);
      return true;
    } catch (e) {
      return false;
    }
  }

360 361
  @override
  bool isSupported() {
362
    if (!Platform.isMacOS) {
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
      _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);

385 386
    // Not an iPhone. All available non-iPhone simulators are compatible.
    if (match == null)
387 388
      return true;

389 390
    // iPhones 6 and above are always fine.
    if (int.parse(match.group(1)) > 5)
391 392 393
      return true;

    // The 's' subtype of 5 is compatible.
394
    if (name.contains('iPhone 5s'))
395 396 397 398 399 400 401 402 403 404
      return true;

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

  String _supportMessage;

  @override
  String supportMessage() {
405
    if (isSupported())
406 407 408 409 410 411
      return "Supported";

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

  @override
Devon Carew's avatar
Devon Carew committed
412
  Future<LaunchResult> startApp(
413 414
    ApplicationPackage app,
    BuildMode mode, {
415 416
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
417
    DebuggingOptions debuggingOptions,
418
    Map<String, dynamic> platformArgs,
419 420
    bool prebuiltApplication: false,
    bool applicationNeedsRebuild: false,
421
  }) async {
422 423
    if (!prebuiltApplication) {
      printTrace('Building ${app.name} for $id.');
424

425 426
      try {
        await _setupUpdatedApplicationBundle(app);
427 428
      } on ToolExit catch (e) {
        printError(e.message);
429
        return new LaunchResult.failed();
430
      }
431 432 433
    } else {
      if (!installApp(app))
        return new LaunchResult.failed();
434
    }
435

436
    // Prepare launch arguments.
437
    List<String> args = <String>["--enable-dart-profiling"];
438 439 440 441 442 443 444 445

    if (!prebuiltApplication) {
      args.addAll(<String>[
        "--flx=${path.absolute(path.join(getBuildDirectory(), 'app.flx'))}",
        "--dart-main=${path.absolute(mainPath)}",
        "--packages=${path.absolute('.packages')}",
      ]);
    }
446

Devon Carew's avatar
Devon Carew committed
447
    if (debuggingOptions.debuggingEnabled) {
448
      if (debuggingOptions.buildMode == BuildMode.debug)
Devon Carew's avatar
Devon Carew committed
449 450 451
        args.add("--enable-checked-mode");
      if (debuggingOptions.startPaused)
        args.add("--start-paused");
452

Devon Carew's avatar
Devon Carew committed
453
      int observatoryPort = await debuggingOptions.findBestObservatoryPort();
454
      args.add("--observatory-port=$observatoryPort");
Devon Carew's avatar
Devon Carew committed
455
    }
456

457 458 459 460
    ProtocolDiscovery observatoryDiscovery;
    if (debuggingOptions.debuggingEnabled)
      observatoryDiscovery = new ProtocolDiscovery.observatory(getLogReader(app: app));

461
    // Launch the updated application in the simulator.
462
    try {
463
      SimControl.instance.launch(id, app.id, args);
464 465
    } catch (error) {
      printError('$error');
Devon Carew's avatar
Devon Carew committed
466
      return new LaunchResult.failed();
467 468
    }

Devon Carew's avatar
Devon Carew committed
469 470
    if (!debuggingOptions.debuggingEnabled) {
      return new LaunchResult.succeeded();
471
    }
Devon Carew's avatar
Devon Carew committed
472

473 474 475 476 477 478 479 480 481 482 483 484
    // 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 {
      Uri deviceUri = await observatoryDiscovery.nextUri();
      return new LaunchResult.succeeded(observatoryUri: deviceUri);
    } catch (error) {
      printError('Error waiting for a debug connection: $error');
      return new LaunchResult.failed();
    } finally {
      observatoryDiscovery.cancel();
Devon Carew's avatar
Devon Carew committed
485
    }
486 487
  }

488
  bool _applicationIsInstalledAndRunning(ApplicationPackage app) {
489
    bool isInstalled = isAppInstalled(app);
490

491
    bool isRunning = exitsHappy(<String>[
492 493 494 495 496 497 498
      '/usr/bin/killall',
      'Runner',
    ]);

    return isInstalled && isRunning;
  }

499 500
  Future<Null> _setupUpdatedApplicationBundle(ApplicationPackage app) async {
    await _sideloadUpdatedAssetsForInstalledApplicationBundle(app);
501 502

    if (!_applicationIsInstalledAndRunning(app))
503 504 505
      return _buildAndInstallApplicationBundle(app);
  }

506
  Future<Null> _buildAndInstallApplicationBundle(ApplicationPackage app) async {
507
    // Step 1: Build the Xcode project.
508
    // The build mode for the simulator is always debug.
509
    XcodeBuildResult buildResult = await buildXcodeProject(app: app, mode: BuildMode.debug, buildForDevice: false);
510 511
    if (!buildResult.success)
      throwToolExit('Could not build the application for the simulator.');
512 513

    // Step 2: Assert that the Xcode project was successfully built.
514
    IOSApp iosApp = app;
515
    Directory bundle = fs.directory(iosApp.simulatorBundlePath);
516
    bool bundleExists = bundle.existsSync();
517 518
    if (!bundleExists)
      throwToolExit('Could not find the built application bundle at ${bundle.path}.');
519 520 521 522 523

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

524 525
  Future<Null> _sideloadUpdatedAssetsForInstalledApplicationBundle(ApplicationPackage app) =>
      flx.build(precompiledSnapshot: true);
526

527 528 529 530 531 532 533 534
  @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 {
535
    if (Platform.isMacOS) {
536 537 538 539 540 541 542 543
      String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
      runCheckedSync(<String>['cp', localFile, path.join(simulatorHomeDirectory, targetFile)]);
      return true;
    }
    return false;
  }

  String get logFilePath {
544
    return path.join(homeDirPath, 'Library', 'Logs', 'CoreSimulator', id, 'system.log');
545 546 547
  }

  @override
548
  TargetPlatform get platform => TargetPlatform.ios;
549

550 551 552
  @override
  String get sdkNameAndVersion => category;

553
  @override
554 555 556
  DeviceLogReader getLogReader({ApplicationPackage app}) {
    _logReaders ??= <ApplicationPackage, _IOSSimulatorLogReader>{};
    return _logReaders.putIfAbsent(app, () => new _IOSSimulatorLogReader(this, app));
557
  }
558

559
  @override
560 561 562 563 564 565 566
  DevicePortForwarder get portForwarder {
    if (_portForwarder == null)
      _portForwarder = new _IOSSimulatorDevicePortForwarder(this);

    return _portForwarder;
  }

567
  @override
568
  void clearLogs() {
569
    File logFile = fs.file(logFilePath);
570 571 572 573 574 575 576 577
    if (logFile.existsSync()) {
      RandomAccessFile randomFile = logFile.openSync(mode: FileMode.WRITE);
      randomFile.truncateSync(0);
      randomFile.closeSync();
    }
  }

  void ensureLogsExists() {
578
    File logFile = fs.file(logFilePath);
579 580 581
    if (!logFile.existsSync())
      logFile.writeAsBytesSync(<int>[]);
  }
Devon Carew's avatar
Devon Carew committed
582 583 584 585 586 587

  @override
  bool get supportsScreenshot => true;

  @override
  Future<bool> takeScreenshot(File outputFile) async {
588
    Directory desktopDir = fs.directory(path.join(homeDirPath, 'Desktop'));
Devon Carew's avatar
Devon Carew committed
589 590 591 592 593 594 595 596

    // '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');
      }));
pq's avatar
pq committed
597
    }
Devon Carew's avatar
Devon Carew committed
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

    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;
  }
624 625 626
}

class _IOSSimulatorLogReader extends DeviceLogReader {
627 628 629
  String _appName;

  _IOSSimulatorLogReader(this.device, ApplicationPackage app) {
Devon Carew's avatar
Devon Carew committed
630 631 632 633 634 635
    _linesController = new StreamController<String>.broadcast(
      onListen: () {
        _start();
      },
      onCancel: _stop
    );
636
    _appName = app == null ? null : app.name.replaceAll('.app', '');
Devon Carew's avatar
Devon Carew committed
637
  }
638 639 640

  final IOSSimulator device;

Devon Carew's avatar
Devon Carew committed
641
  StreamController<String> _linesController;
642

Devon Carew's avatar
Devon Carew committed
643
  // We log from two files: the device and the system log.
644 645
  Process _deviceProcess;
  Process _systemProcess;
646

647
  @override
Devon Carew's avatar
Devon Carew committed
648
  Stream<String> get logLines => _linesController.stream;
649

650
  @override
651 652
  String get name => device.name;

Devon Carew's avatar
Devon Carew committed
653
  Future<Null> _start() async {
654 655
    // Device log.
    device.ensureLogsExists();
Devon Carew's avatar
Devon Carew committed
656 657 658
    _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);
659 660 661

    // Track system.log crashes.
    // ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
Devon Carew's avatar
Devon Carew committed
662 663 664
    _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);
665

Devon Carew's avatar
Devon Carew committed
666 667 668 669
    _deviceProcess.exitCode.then((int code) {
      if (_linesController.hasListener)
        _linesController.close();
    });
670 671 672 673
  }

  // 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
674
  static final RegExp _mapRegex = new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$');
675 676

  // Jan 31 19:23:28 --- last message repeated 1 time ---
677 678
  static final RegExp _lastMessageSingleRegex = new RegExp(r'\S+ +\S+ +\S+ --- last message repeated 1 time ---$');
  static final RegExp _lastMessageMultipleRegex = new RegExp(r'\S+ +\S+ +\S+ --- last message repeated (\d+) times ---$');
679

Devon Carew's avatar
Devon Carew committed
680
  static final RegExp _flutterRunnerRegex = new RegExp(r' FlutterRunner\[\d+\] ');
681

682 683 684 685 686 687 688
  /// List of log categories to always show in the logs, even if this is an
  /// app-secific [DeviceLogReader]. Add to this list to make the log output
  /// more verbose.
  static final List<String> _whitelistedLogCategories = <String>[
    'CoreSimulatorBridge',
  ];

689 690 691 692 693
  String _filterDeviceLine(String string) {
    Match match = _mapRegex.matchAsPrefix(string);
    if (match != null) {
      String category = match.group(1);
      String content = match.group(2);
694 695 696

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

699 700 701 702 703 704 705 706 707 708 709
      if (category == 'CoreSimulatorBridge'
          && content.startsWith('Pasteboard change listener callback port'))
        return null;

      if (category == 'routined'
          && content.startsWith('CoreLocation: Error occurred while trying to retrieve motion state update'))
        return null;

      if (category == 'syslogd' && content == 'ASL Sender Statistics')
        return null;

710
      // assertiond: assertion failed: 15E65 13E230: assertiond + 15801 [3C808658-78EC-3950-A264-79A64E0E463B]: 0x1
711 712 713
      if (category == 'assertiond'
          && content.startsWith('assertion failed: ')
          && content.endsWith(']: 0x1'))
714 715
         return null;

716 717 718
      if (_appName == null || _whitelistedLogCategories.contains(category))
        return '$category: $content';
      else if (category == _appName)
719
        return content;
720 721

      return null;
722
    }
723 724

    if (_lastMessageSingleRegex.matchAsPrefix(string) != null)
725
      return null;
726 727 728 729

    if (new RegExp(r'assertion failed: .* libxpc.dylib .* 0x7d$').matchAsPrefix(string) != null)
      return null;

730 731 732
    return string;
  }

733 734
  String _lastLine;

735
  void _onDeviceLine(String line) {
736
    printTrace('[DEVICE LOG] $line');
737 738 739 740 741 742
    Match multi = _lastMessageMultipleRegex.matchAsPrefix(line);

    if (multi != null) {
      if (_lastLine != null) {
        int repeat = int.parse(multi.group(1));
        repeat = math.max(0, math.min(100, repeat));
743
        for (int i = 1; i < repeat; i++)
744 745 746 747 748 749 750
          _linesController.add(_lastLine);
      }
    } else {
      _lastLine = _filterDeviceLine(line);
      if (_lastLine != null)
        _linesController.add(_lastLine);
    }
751 752 753 754 755 756 757 758
  }

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

  void _onSystemLine(String line) {
759
    printTrace('[SYS LOG] $line');
760 761 762 763 764 765
    if (!_flutterRunnerRegex.hasMatch(line))
      return;

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

Devon Carew's avatar
Devon Carew committed
767
    _linesController.add(filteredLine);
768 769
  }

Devon Carew's avatar
Devon Carew committed
770 771 772
  void _stop() {
    _deviceProcess?.kill();
    _systemProcess?.kill();
773 774
  }
}
775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812

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
813
  const List<String> qualifiers = const <String>['-Plus', '', 's-Plus', 's'];
814 815 816 817 818

  int q1 = qualifiers.indexOf(m1[2]);
  int q2 = qualifiers.indexOf(m2[2]);
  return q1.compareTo(q2);
}
819 820 821 822 823 824 825 826

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

  final IOSSimulator device;

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

827
  @override
828 829 830 831
  List<ForwardedPort> get forwardedPorts {
    return _ports;
  }

832
  @override
833 834 835 836 837 838 839 840 841
  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;
  }

842
  @override
Ian Hickson's avatar
Ian Hickson committed
843
  Future<Null> unforward(ForwardedPort forwardedPort) async {
844 845 846
    _ports.remove(forwardedPort);
  }
}