simulators.dart 25.7 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 9
import 'package:meta/meta.dart';

10
import '../application_package.dart';
11
import '../base/common.dart';
12
import '../base/context.dart';
13
import '../base/file_system.dart';
14
import '../base/io.dart';
15
import '../base/process.dart';
16
import '../base/utils.dart';
17
import '../build_info.dart';
18
import '../bundle.dart';
19
import '../convert.dart';
20
import '../device.dart';
21
import '../globals.dart' as globals;
22
import '../project.dart';
23
import '../protocol_discovery.dart';
24
import 'ios_workflow.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
  IOSSimulators() : super('iOS simulators');
33

34
  @override
35
  bool get supportsPlatform => globals.platform.isMacOS;
36

37
  @override
38
  bool get canListAnything => iosWorkflow.canListDevices;
39

40
  @override
41
  Future<List<Device>> pollingGetDevices() async => IOSSimulatorUtils.instance.getAttachedDevices();
42 43 44 45
}

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

48
  Future<List<IOSSimulator>> getAttachedDevices() async {
49
    if (!globals.xcode.isInstalledAndMeetsVersionCheck) {
50
      return <IOSSimulator>[];
51
    }
52

53 54
    final List<SimDevice> connected = await SimControl.instance.getConnectedDevices();
    return connected.map<IOSSimulator>((SimDevice device) {
55
      return IOSSimulator(device.udid, name: device.name, simulatorCategory: device.category);
56 57
    }).toList();
  }
58 59 60 61
}

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

65 66
  /// Runs `simctl list --json` and returns the JSON of the corresponding
  /// [section].
67
  Future<Map<String, dynamic>> _list(SimControlListSection section) async {
68 69
    // Sample output from `simctl list --json`:
    //
70
    // {
71 72
    //   "devicetypes": { ... },
    //   "runtimes": { ... },
73 74 75 76 77 78 79 80 81
    //   "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"
    //       },
    //       ...
82 83
    //   },
    //   "pairs": { ... },
84

85
    final List<String> command = <String>[_xcrunPath, 'simctl', 'list', '--json', section.name];
86 87
    globals.printTrace(command.join(' '));
    final ProcessResult results = await globals.processManager.run(command);
88
    if (results.exitCode != 0) {
89
      globals.printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
90
      return <String, Map<String, dynamic>>{};
91
    }
92
    try {
93 94 95 96
      final Object decodeResult = json.decode(results.stdout?.toString())[section.name];
      if (decodeResult is Map<String, dynamic>) {
        return decodeResult;
      }
97
      globals.printError('simctl returned unexpected JSON response: ${results.stdout}');
98
      return <String, dynamic>{};
99 100 101 102
    } 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.
103
      globals.printError('simctl returned non-JSON response: ${results.stdout}');
104 105
      return <String, dynamic>{};
    }
106 107 108
  }

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

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

114
    for (final String deviceCategory in devicesSection.keys) {
115 116
      final Object devicesData = devicesSection[deviceCategory];
      if (devicesData != null && devicesData is List<dynamic>) {
117
        for (final Map<String, dynamic> data in devicesData.map<Map<String, dynamic>>(castStringKeyedMap)) {
118 119
          devices.add(SimDevice(deviceCategory, data));
        }
120 121 122 123 124 125 126
      }
    }

    return devices;
  }

  /// Returns all the connected simulator devices.
127 128 129
  Future<List<SimDevice>> getConnectedDevices() async {
    final List<SimDevice> simDevices = await getDevices();
    return simDevices.where((SimDevice device) => device.isBooted).toList();
130 131
  }

132
  Future<bool> isInstalled(String deviceId, String appId) {
133
    return processUtils.exitsHappy(<String>[
134 135 136
      _xcrunPath,
      'simctl',
      'get_app_container',
137
      deviceId,
138 139 140 141
      appId,
    ]);
  }

142
  Future<RunResult> install(String deviceId, String appPath) {
143 144
    Future<RunResult> result;
    try {
145 146 147 148
      result = processUtils.run(
        <String>[_xcrunPath, 'simctl', 'install', deviceId, appPath],
        throwOnError: true,
      );
149 150 151 152
    } on ProcessException catch (exception) {
      throwToolExit('Unable to install $appPath on $deviceId:\n$exception');
    }
    return result;
153 154
  }

