simulators.dart 24.7 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:math' as math;
7 8

import '../application_package.dart';
9
import '../base/common.dart';
10
import '../base/context.dart';
11
import '../base/file_system.dart';
12
import '../base/io.dart';
13
import '../base/platform.dart';
14
import '../base/process.dart';
15
import '../base/process_manager.dart';
16
import '../base/utils.dart';
17
import '../build_info.dart';
18
import '../bundle.dart';
19
import '../convert.dart';
20 21
import '../device.dart';
import '../globals.dart';
22
import '../macos/xcode.dart';
23
import '../project.dart';
24
import '../protocol_discovery.dart';
25
import 'ios_workflow.dart';
26
import 'mac.dart';
27
import 'plist_parser.dart';
28 29

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

class IOSSimulators extends PollingDeviceDiscovery {
33
  IOSSimulators() : super('iOS simulators');
34

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

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

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

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

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

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

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

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

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

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

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

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

    return devices;
  }

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

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

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

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

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

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

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

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

207 208 209 210
  static const SimControlListSection devices = SimControlListSection._('devices');
  static const SimControlListSection devicetypes = SimControlListSection._('devicetypes');
  static const SimControlListSection runtimes = SimControlListSection._('runtimes');
  static const SimControlListSection pairs = SimControlListSection._('pairs');
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 236
/// 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;
}

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

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

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

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

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

259
  @override
260 261
  final String name;

262
  final String simulatorCategory;
263

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

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

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

  @override
  bool get supportsHotRestart => true;
275

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

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

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

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

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

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

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

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

  String _supportMessage;

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

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

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

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

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

379
    ProtocolDiscovery observatoryDiscovery;
380
    if (debuggingOptions.debuggingEnabled) {
381
      observatoryDiscovery = ProtocolDiscovery.observatory(
382
          getLogReader(app: package), ipv6: ipv6);
383
    }
384

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

      await SimControl.instance.launch(id, bundleIdentifier, args);
395 396
    } catch (error) {
      printError('$error');
397
      return LaunchResult.failed();
398 399
    }

Devon Carew's avatar
Devon Carew committed
400
    if (!debuggingOptions.debuggingEnabled) {
401
      return LaunchResult.succeeded();
402
    }
Devon Carew's avatar
Devon Carew committed
403

404 405 406 407 408
    // 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 {
409
      final Uri deviceUri = await observatoryDiscovery.uri;
410
      return LaunchResult.succeeded(observatoryUri: deviceUri);
411 412
    } catch (error) {
      printError('Error waiting for a debug connection: $error');
413
      return LaunchResult.failed();
414
    } finally {
415
      await observatoryDiscovery.cancel();
Devon Carew's avatar
Devon Carew committed
416
    }
417 418
  }

419
  Future<void> _setupUpdatedApplicationBundle(covariant BuildableIOSApp app, BuildInfo buildInfo, String mainPath) async {
420
    await _sideloadUpdatedAssetsForInstalledApplicationBundle(app, buildInfo, mainPath);
421

422
    // Step 1: Build the Xcode project.
423
    // The build mode for the simulator is always debug.
424

425
    final BuildInfo debugBuildInfo = BuildInfo(BuildMode.debug, buildInfo.flavor,
426
        trackWidgetCreation: buildInfo.trackWidgetCreation,
427
        extraFrontEndOptions: buildInfo.extraFrontEndOptions,
428
        extraGenSnapshotOptions: buildInfo.extraGenSnapshotOptions);
429

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

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

    // Step 3: Install the updated bundle to the simulator.
xster's avatar
xster committed
448
    await SimControl.instance.install(id, fs.path.absolute(bundle.path));
449 450
  }

451
  Future<void> _sideloadUpdatedAssetsForInstalledApplicationBundle(ApplicationPackage app, BuildInfo buildInfo, String mainPath) {
452
    // Run compiler to produce kernel file for the application.
453
    return BundleBuilder().build(
454
      mainPath: mainPath,
455
      precompiledSnapshot: false,
456 457 458
      trackWidgetCreation: buildInfo.trackWidgetCreation,
    );
  }
459

460 461 462 463 464 465 466
  @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 {
467 468 469
    return platform.environment.containsKey('IOS_SIMULATOR_LOG_FILE_PATH')
        ? platform.environment['IOS_SIMULATOR_LOG_FILE_PATH'].replaceAll('%{id}', id)
        : fs.path.join(homeDirPath, 'Library', 'Logs', 'CoreSimulator', id, 'system.log');
470 471 472
  }

  @override
473
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
474

475
  @override
476
  Future<String> get sdkNameAndVersion async => simulatorCategory;
477

