simulators.dart 25.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// 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:math' as math;
7

8
import 'package:meta/meta.dart';
9
import 'package:process/process.dart';
10

11
import '../application_package.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/process.dart';
17
import '../base/utils.dart';
18
import '../build_info.dart';
19
import '../convert.dart';
20
import '../device.dart';
21
import '../globals.dart' as globals;
22
import '../macos/xcode.dart';
23
import '../project.dart';
24
import '../protocol_discovery.dart';
25
import 'mac.dart';
26
import 'plist_parser.dart';
27 28

const String _xcrunPath = '/usr/bin/xcrun';
29
const String iosSimulatorId = 'apple_ios_simulator';
30 31

class IOSSimulators extends PollingDeviceDiscovery {
32 33 34 35 36 37
  IOSSimulators({
    @required IOSSimulatorUtils iosSimulatorUtils,
  }) : _iosSimulatorUtils = iosSimulatorUtils,
       super('iOS simulators');

  final IOSSimulatorUtils _iosSimulatorUtils;
38

39
  @override
40
  bool get supportsPlatform => globals.platform.isMacOS;
41

42
  @override
43
  bool get canListAnything => globals.iosWorkflow.canListDevices;
44

45
  @override
46
  Future<List<Device>> pollingGetDevices() async => _iosSimulatorUtils.getAttachedDevices();
47 48 49
}

class IOSSimulatorUtils {
50 51 52 53 54 55 56 57
  IOSSimulatorUtils({
    @required SimControl simControl,
    @required Xcode xcode,
  }) : _simControl = simControl,
       _xcode = xcode;

  final SimControl _simControl;
  final Xcode _xcode;
58

59
  Future<List<IOSSimulator>> getAttachedDevices() async {
60
    if (!_xcode.isInstalledAndMeetsVersionCheck) {
61
      return <IOSSimulator>[];
62
    }
63

64
    final List<SimDevice> connected = await _simControl.getConnectedDevices();
65
    return connected.map<IOSSimulator>((SimDevice device) {
66 67 68 69 70 71 72
      return IOSSimulator(
        device.udid,
        name: device.name,
        simControl: _simControl,
        simulatorCategory: device.category,
        xcode: _xcode,
      );
73 74
    }).toList();
  }
75 76 77 78
}

/// A wrapper around the `simctl` command line tool.
class SimControl {
79 80 81 82 83 84 85 86
  SimControl({
    @required Logger logger,
    @required ProcessManager processManager,
  }) : _logger = logger,
       _processUtils = ProcessUtils(processManager: processManager, logger: logger);

  final Logger _logger;
  final ProcessUtils _processUtils;
87

88 89
  /// Runs `simctl list --json` and returns the JSON of the corresponding
  /// [section].
90
  Future<Map<String, dynamic>> _list(SimControlListSection section) async {
91 92
    // Sample output from `simctl list --json`:
    //
93
    // {
94 95
    //   "devicetypes": { ... },
    //   "runtimes": { ... },
96 97 98 99 100 101 102 103 104
    //   "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"
    //       },
    //       ...
105 106
    //   },
    //   "pairs": { ... },
107

108
    final List<String> command = <String>[_xcrunPath, 'simctl', 'list', '--json', section.name];
109 110
    _logger.printTrace(command.join(' '));
    final RunResult results = await _processUtils.run(command);
111
    if (results.exitCode != 0) {
112
      _logger.printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
113
      return <String, Map<String, dynamic>>{};
114
    }
115
    try {
116 117 118 119
      final Object decodeResult = json.decode(results.stdout?.toString())[section.name];
      if (decodeResult is Map<String, dynamic>) {
        return decodeResult;
      }
120
      _logger.printError('simctl returned unexpected JSON response: ${results.stdout}');
121
      return <String, dynamic>{};
122 123 124 125
    } on FormatException {
      // We failed to parse the simctl output, or it returned junk.
      // One known message is "Install Started" isn't valid JSON but is
      // returned sometimes.
126
      _logger.printError('simctl returned non-JSON response: ${results.stdout}');
127 128
      return <String, dynamic>{};
    }
129 130 131
  }