155
  Future<RunResult> uninstall(String deviceId, String appId) {
156 157
    Future<RunResult> result;
    try {
158 159 160 161
      result = processUtils.run(
        <String>[_xcrunPath, 'simctl', 'uninstall', deviceId, appId],
        throwOnError: true,
      );
162 163 164 165
    } on ProcessException catch (exception) {
      throwToolExit('Unable to uninstall $appId from $deviceId:\n$exception');
    }
    return result;
166 167
  }

168
  Future<RunResult> launch(String deviceId, String appIdentifier, [ List<String> launchArgs ]) {
169 170
    Future<RunResult> result;
    try {
171 172 173 174 175 176 177 178 179 180 181
      result = processUtils.run(
        <String>[
          _xcrunPath,
          'simctl',
          'launch',
          deviceId,
          appIdentifier,
          ...?launchArgs,
        ],
        throwOnError: true,
      );
182 183 184 185
    } on ProcessException catch (exception) {
      throwToolExit('Unable to launch $appIdentifier on $deviceId:\n$exception');
    }
    return result;
186
  }
187

188
  Future<void> takeScreenshot(String deviceId, String outputPath) async {
189
    try {
190 191 192 193
      await processUtils.run(
        <String>[_xcrunPath, 'simctl', 'io', deviceId, 'screenshot', outputPath],
        throwOnError: true,
      );
194 195 196
    } on ProcessException catch (exception) {
      throwToolExit('Unable to take screenshot of $deviceId:\n$exception');
    }
197
  }
198 199
}

200 201
/// Enumerates all data sections of `xcrun simctl list --json` command.
class SimControlListSection {
Ian Hickson's avatar
Ian Hickson committed
202
  const SimControlListSection._(this.name);
203 204

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

206 207 208 209
  static const SimControlListSection devices = SimControlListSection._('devices');
  static const SimControlListSection devicetypes = SimControlListSection._('devicetypes');
  static const SimControlListSection runtimes = SimControlListSection._('runtimes');
  static const SimControlListSection pairs = SimControlListSection._('pairs');
210 211
}

212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
/// 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;
}

236 237 238 239
class SimDevice {
  SimDevice(this.category, this.data);

  final String category;
240
  final Map<String, dynamic> data;
241

242 243 244 245
  String get state => data['state']?.toString();
  String get availability => data['availability']?.toString();
  String get name => data['name']?.toString();
  String get udid => data['udid']?.toString();
246 247 248 249 250

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

class IOSSimulator extends Device {
251 252 253 254 255 256
  IOSSimulator(String id, { this.name, this.simulatorCategory }) : super(
      id,
      category: Category.mobile,
      platformType: PlatformType.ios,
      ephemeral: true,
  );
257

258
  @override
259 260
  final String name;

261
  final String simulatorCategory;
262

263
  @override
264
  Future<bool> get isLocalEmulator async => true;
265

266 267 268
  @override
  Future<String> get emulatorId async => iosSimulatorId;

269
  @override
270 271 272 273
  bool get supportsHotReload => true;

  @override
  bool get supportsHotRestart => true;
274

275
  Map<ApplicationPackage, _IOSSimulatorLogReader> _logReaders;
276
  _IOSSimulatorDevicePortForwarder _portForwarder;
277

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

280
  @override
281
  Future<bool> isAppInstalled(ApplicationPackage app) {
282
    return SimControl.instance.isInstalled(id, app.id);
283 284
  }

285
  @override
286
  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;
287

288
  @override
289
  Future<bool> installApp(covariant IOSApp app) async {
290
    try {
291
      final IOSApp iosApp = app;
292
      await SimControl.instance.install(id, iosApp.simulatorBundlePath);
293 294 295 296 297 298
      return true;
    } catch (e) {
      return false;
    }
  }

299
  @override
300
  Future<bool> uninstallApp(ApplicationPackage app) async {
301
    try {
302
      await SimControl.instance.uninstall(id, app.id);
303 304 305 306 307 308
      return true;
    } catch (e) {
      return false;
    }
  }

309 310
  @override
  bool isSupported() {
311
    if (!globals.platform.isMacOS) {
312
      _supportMessage = 'iOS devices require a Mac host machine.';
313 314 315
      return false;
    }

316 317
    // Check if the device is part of a blacklisted category.
    // We do not yet support WatchOS or tvOS devices.
318
    final RegExp blacklist = RegExp(r'Apple (TV|Watch)', caseSensitive: false);
319
    if (blacklist.hasMatch(name)) {
320
      _supportMessage = 'Flutter does not support Apple TV or Apple Watch.';
321 322 323
      return false;
    }
    return true;
324 325 326 327 328 329
  }