478
  final RegExp _iosSdkRegExp = RegExp(r'iOS( |-)(\d+)');
479 480 481

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

485
  @override
486
  DeviceLogReader getLogReader({ covariant IOSApp app }) {
487
    assert(app is IOSApp);
488
    _logReaders ??= <ApplicationPackage, _IOSSimulatorLogReader>{};
489
    return _logReaders.putIfAbsent(app, () => _IOSSimulatorLogReader(this, app));
490
  }
491

492
  @override
493
  DevicePortForwarder get portForwarder => _portForwarder ??= _IOSSimulatorDevicePortForwarder(this);
494

495
  @override
496
  void clearLogs() {
497
    final File logFile = fs.file(logFilePath);
498
    if (logFile.existsSync()) {
499
      final RandomAccessFile randomFile = logFile.openSync(mode: FileMode.write);
500 501 502 503 504
      randomFile.truncateSync(0);
      randomFile.closeSync();
    }
  }

505
  Future<void> ensureLogsExists() async {
506 507
    if (await sdkMajorVersion < 11) {
      final File logFile = fs.file(logFilePath);
508
      if (!logFile.existsSync()) {
509
        logFile.writeAsBytesSync(<int>[]);
510
      }
511
    }
512
  }
Devon Carew's avatar
Devon Carew committed
513

514
  bool get _xcodeVersionSupportsScreenshot {
515
    return xcode.majorVersion > 8 || (xcode.majorVersion == 8 && xcode.minorVersion >= 2);
516
  }
Devon Carew's avatar
Devon Carew committed
517 518

  @override
519
  bool get supportsScreenshot => _xcodeVersionSupportsScreenshot;
Devon Carew's avatar
Devon Carew committed
520

521
  @override
522
  Future<void> takeScreenshot(File outputFile) {
523
    return SimControl.instance.takeScreenshot(id, outputFile.path);
Devon Carew's avatar
Devon Carew committed
524
  }
525 526 527 528 529

  @override
  bool isSupportedForProject(FlutterProject flutterProject) {
    return flutterProject.ios.existsSync();
  }
530 531
}

532 533 534
/// 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.
535 536 537
  if (await device.sdkMajorVersion < 11) {
    return processUtils.start(<String>['tail', '-n', '0', '-F', device.logFilePath]);
  }
538 539 540

  // 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)
541
  return processUtils.start(<String>[
542 543 544 545 546 547
    '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.
548 549 550
  if (await device.sdkMajorVersion < 11) {
    return processUtils.start(<String>['tail', '-n', '0', '-F', '/private/var/log/system.log']);
  }
551 552 553 554 555

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

556
class _IOSSimulatorLogReader extends DeviceLogReader {
557
  _IOSSimulatorLogReader(this.device, IOSApp app) {
558
    _linesController = StreamController<String>.broadcast(
559
      onListen: _start,
560
      onCancel: _stop,
Devon Carew's avatar
Devon Carew committed
561
    );
562
    _appName = app == null ? null : app.name.replaceAll('.app', '');
Devon Carew's avatar
Devon Carew committed
563
  }
564 565 566

  final IOSSimulator device;

567 568
  String _appName;

Devon Carew's avatar
Devon Carew committed
569
  StreamController<String> _linesController;
570

Devon Carew's avatar
Devon Carew committed
571
  // We log from two files: the device and the system log.
572 573
  Process _deviceProcess;
  Process _systemProcess;
574

575
  @override
Devon Carew's avatar
Devon Carew committed
576
  Stream<String> get logLines => _linesController.stream;
577

578
  @override
579 580
  String get name => device.name;

581
  Future<void> _start() async {
582
    // Device log.
583
    await device.ensureLogsExists();
584
    _deviceProcess = await launchDeviceLogTool(device);
585 586
    _deviceProcess.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onDeviceLine);
    _deviceProcess.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onDeviceLine);
587 588 589

    // Track system.log crashes.
    // ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
590 591
    _systemProcess = await launchSystemLogTool(device);
    if (_systemProcess != null) {
592 593
      _systemProcess.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onSystemLine);
      _systemProcess.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onSystemLine);
594
    }
595

596 597
    // We don't want to wait for the process or its callback. Best effort
    // cleanup in the callback.
598
    unawaited(_deviceProcess.exitCode.whenComplete(() {
599
      if (_linesController.hasListener) {
Devon Carew's avatar
Devon Carew committed
600
        _linesController.close();
601
      }
602
    }));
603 604 605
  }

  // Match the log prefix (in order to shorten it):
606 607
  // * 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/
608
  static final RegExp _mapRegex = RegExp(r'\S+ +\S+ +\S+ +(\S+ +)?(\S+)\[\d+\]\)?: (\(.*?\))? *(.*)$');