  /// Returns a list of all available devices, both potential and connected.
132
  Future<List<SimDevice>> getDevices() async {
133
    final List<SimDevice> devices = <SimDevice>[];
134

135
    final Map<String, dynamic> devicesSection = await _list(SimControlListSection.devices);
136

137
    for (final String deviceCategory in devicesSection.keys) {
138 139
      final Object devicesData = devicesSection[deviceCategory];
      if (devicesData != null && devicesData is List<dynamic>) {
140
        for (final Map<String, dynamic> data in devicesData.map<Map<String, dynamic>>(castStringKeyedMap)) {
141 142
          devices.add(SimDevice(deviceCategory, data));
        }
143 144 145 146 147 148 149
      }
    }

    return devices;
  }

  /// Returns all the connected simulator devices.
150 151 152
  Future<List<SimDevice>> getConnectedDevices() async {
    final List<SimDevice> simDevices = await getDevices();
    return simDevices.where((SimDevice device) => device.isBooted).toList();
153 154
  }

155
  Future<bool> isInstalled(String deviceId, String appId) {
156
    return _processUtils.exitsHappy(<String>[
157 158 159
      _xcrunPath,
      'simctl',
      'get_app_container',
160
      deviceId,
161 162 163 164
      appId,
    ]);
  }

165 166
  Future<RunResult> install(String deviceId, String appPath) async {
    RunResult result;
167
    try {
168
      result = await _processUtils.run(
169 170 171
        <String>[_xcrunPath, 'simctl', 'install', deviceId, appPath],
        throwOnError: true,
      );
172
    } on ProcessException catch (exception) {
173
      throwToolExit('Unable to install $appPath on $deviceId. This is sometimes caused by a malformed plist file:\n$exception');
174 175
    }
    return result;
176 177
  }

178 179
  Future<RunResult> uninstall(String deviceId, String appId) async {
    RunResult result;
180
    try {
181
      result = await _processUtils.run(
182 183 184
        <String>[_xcrunPath, 'simctl', 'uninstall', deviceId, appId],
        throwOnError: true,
      );
185 186 187 188
    } on ProcessException catch (exception) {
      throwToolExit('Unable to uninstall $appId from $deviceId:\n$exception');
    }
    return result;
189 190
  }

191 192
  Future<RunResult> launch(String deviceId, String appIdentifier, [ List<String> launchArgs ]) async {
    RunResult result;
193
    try {
194
      result = await _processUtils.run(
195 196 197 198 199 200 201 202 203 204
        <String>[
          _xcrunPath,
          'simctl',
          'launch',
          deviceId,
          appIdentifier,
          ...?launchArgs,
        ],
        throwOnError: true,
      );
205 206 207 208
    } on ProcessException catch (exception) {
      throwToolExit('Unable to launch $appIdentifier on $deviceId:\n$exception');
    }
    return result;
209
  }
210

211
  Future<void> takeScreenshot(String deviceId, String outputPath) async {
212
    try {
213
      await _processUtils.run(
214 215 216
        <String>[_xcrunPath, 'simctl', 'io', deviceId, 'screenshot', outputPath],
        throwOnError: true,
      );
217
    } on ProcessException catch (exception) {
218
      _logger.printError('Unable to take screenshot of $deviceId:\n$exception');
219
    }
220
  }
221 222
}

223 224
/// Enumerates all data sections of `xcrun simctl list --json` command.
class SimControlListSection {
Ian Hickson's avatar
Ian Hickson committed
225
  const SimControlListSection._(this.name);
226 227

  final String name;
Ian Hickson's avatar
Ian Hickson committed
228

229 230 231 232
  static const SimControlListSection devices = SimControlListSection._('devices');
  static const SimControlListSection devicetypes = SimControlListSection._('devicetypes');
  static const SimControlListSection runtimes = SimControlListSection._('runtimes');
  static const SimControlListSection pairs = SimControlListSection._('pairs');
233 234
}

235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
/// 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;
}

259 260 261 262
class SimDevice {
  SimDevice(this.category, this.data);