  String _supportMessage;

  @override
  String supportMessage() {
330
    if (isSupported()) {
331
      return 'Supported';
332
    }
333

334
    return _supportMessage ?? 'Unknown';
335 336 337
  }

  @override
Devon Carew's avatar
Devon Carew committed
338
  Future<LaunchResult> startApp(
339
    covariant IOSApp package, {
340 341
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
342
    DebuggingOptions debuggingOptions,
343
    Map<String, dynamic> platformArgs,
344 345
    bool prebuiltApplication = false,
    bool ipv6 = false,
346
  }) async {
347
    if (!prebuiltApplication && package is BuildableIOSApp) {
348
      globals.printTrace('Building ${package.name} for $id.');
349

350
      try {
351
        await _setupUpdatedApplicationBundle(package, debuggingOptions.buildInfo, mainPath);
352
      } on ToolExit catch (e) {
353
        globals.printError(e.message);
354
        return LaunchResult.failed();
355
      }
356
    } else {
357
      if (!await installApp(package)) {
358
        return LaunchResult.failed();
359
      }
360
    }
361

362
    // Prepare launch arguments.
363 364 365 366
    final List<String> args = <String>[
      '--enable-dart-profiling',
      if (debuggingOptions.debuggingEnabled) ...<String>[
        if (debuggingOptions.buildInfo.isDebug) ...<String>[
367
          '--enable-checked-mode',
368
          '--verify-entry-points',
369 370 371 372 373
        ],
        if (debuggingOptions.startPaused) '--start-paused',
        if (debuggingOptions.disableServiceAuthCodes) '--disable-service-auth-codes',
        if (debuggingOptions.skiaDeterministicRendering) '--skia-deterministic-rendering',
        if (debuggingOptions.useTestFonts) '--use-test-fonts',
374
        '--observatory-port=${debuggingOptions.hostVmServicePort ?? 0}',
375
      ],
376
    ];
377

378
    ProtocolDiscovery observatoryDiscovery;
379
    if (debuggingOptions.debuggingEnabled) {
380
      observatoryDiscovery = ProtocolDiscovery.observatory(
381 382 383 384 385
        getLogReader(app: package),
        ipv6: ipv6,
        hostPort: debuggingOptions.hostVmServicePort,
        devicePort: debuggingOptions.deviceVmServicePort,
      );
386
    }
387

388
    // Launch the updated application in the simulator.
389
    try {
390 391 392 393
      // 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.
394
      final String plistPath = globals.fs.path.join(package.simulatorBundlePath, 'Info.plist');
395
      final String bundleIdentifier = PlistParser.instance.getValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
396 397

      await SimControl.instance.launch(id, bundleIdentifier, args);
398
    } catch (error) {
399
      globals.printError('$error');
400
      return LaunchResult.failed();
401 402
    }

Devon Carew's avatar
Devon Carew committed
403
    if (!debuggingOptions.debuggingEnabled) {
404
      return LaunchResult.succeeded();
405
    }
Devon Carew's avatar
Devon Carew committed
406

407 408
    // Wait for the service protocol port here. This will complete once the
    // device has printed "Observatory is listening on..."
409
    globals.printTrace('Waiting for observatory port to be available...');
410 411

    try {
412
      final Uri deviceUri = await observatoryDiscovery.uri;
413
      return LaunchResult.succeeded(observatoryUri: deviceUri);
414
    } catch (error) {
415
      globals.printError('Error waiting for a debug connection: $error');
416
      return LaunchResult.failed();
417
    } finally {
418
      await observatoryDiscovery.cancel();
Devon Carew's avatar
Devon Carew committed
419
    }
420 421
  }

422
  Future<void> _setupUpdatedApplicationBundle(covariant BuildableIOSApp app, BuildInfo buildInfo, String mainPath) async {
423
    await sideloadUpdatedAssetsForInstalledApplicationBundle(buildInfo, mainPath);
424

425
    // Step 1: Build the Xcode project.
426
    // The build mode for the simulator is always debug.
427

428
    final BuildInfo debugBuildInfo = BuildInfo(BuildMode.debug, buildInfo.flavor,
429
        trackWidgetCreation: buildInfo.trackWidgetCreation,
430
        extraFrontEndOptions: buildInfo.extraFrontEndOptions,
431 432
        extraGenSnapshotOptions: buildInfo.extraGenSnapshotOptions,
        treeShakeIcons: buildInfo.treeShakeIcons);
433

434 435 436 437 438 439
    final XcodeBuildResult buildResult = await buildXcodeProject(
      app: app,
      buildInfo: debugBuildInfo,
      targetOverride: mainPath,
      buildForDevice: false,
    );
440
    if (!buildResult.success) {
441
      throwToolExit('Could not build the application for the simulator.');
442
    }
443 444

    // Step 2: Assert that the Xcode project was successfully built.
445
    final Directory bundle = globals.fs.directory(app.simulatorBundlePath);
446
    final bool bundleExists = bundle.existsSync();
447
    if (!bundleExists) {
448
      throwToolExit('Could not find the built application bundle at ${bundle.path}.');
449
    }
450 451

    // Step 3: Install the updated bundle to the simulator.
452
    await SimControl.instance.install(id, globals.fs.path.absolute(bundle.path));
453 454
  }

455 456
  @visibleForTesting
  Future<void> sideloadUpdatedAssetsForInstalledApplicationBundle(BuildInfo buildInfo, String mainPath) {
457
    // Run compiler to produce kernel file for the application.
458
    return BundleBuilder().build(
459
      platform: TargetPlatform.ios,
460
      mainPath: mainPath,
461
      precompiledSnapshot: false,
462
      buildMode: buildInfo.mode,
463
      trackWidgetCreation: buildInfo.trackWidgetCreation,
464
      treeShakeIcons: false,
465 466
    );
  }
467

468 469 470 471 472 473 474
  @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 {
475
    return globals.platform.environment.containsKey('IOS_SIMULATOR_LOG_FILE_PATH')
476 477 478 479 480 481 482 483 484
      ? globals.platform.environment['IOS_SIMULATOR_LOG_FILE_PATH'].replaceAll('%{id}', id)
      : globals.fs.path.join(
          globals.fsUtils.homeDirPath,
          'Library',
          'Logs',
          'CoreSimulator',
          id,
          'system.log',
        );
485 486 487
  }