609 610

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

614
  static final RegExp _flutterRunnerRegex = RegExp(r' FlutterRunner\[\d+\] ');
615 616

  String _filterDeviceLine(String string) {
617
    final Match match = _mapRegex.matchAsPrefix(string);
618
    if (match != null) {
619 620 621
      final String category = match.group(2);
      final String tag = match.group(3);
      final String content = match.group(4);
622

623
      // Filter out non-Flutter originated noise from the engine.
624
      if (_appName != null && category != _appName) {
625
        return null;
626
      }
627

628
      if (tag != null && tag != '(Flutter)') {
629
        return null;
630
      }
631

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

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

642
      if (_appName == null) {
643
        return '$category: $content';
644
      } else if (category == _appName) {
645
        return content;
646
      }
647 648

      return null;
649
    }
650

651
    if (string.startsWith('Filtering the log data using ')) {
652
      return null;
653
    }
654

655
    if (string.startsWith('Timestamp                       (process)[PID]')) {
656
      return null;
657
    }
658

659
    if (_lastMessageSingleRegex.matchAsPrefix(string) != null) {
660
      return null;
661
    }
662

663
    if (RegExp(r'assertion failed: .* libxpc.dylib .* 0x7d$').matchAsPrefix(string) != null) {
664
      return null;
665
    }
666

667 668 669
    return string;
  }

670 671
  String _lastLine;

672
  void _onDeviceLine(String line) {
673
    printTrace('[DEVICE LOG] $line');
674
    final Match multi = _lastMessageMultipleRegex.matchAsPrefix(line);
675 676 677 678 679

    if (multi != null) {
      if (_lastLine != null) {
        int repeat = int.parse(multi.group(1));
        repeat = math.max(0, math.min(100, repeat));
680
        for (int i = 1; i < repeat; i++) {
681
          _linesController.add(_lastLine);
682
        }
683 684 685
      }
    } else {
      _lastLine = _filterDeviceLine(line);
686
      if (_lastLine != null) {
687
        _linesController.add(_lastLine);
688
      }
689
    }
690 691 692
  }

  String _filterSystemLog(String string) {
693
    final Match match = _mapRegex.matchAsPrefix(string);
694 695 696 697
    return match == null ? string : '${match.group(1)}: ${match.group(2)}';
  }

  void _onSystemLine(String line) {
698
    printTrace('[SYS LOG] $line');
699
    if (!_flutterRunnerRegex.hasMatch(line)) {
700
      return;
701
    }
702

703
    final String filteredLine = _filterSystemLog(line);
704
    if (filteredLine == null) {
705
      return;
706
    }
707

Devon Carew's avatar
Devon Carew committed
708
    _linesController.add(filteredLine);
709 710
  }

Devon Carew's avatar
Devon Carew committed
711 712 713
  void _stop() {
    _deviceProcess?.kill();
    _systemProcess?.kill();
714 715
  }
}
716 717

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

  int i = 0;
722
  while (i < v1Fragments.length && i < v2Fragments.length) {
723 724
    final int v1Fragment = v1Fragments[i];
    final int v2Fragment = v2Fragments[i];
725
    if (v1Fragment != v2Fragment) {
726
      return v1Fragment.compareTo(v2Fragment);
727
    }
728
    i += 1;
729 730 731 732 733 734 735 736 737 738 739 740 741
  }
  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 =
742
    RegExp(r'com.apple.CoreSimulator.SimDeviceType.iPhone-(\d+)(.*)');
743 744

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

748 749
  final int v1 = int.parse(m1[1]);
  final int v2 = int.parse(m2[1]);
750

751
  if (v1 != v2) {
752
    return v1.compareTo(v2);
753
  }
754 755

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

758 759
  final int q1 = qualifiers.indexOf(m1[2]);
  final int q2 = qualifiers.indexOf(m2[2]);
760 761
  return q1.compareTo(q2);
}
762 763 764 765 766 767 768 769

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

  final IOSSimulator device;

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

770
  @override
771 772 773 774
  List<ForwardedPort> get forwardedPorts {
    return _ports;
  }

775
  @override
776
  Future<int> forward(int devicePort, { int hostPort }) async {
777
    if (hostPort == null || hostPort == 0) {
778 779 780
      hostPort = devicePort;
    }
    assert(devicePort == hostPort);
781
    _ports.add(ForwardedPort(devicePort, hostPort));
782 783 784
    return hostPort;
  }

785
  @override
786
  Future<void> unforward(ForwardedPort forwardedPort) async {
787 788 789
    _ports.remove(forwardedPort);
  }
}