  final String category;
263
  final Map<String, dynamic> data;
264

265 266 267 268
  String get state => data['state']?.toString();
  String get availability => data['availability']?.toString();
  String get name => data['name']?.toString();
  String get udid => data['udid']?.toString();
269 270 271 272 273

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

class IOSSimulator extends Device {
274 275 276 277 278 279 280 281 282 283 284 285 286 287
  IOSSimulator(
    String id, {
      this.name,
      this.simulatorCategory,
      @required SimControl simControl,
      @required Xcode xcode,
    }) : _simControl = simControl,
         _xcode = xcode,
         super(
           id,
           category: Category.mobile,
           platformType: PlatformType.ios,
           ephemeral: true,
         );
288

289
  @override
290 291
  final String name;

292
  final String simulatorCategory;
293

294 295 296
  final SimControl _simControl;
  final Xcode _xcode;

297
  @override
298
  Future<bool> get isLocalEmulator async => true;
299

300 301 302
  @override
  Future<String> get emulatorId async => iosSimulatorId;

303
  @override
304 305 306 307
  bool get supportsHotReload => true;

  @override
  bool get supportsHotRestart => true;
308

309
  Map<ApplicationPackage, _IOSSimulatorLogReader> _logReaders;
310
  _IOSSimulatorDevicePortForwarder _portForwarder;
311

312
  String get xcrunPath => globals.fs.path.join('/usr', 'bin', 'xcrun');
313

314
  @override
315
  Future<bool> isAppInstalled(ApplicationPackage app) {
316
    return _simControl.isInstalled(id, app.id);
317 318
  }

319
  @override
320
  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;
321

322
  @override
323
  Future<bool> installApp(covariant IOSApp app) async {
324
    try {
325
      final IOSApp iosApp = app;
326
      await _simControl.install(id, iosApp.simulatorBundlePath);
327
      return true;
328
    } on Exception {
329 330 331 332
      return false;
    }
  }

333
  @override
334
  Future<bool> uninstallApp(ApplicationPackage app) async {
335
    try {
336
      await _simControl.uninstall(id, app.id);
337
      return true;
338
    } on Exception {
339 340 341 342
      return false;
    }
  }

343 344
  @override
  bool isSupported() {
345
    if (!globals.platform.isMacOS) {
346
      _supportMessage = 'iOS devices require a Mac host machine.';
347 348 349
      return false;
    }

350 351
    // Check if the device is part of a blacklisted category.
    // We do not yet support WatchOS or tvOS devices.
352
    final RegExp blacklist = RegExp(r'Apple (TV|Watch)', caseSensitive: false);
353
    if (blacklist.hasMatch(name)) {
354
      _supportMessage = 'Flutter does not support Apple TV or Apple Watch.';
355 356 357
      return false;
    }
    return true;
358 359 360 361 362 363
  }

  String _supportMessage;

  @override
  String supportMessage() {
364
    if (isSupported()) {
365
      return 'Supported';
366
    }
367

368
    return _supportMessage ?? 'Unknown';
369 370 371
  }