  @override
488
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
489

490
  @override
491
  Future<String> get sdkNameAndVersion async => simulatorCategory;
492

493
  final RegExp _iosSdkRegExp = RegExp(r'iOS( |-)(\d+)');
494 495 496

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

500
  @override
501
  DeviceLogReader getLogReader({ covariant IOSApp app }) {
502
    assert(app is IOSApp);
503
    _logReaders ??= <ApplicationPackage, _IOSSimulatorLogReader>{};
504
    return _logReaders.putIfAbsent(app, () => _IOSSimulatorLogReader(this, app));
505
  }
506

507
  @override
508
  DevicePortForwarder get portForwarder => _portForwarder ??= _IOSSimulatorDevicePortForwarder(this);
509

510
  @override
511
  void clearLogs() {
512
    final File logFile = globals.fs.file(logFilePath);
513
    if (logFile.existsSync()) {
514
      final RandomAccessFile randomFile = logFile.openSync(mode: FileMode.write);
515 516 517 518 519
      randomFile.truncateSync(0);
      randomFile.closeSync();
    }
  }

520
  Future<void> ensureLogsExists() async {
521
    if (await sdkMajorVersion < 11) {
522
      final File logFile = globals.fs.file(logFilePath);
523
      if (!logFile.existsSync()) {
524
        logFile.writeAsBytesSync(<int>[]);
525
      }
526
    }
527
  }
Devon Carew's avatar
Devon Carew committed
528

529
  bool get _xcodeVersionSupportsScreenshot {
530
    return globals.xcode.majorVersion > 8 || (globals.xcode.majorVersion == 8 && globals.xcode.minorVersion >= 2);
531
  }
Devon Carew's avatar
Devon Carew committed
532 533