  @override
Devon Carew's avatar
Devon Carew committed
372
  Future<LaunchResult> startApp(
373
    covariant IOSApp package, {
374 375
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
376
    DebuggingOptions debuggingOptions,
377
    Map<String, dynamic> platformArgs,
378 379
    bool prebuiltApplication = false,
    bool ipv6 = false,
380
  }) async {
381
    if (!prebuiltApplication && package is BuildableIOSApp) {
382
      globals.printTrace('Building ${package.name} for $id.');
383

384
      try {
385
        await _setupUpdatedApplicationBundle(package, debuggingOptions.buildInfo, mainPath);
386
      } on ToolExit catch (e) {
387
        globals.printError(e.message);
388
        return LaunchResult.failed();
389
      }
390
    } else {
391
      if (!await installApp(package)) {
392
        return LaunchResult.failed();
393
      }
394
    }
395

396
    // Prepare launch arguments.
397 398 399 400
    final List<String> args = <String>[
      '--enable-dart-profiling',
      if (debuggingOptions.debuggingEnabled) ...<String>[
        if (debuggingOptions.buildInfo.isDebug) ...<String>[
401
          '--enable-checked-mode',
402
          '--verify-entry-points',
403 404 405 406 407
        ],
        if (debuggingOptions.startPaused) '--start-paused',
        if (debuggingOptions.disableServiceAuthCodes) '--disable-service-auth-codes',
        if (debuggingOptions.skiaDeterministicRendering) '--skia-deterministic-rendering',
        if (debuggingOptions.useTestFonts) '--use-test-fonts',
408
        '--observatory-port=${debuggingOptions.hostVmServicePort ?? 0}',
409
      ],
410
    ];
411

412
    ProtocolDiscovery observatoryDiscovery;
413
    if (debuggingOptions.debuggingEnabled) {
414
      observatoryDiscovery = ProtocolDiscovery.observatory(
415 416 417 418 419
        getLogReader(app: package),
        ipv6: ipv6,
        hostPort: debuggingOptions.hostVmServicePort,
        devicePort: debuggingOptions.deviceVmServicePort,
      );
420
    }
421

422
    // Launch the updated application in the simulator.
423
    try {
424 425 426 427
      // Use the built application's Info.plist to get the bundle identifier,
      // which should always yield the correct value and does not require
      // parsing the xcodeproj or configuration files.
      // See https://github.com/flutter/flutter/issues/31037 for more information.
428
      final String plistPath = globals.fs.path.join(package.simulatorBundlePath, 'Info.plist');
429
      final String bundleIdentifier = globals.plistParser.getValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
430

431
      await _simControl.launch(id, bundleIdentifier, args);
432
    } on Exception catch (error) {
433
      globals.printError('$error');
434
      return LaunchResult.failed();
435 436
    }

Devon Carew's avatar
Devon Carew committed
437
    if (!debuggingOptions.debuggingEnabled) {
438
      return LaunchResult.succeeded();
439
    }
Devon Carew's avatar
Devon Carew committed
440

441 442
    // Wait for the service protocol port here. This will complete once the
    // device has printed "Observatory is listening on..."
443
    globals.printTrace('Waiting for observatory port to be available...');
444 445

    try {
446
      final Uri deviceUri = await observatoryDiscovery.uri;
447 448 449 450 451 452 453
      if (deviceUri != null) {
        return LaunchResult.succeeded(observatoryUri: deviceUri);
      }
      globals.printError(
        'Error waiting for a debug connection: '
        'The log reader failed unexpectedly',
      );
454
    } on Exception catch (error) {
455
      globals.printError('Error waiting for a debug connection: $error');
456
    } finally {
457
      await observatoryDiscovery?.cancel();
Devon Carew's avatar
Devon Carew committed
458
    }
459
    return LaunchResult.failed();
460 461
  }

462
  Future<void> _setupUpdatedApplicationBundle(covariant BuildableIOSApp app, BuildInfo buildInfo, String mainPath) async {
463
    // Step 1: Build the Xcode project.
464
    // The build mode for the simulator is always debug.
465
    assert(buildInfo.isDebug);
466

467 468
    final XcodeBuildResult buildResult = await buildXcodeProject(
      app: app,
469
      buildInfo: buildInfo,
470 471 472
      targetOverride: mainPath,
      buildForDevice: false,
    );
473
    if (!buildResult.success) {
474
      throwToolExit('Could not build the application for the simulator.');
475
    }
476 477

    // Step 2: Assert that the Xcode project was successfully built.
478
    final Directory bundle = globals.fs.directory(app.simulatorBundlePath);
479
    final bool bundleExists = bundle.existsSync();
480
    if (!bundleExists) {
481
      throwToolExit('Could not find the built application bundle at ${bundle.path}.');
482
    }
483 484

    // Step 3: Install the updated bundle to the simulator.
485
    await _simControl.install(id, globals.fs.path.absolute(bundle.path));
486 487
  }

488 489 490 491 492 493 494
  @override
  Future<bool> stopApp(ApplicationPackage app) async {
    // Currently we don't have a way to stop an app running on iOS.
    return false;
  }

  String get logFilePath {
495
    return globals.platform.environment.containsKey('IOS_SIMULATOR_LOG_FILE_PATH')
496 497 498 499 500 501 502 503 504
      ? globals.platform.environment['IOS_SIMULATOR_LOG_FILE_PATH'].replaceAll('%{id}', id)
      : globals.fs.path.join(
          globals.fsUtils.homeDirPath,
          'Library',
          'Logs',
          'CoreSimulator',
          id,
          'system.log',
        );
505 506 507
  }

  @override
508
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
509

510
  @override
511
  Future<String> get sdkNameAndVersion async => simulatorCategory;
512

513
  final RegExp _iosSdkRegExp = RegExp(r'iOS( |-)(\d+)');
514 515 516

  Future<int> get sdkMajorVersion async {
    final Match sdkMatch = _iosSdkRegExp.firstMatch(await sdkNameAndVersion);
517
    return int.parse(sdkMatch?.group(2) ?? '11');
518 519
  }

520
  @override
521
  DeviceLogReader getLogReader({ covariant IOSApp app }) {
522
    assert(app is IOSApp);
523
    _logReaders ??= <ApplicationPackage, _IOSSimulatorLogReader>{};
524
    return _logReaders.putIfAbsent(app, () => _IOSSimulatorLogReader(this, app));
525
  }
526

527
  @override
528
  DevicePortForwarder get portForwarder => _portForwarder ??= _IOSSimulatorDevicePortForwarder(this);
529

530
  @override
531
  void clearLogs() {
532
    final File logFile = globals.fs.file(logFilePath);
533
    if (logFile.existsSync()) {
534
      final RandomAccessFile randomFile = logFile.openSync(mode: FileMode.write);
535 536 537 538 539
      randomFile.truncateSync(0);
      randomFile.closeSync();
    }
  }

540
  Future<void> ensureLogsExists() async {
541
    if (await sdkMajorVersion < 11) {
542
      final File logFile = globals.fs.file(logFilePath);
543
      if (!logFile.existsSync()) {
544
        logFile.writeAsBytesSync(<int>[]);
545
      }
546
    }
547
  }
Devon Carew's avatar
Devon Carew committed
548

549
  bool get _xcodeVersionSupportsScreenshot {
550
    return _xcode.majorVersion > 8 || (_xcode.majorVersion == 8 && _xcode.minorVersion >= 2);
551
  }
Devon Carew's avatar
Devon Carew committed
552 553

  @override
554
  bool get supportsScreenshot => _xcodeVersionSupportsScreenshot;
Devon Carew's avatar
Devon Carew committed
555

556
  @override
557
  Future<void> takeScreenshot(File outputFile) {
558
    return _simControl.takeScreenshot(id, outputFile.path);
Devon Carew's avatar
Devon Carew committed
559
  }
560 561 562 563 564

  @override
  bool isSupportedForProject(FlutterProject flutterProject) {
    return flutterProject.ios.existsSync();
  }
565 566 567 568 569 570 571 572 573 574

  @override
  Future<void> dispose() async {
    _logReaders?.forEach(
      (ApplicationPackage application, _IOSSimulatorLogReader logReader) {
        logReader.dispose();
      },
    );
    await _portForwarder?.dispose();
  }
575 576
}

577 578 579
/// Launches the device log reader process on the host.
Future<Process> launchDeviceLogTool(IOSSimulator device) async {
  // Versions of iOS prior to iOS 11 log to the simulator syslog file.
580 581 582
  if (await device.sdkMajorVersion < 11) {
    return processUtils.start(<String>['tail', '-n', '0', '-F', device.logFilePath]);
  }
583 584 585

  // For iOS 11 and above, use /usr/bin/log to tail process logs.
  // Run in interactive mode (via script), otherwise /usr/bin/log buffers in 4k chunks. (radar: 34420207)
586
  return processUtils.start(<String>[
587 588 589 590 591 592
    'script', '/dev/null', '/usr/bin/log', 'stream', '--style', 'syslog', '--predicate', 'processImagePath CONTAINS "${device.id}"',
  ]);
}

Future<Process> launchSystemLogTool(IOSSimulator device) async {
  // Versions of iOS prior to 11 tail the simulator syslog file.
593 594 595
  if (await device.sdkMajorVersion < 11) {
    return processUtils.start(<String>['tail', '-n', '0', '-F', '/private/var/log/system.log']);
  }
596 597 598 599 600

  // For iOS 11 and later, all relevant detail is in the device log.
  return null;
}

601
class _IOSSimulatorLogReader extends DeviceLogReader {
602
  _IOSSimulatorLogReader(this.device, IOSApp app) {
603
    _linesController = StreamController<String>.broadcast(
604
      onListen: _start,
605
      onCancel: _stop,
Devon Carew's avatar
Devon Carew committed
606
    );
607
    _appName = app == null ? null : app.name.replaceAll('.app', '');
Devon Carew's avatar
Devon Carew committed
608
  }
609 610 611