  @override
534
  bool get supportsScreenshot => _xcodeVersionSupportsScreenshot;
Devon Carew's avatar
Devon Carew committed
535

536
  @override
537
  Future<void> takeScreenshot(File outputFile) {
538
    return SimControl.instance.takeScreenshot(id, outputFile.path);
Devon Carew's avatar
Devon Carew committed
539
  }
540 541 542 543 544

  @override
  bool isSupportedForProject(FlutterProject flutterProject) {
    return flutterProject.ios.existsSync();
  }
545 546 547 548 549 550 551 552 553 554

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

557 558 559
/// 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.
560 561 562
  if (await device.sdkMajorVersion < 11) {
    return processUtils.start(<String>['tail', '-n', '0', '-F', device.logFilePath]);
  }
563 564 565

  // 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)
566
  return processUtils.start(<String>[
567 568 569 570 571 572
    '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.
573 574 575
  if (await device.sdkMajorVersion < 11) {
    return processUtils.start(<String>['tail', '-n', '0', '-F', '/private/var/log/system.log']);
  }
576 577 578 579 580

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

581
class _IOSSimulatorLogReader extends DeviceLogReader {
582
  _IOSSimulatorLogReader(this.device, IOSApp app) {
583
    _linesController = StreamController<String>.broadcast(
584
      onListen: _start,
585
      onCancel: _stop,
Devon Carew's avatar
Devon Carew committed
586
    );
587
    _appName = app == null ? null : app.name.replaceAll('.app', '');
Devon Carew's avatar
Devon Carew committed
588
  }
589 590 591

  final IOSSimulator device;

592 593
  String _appName;

Devon Carew's avatar
Devon Carew committed
594
  StreamController<String> _linesController;
595

Devon Carew's avatar
Devon Carew committed
596
  // We log from two files: the device and the system log.
597 598
  Process _deviceProcess;
  Process _systemProcess;
599

600
  @override
Devon Carew's avatar
Devon Carew committed
601
  Stream<String> get logLines => _linesController.stream;
602

603
  @override
604 605
  String get name => device.name;

606
  Future<void> _start() async {
607
    // Device log.
608
    await device.ensureLogsExists();
609
    _deviceProcess = await launchDeviceLogTool(device);
610 611
    _deviceProcess.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onDeviceLine);
    _deviceProcess.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onDeviceLine);
612 613 614

    // Track system.log crashes.
    // ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
615 616
    _systemProcess = await launchSystemLogTool(device);
    if (_systemProcess != null) {
617 618
      _systemProcess.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onSystemLine);
      _systemProcess.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onSystemLine);
619
    }
620

621 622
    // We don't want to wait for the process or its callback. Best effort
    // cleanup in the callback.
623
    unawaited(_deviceProcess.exitCode.whenComplete(() {
624
      if (_linesController.hasListener) {
Devon Carew's avatar
Devon Carew committed
625
        _linesController.close();
626
      }
627
    }));
628 629 630
  }

  // Match the log prefix (in order to shorten it):
631 632 633
  // * 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+\]\)?: (\(.*?\))? *(.*)$');
634 635

  // Jan 31 19:23:28 --- last message repeated 1 time ---
636 637
  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 ---$');
638

639
  static final RegExp _flutterRunnerRegex = RegExp(r' FlutterRunner\[\d+\] ');
640 641