  final IOSSimulator device;

612 613
  String _appName;

Devon Carew's avatar
Devon Carew committed
614
  StreamController<String> _linesController;
615

Devon Carew's avatar
Devon Carew committed
616
  // We log from two files: the device and the system log.
617 618
  Process _deviceProcess;
  Process _systemProcess;
619

620
  @override
Devon Carew's avatar
Devon Carew committed
621
  Stream<String> get logLines => _linesController.stream;
622

623
  @override
624 625
  String get name => device.name;

626
  Future<void> _start() async {
627
    // Device log.
628
    await device.ensureLogsExists();
629
    _deviceProcess = await launchDeviceLogTool(device);
630 631
    _deviceProcess.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onDeviceLine);
    _deviceProcess.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onDeviceLine);
632 633 634

    // Track system.log crashes.
    // ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
635 636
    _systemProcess = await launchSystemLogTool(device);
    if (_systemProcess != null) {
637 638
      _systemProcess.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onSystemLine);
      _systemProcess.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onSystemLine);
639
    }
640

641 642
    // We don't want to wait for the process or its callback. Best effort
    // cleanup in the callback.
643
    unawaited(_deviceProcess.exitCode.whenComplete(() {
644
      if (_linesController.hasListener) {
Devon Carew's avatar
Devon Carew committed
645
        _linesController.close();
646
      }
647
    }));
648 649 650
  }

  // Match the log prefix (in order to shorten it):
651 652 653
  // * Xcode 8: Sep 13 15:28:51 cbracken-macpro localhost Runner[37195]: (Flutter) Observatory listening on http://127.0.0.1:57701/
  // * Xcode 9: 2017-09-13 15:26:57.228948-0700  localhost Runner[37195]: (Flutter) Observatory listening on http://127.0.0.1:57701/
  static final RegExp _mapRegex = RegExp(r'\S+ +\S+ +\S+ +(\S+ +)?(\S+)\[\d+\]\)?: (\(.*?\))? *(.*)$');
654 655

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

659
  static final RegExp _flutterRunnerRegex = RegExp(r' FlutterRunner\[\d+\] ');
660 661

  String _filterDeviceLine(String string) {
662
    final Match match = _mapRegex.matchAsPrefix(string);
663
    if (match != null) {
664 665 666
      final String category = match.group(2);
      final String tag = match.group(3);
      final String content = match.group(4);
667

668
      // Filter out non-Flutter originated noise from the engine.
669
      if (_appName != null && category != _appName) {
670
        return null;
671
      }
672

673
      if (tag != null && tag != '(Flutter)') {
674
        return null;
675
      }
676

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

682
      // assertion failed: 15G1212 13E230: libxpc.dylib + 57882 [66C28065-C9DB-3C8E-926F-5A40210A6D1B]: 0x7d
683
      if (content.startsWith('assertion failed: ') && content.contains(' libxpc.dylib ')) {
684
        return null;
685
      }
686

687
      if (_appName == null) {
688
        return '$category: $content';
689
      } else if (category == _appName) {
690
        return content;
691
      }
692 693

      return null;
694
    }
695

696
    if (string.startsWith('Filtering the log data using ')) {
697
      return null;
698
    }
699

700
    if (string.startsWith('Timestamp                       (process)[PID]')) {
701
      return null;
702
    }
703

704
    if (_lastMessageSingleRegex.matchAsPrefix(string) != null) {
705
      return null;
706
    }
707

708
    if (RegExp(r'assertion failed: .* libxpc.dylib .* 0x7d$').matchAsPrefix(string) != null) {
709
      return null;
710
    }
711

712 713 714
    return string;
  }

715 716
  String _lastLine;

717
  void _onDeviceLine(String line) {
718
    globals.printTrace('[DEVICE LOG] $line');
719
    final Match multi = _lastMessageMultipleRegex.matchAsPrefix(line);
720 721 722 723 724

    if (multi != null) {
      if (_lastLine != null) {
        int repeat = int.parse(multi.group(1));
        repeat = math.max(0, math.min(100, repeat));
725
        for (int i = 1; i < repeat; i++) {
726
          _linesController.add(_lastLine);
727
        }
728 729 730
      }
    } else {
      _lastLine = _filterDeviceLine(line);
731
      if (_lastLine != null) {
732
        _linesController.add(_lastLine);
733
      }
734
    }
735 736 737
  }

  String _filterSystemLog(String string) {
738
    final Match match = _mapRegex.matchAsPrefix(string);
739 740 741 742
    return match == null ? string : '${match.group(1)}: ${match.group(2)}';
  }

  void _onSystemLine(String line) {
743
    globals.printTrace('[SYS LOG] $line');
744
    if (!_flutterRunnerRegex.hasMatch(line)) {
745
      return;
746
    }
747

748
    final String filteredLine = _filterSystemLog(line);
749
    if (filteredLine == null) {
750
      return;
751
    }
752

Devon Carew's avatar
Devon Carew committed
753
    _linesController.add(filteredLine);
754 755
  }

Devon Carew's avatar
Devon Carew committed
756 757 758
  void _stop() {
    _deviceProcess?.kill();
    _systemProcess?.kill();
759
  }
760 761 762 763 764

  @override
  void dispose() {
    _stop();
  }
765
}
766 767

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

  int i = 0;
772
  while (i < v1Fragments.length && i < v2Fragments.length) {
773 774
    final int v1Fragment = v1Fragments[i];
    final int v2Fragment = v2Fragments[i];
775
    if (v1Fragment != v2Fragment) {
776
      return v1Fragment.compareTo(v2Fragment);
777
    }
778
    i += 1;
779 780 781 782 783 784 785 786 787 788 789 790 791
  }
  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 =
792
    RegExp(r'com.apple.CoreSimulator.SimDeviceType.iPhone-(\d+)(.*)');
793 794

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

798 799
  final int v1 = int.parse(m1[1]);
  final int v2 = int.parse(m2[1]);
800

801
  if (v1 != v2) {
802
    return v1.compareTo(v2);
803
  }
804 805

  // Sorted in the least preferred first order.
806
  const List<String> qualifiers = <String>['-Plus', '', 's-Plus', 's'];
807

808 809
  final int q1 = qualifiers.indexOf(m1[2]);
  final int q2 = qualifiers.indexOf(m2[2]);
810 811
  return q1.compareTo(q2);
}
812 813 814 815 816 817 818 819

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

  final IOSSimulator device;

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

820
  @override
821
  List<ForwardedPort> get forwardedPorts => _ports;
822

823
  @override
824
  Future<int> forward(int devicePort, { int hostPort }) async {
825
    if (hostPort == null || hostPort == 0) {
826 827 828
      hostPort = devicePort;
    }
    assert(devicePort == hostPort);
829
    _ports.add(ForwardedPort(devicePort, hostPort));
830 831 832
    return hostPort;
  }

833
  @override
834
  Future<void> unforward(ForwardedPort forwardedPort) async {
835 836
    _ports.remove(forwardedPort);
  }
837 838 839

  @override
  Future<void> dispose() async {
840 841
    final List<ForwardedPort> portsCopy = List<ForwardedPort>.from(_ports);
    for (final ForwardedPort port in portsCopy) {
842 843 844
      await unforward(port);
    }
  }
845
}