  String _filterDeviceLine(String string) {
642
    final Match match = _mapRegex.matchAsPrefix(string);
643
    if (match != null) {
644 645 646
      final String category = match.group(2);
      final String tag = match.group(3);
      final String content = match.group(4);
647

648
      // Filter out non-Flutter originated noise from the engine.
649
      if (_appName != null && category != _appName) {
650
        return null;
651
      }
652

653
      if (tag != null && tag != '(Flutter)') {
654
        return null;
655
      }
656

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

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

667
      if (_appName == null) {
668
        return '$category: $content';
669
      } else if (category == _appName) {
670
        return content;
671
      }
672 673

      return null;
674
    }
675

676
    if (string.startsWith('Filtering the log data using ')) {
677
      return null;
678
    }
679

680
    if (string.startsWith('Timestamp                       (process)[PID]')) {
681
      return null;
682
    }
683

684
    if (_lastMessageSingleRegex.matchAsPrefix(string) != null) {
685
      return null;
686
    }
687

688
    if (RegExp(r'assertion failed: .* libxpc.dylib .* 0x7d$').matchAsPrefix(string) != null) {
689
      return null;
690
    }
691

692 693 694
    return string;
  }

695 696
  String _lastLine;

697
  void _onDeviceLine(String line) {
698
    globals.printTrace('[DEVICE LOG] $line');
699
    final Match multi = _lastMessageMultipleRegex.matchAsPrefix(line);
700 701 702 703 704

    if (multi != null) {
      if (_lastLine != null) {
        int repeat = int.parse(multi.group(1));
        repeat = math.max(0, math.min(100, repeat));
705
        for (int i = 1; i < repeat; i++) {
706
          _linesController.add(_lastLine);
707
        }
708 709 710
      }
    } else {
      _lastLine = _filterDeviceLine(line);
711
      if (_lastLine != null) {
712
        _linesController.add(_lastLine);
713
      }
714
    }
715 716 717
  }

  String _filterSystemLog(String string) {
718
    final Match match = _mapRegex.matchAsPrefix(string);
719 720 721 722
    return match == null ? string : '${match.group(1)}: ${match.group(2)}';
  }

  void _onSystemLine(String line) {
723
    globals.printTrace('[SYS LOG] $line');
724
    if (!_flutterRunnerRegex.hasMatch(line)) {
725
      return;
726
    }
727

728
    final String filteredLine = _filterSystemLog(line);
729
    if (filteredLine == null) {
730
      return;
731
    }
732

Devon Carew's avatar
Devon Carew committed
733
    _linesController.add(filteredLine);
734 735
  }

Devon Carew's avatar
Devon Carew committed
736 737 738
  void _stop() {
    _deviceProcess?.kill();
    _systemProcess?.kill();
739
  }
740 741 742 743 744

  @override
  void dispose() {
    _stop();
  }
745
}
746 747

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

  int i = 0;
752
  while (i < v1Fragments.length && i < v2Fragments.length) {
753 754
    final int v1Fragment = v1Fragments[i];
    final int v2Fragment = v2Fragments[i];
755
    if (v1Fragment != v2Fragment) {
756
      return v1Fragment.compareTo(v2Fragment);
757
    }
758
    i += 1;
759 760 761 762 763 764 765 766 767 768 769 770 771
  }
  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 =
772
    RegExp(r'com.apple.CoreSimulator.SimDeviceType.iPhone-(\d+)(.*)');
773 774

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

778 779
  final int v1 = int.parse(m1[1]);
  final int v2 = int.parse(m2[1]);
780

781
  if (v1 != v2) {
782
    return v1.compareTo(v2);
783
  }
784 785

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

788 789
  final int q1 = qualifiers.indexOf(m1[2]);
  final int q2 = qualifiers.indexOf(m2[2]);
790 791
  return q1.compareTo(q2);
}
792 793 794 795 796 797 798 799

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

  final IOSSimulator device;

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

800
  @override
801
  List<ForwardedPort> get forwardedPorts => _ports;
802

803
  @override
804
  Future<int> forward(int devicePort, { int hostPort }) async {
805
    if (hostPort == null || hostPort == 0) {
806 807 808
      hostPort = devicePort;
    }
    assert(devicePort == hostPort);
809
    _ports.add(ForwardedPort(devicePort, hostPort));
810 811 812
    return hostPort;
  }

813
  @override
814
  Future<void> unforward(ForwardedPort forwardedPort) async {
815 816
    _ports.remove(forwardedPort);
  }
817 818 819

  @override
  Future<void> dispose() async {
820 821
    final List<ForwardedPort> portsCopy = List<ForwardedPort>.from(_ports);
    for (final ForwardedPort port in portsCopy) {
822 823 824
      await unforward(port);
    }
  }
825
}