simulators.dart 22.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

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

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

class IOSSimulators extends PollingDeviceDiscovery {
29
  IOSSimulators() : super('iOS simulators');
30

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

34
  @override
35
  bool get canListAnything => iosWorkflow.canListDevices;
36

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

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

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

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

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

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

80
    final List<String> command = <String>[_xcrunPath, 'simctl', 'list', '--json', section.name];
81
    printTrace(command.join(' '));
82
    final ProcessResult results = processManager.runSync(command);
83 84
    if (results.exitCode != 0) {
      printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
85
      return <String, Map<String, dynamic>>{};
86 87
    }

88
    return json.decode(results.stdout)[section.name];
89 90 91 92
  }

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

95
    final Map<String, dynamic> devicesSection = _list(SimControlListSection.devices);
96 97

    for (String deviceCategory in devicesSection.keys) {
98
      final List<dynamic> devicesData = devicesSection[deviceCategory];
99
      for (Map<String, dynamic> data in devicesData.map<Map<String, dynamic>>(castStringKeyedMap)) {
100
        devices.add(SimDevice(deviceCategory, data));
101 102 103 104 105 106 107
      }
    }

    return devices;
  }

  /// Returns all the connected simulator devices.
108
  List<SimDevice> getConnectedDevices() {
109 110 111
    return getDevices().where((SimDevice device) => device.isBooted).toList();
  }

112
  Future<bool> isInstalled(String deviceId, String appId) {
113
    return exitsHappyAsync(<String>[
114 115 116
      _xcrunPath,
      'simctl',
      'get_app_container',
117
      deviceId,
118 119 120 121
      appId,
    ]);
  }

122
  Future<RunResult> install(String deviceId, String appPath) {
123 124 125 126 127 128 129
    Future<RunResult> result;
    try {
      result = runCheckedAsync(<String>[_xcrunPath, 'simctl', 'install', deviceId, appPath]);
    } on ProcessException catch (exception) {
      throwToolExit('Unable to install $appPath on $deviceId:\n$exception');
    }
    return result;
130 131
  }

132
  Future<RunResult> uninstall(String deviceId, String appId) {
133 134 135 136 137 138 139
    Future<RunResult> result;
    try {
      result = runCheckedAsync(<String>[_xcrunPath, 'simctl', 'uninstall', deviceId, appId]);
    } on ProcessException catch (exception) {
      throwToolExit('Unable to uninstall $appId from $deviceId:\n$exception');
    }
    return result;
140 141
  }

142
  Future<RunResult> launch(String deviceId, String appIdentifier, [List<String> launchArgs]) {
143
    final List<String> args = <String>[_xcrunPath, 'simctl', 'launch', deviceId, appIdentifier];
144 145
    if (launchArgs != null)
      args.addAll(launchArgs);
146 147 148 149 150 151 152
    Future<RunResult> result;
    try {
      result = runCheckedAsync(args);
    } on ProcessException catch (exception) {
      throwToolExit('Unable to launch $appIdentifier on $deviceId:\n$exception');
    }
    return result;
153
  }
154

155
  Future<void> takeScreenshot(String deviceId, String outputPath) async {
156 157 158 159 160
    try {
      await runCheckedAsync(<String>[_xcrunPath, 'simctl', 'io', deviceId, 'screenshot', outputPath]);
    } on ProcessException catch (exception) {
      throwToolExit('Unable to take screenshot of $deviceId:\n$exception');
    }
161
  }
162 163
}

164 165
/// Enumerates all data sections of `xcrun simctl list --json` command.
class SimControlListSection {
Ian Hickson's avatar
Ian Hickson committed
166
  const SimControlListSection._(this.name);
167 168

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

170 171 172 173
  static const SimControlListSection devices = SimControlListSection._('devices');
  static const SimControlListSection devicetypes = SimControlListSection._('devicetypes');
  static const SimControlListSection runtimes = SimControlListSection._('runtimes');
  static const SimControlListSection pairs = SimControlListSection._('pairs');
174 175
}

176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
/// 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;
}

200 201 202 203
class SimDevice {
  SimDevice(this.category, this.data);

  final String category;
204
  final Map<String, dynamic> data;
205 206 207 208 209 210 211 212 213 214

  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 {
215
  IOSSimulator(String id, { this.name, this.category }) : super(id);
216

217
  @override
218 219
  final String name;

220 221
  final String category;

222
  @override
223
  Future<bool> get isLocalEmulator async => true;
224

225 226 227
  @override
  bool get supportsHotMode => true;

228
  Map<ApplicationPackage, _IOSSimulatorLogReader> _logReaders;
229
  _IOSSimulatorDevicePortForwarder _portForwarder;
230

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

233
  @override
234
  Future<bool> isAppInstalled(ApplicationPackage app) {
235
    return SimControl.instance.isInstalled(id, app.id);
236 237
  }

238
  @override
239
  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;
240

241
  @override
242
  Future<bool> installApp(ApplicationPackage app) async {
243
    try {
244
      final IOSApp iosApp = app;
245
      await SimControl.instance.install(id, iosApp.simulatorBundlePath);
246 247 248 249 250 251
      return true;
    } catch (e) {
      return false;
    }
  }

252
  @override
253
  Future<bool> uninstallApp(ApplicationPackage app) async {
254
    try {
255
      await SimControl.instance.uninstall(id, app.id);
256 257 258 259 260 261
      return true;
    } catch (e) {
      return false;
    }
  }

262 263
  @override
  bool isSupported() {
264
    if (!platform.isMacOS) {
265
      _supportMessage = 'iOS devices require a Mac host machine.';
266 267 268
      return false;
    }

269 270
    // Check if the device is part of a blacklisted category.
    // We do not yet support WatchOS or tvOS devices.
271
    final RegExp blacklist = RegExp(r'Apple (TV|Watch)', caseSensitive: false);
272
    if (blacklist.hasMatch(name)) {
273
      _supportMessage = 'Flutter does not support Apple TV or Apple Watch.';
274 275 276
      return false;
    }
    return true;
277 278 279 280 281 282
  }

  String _supportMessage;

  @override
  String supportMessage() {
283
    if (isSupported())
284
      return 'Supported';
285

286
    return _supportMessage != null ? _supportMessage : 'Unknown';
287 288 289
  }

  @override
Devon Carew's avatar
Devon Carew committed
290
  Future<LaunchResult> startApp(
291
    ApplicationPackage package, {
292 293
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
294
    DebuggingOptions debuggingOptions,
295
    Map<String, dynamic> platformArgs,
296 297 298 299
    bool prebuiltApplication = false,
    bool applicationNeedsRebuild = false,
    bool usesTerminalUi = true,
    bool ipv6 = false,
300
  }) async {
301
    if (!prebuiltApplication) {
302
      printTrace('Building ${package.name} for $id.');
303

304
      try {
305
        await _setupUpdatedApplicationBundle(package, debuggingOptions.buildInfo, mainPath, usesTerminalUi);
306 307
      } on ToolExit catch (e) {
        printError(e.message);
308
        return LaunchResult.failed();
309
      }
310
    } else {
311
      if (!await installApp(package))
312
        return LaunchResult.failed();
313
    }
314

315
    // Prepare launch arguments.
316
    final List<String> args = <String>['--enable-dart-profiling'];
317

Devon Carew's avatar
Devon Carew committed
318
    if (debuggingOptions.debuggingEnabled) {
319
      if (debuggingOptions.buildInfo.isDebug)
320
        args.add('--enable-checked-mode');
Devon Carew's avatar
Devon Carew committed
321
      if (debuggingOptions.startPaused)
322
        args.add('--start-paused');
323 324
      if (debuggingOptions.skiaDeterministicRendering)
        args.add('--skia-deterministic-rendering');
325 326
      if (debuggingOptions.useTestFonts)
        args.add('--use-test-fonts');
327
      final int observatoryPort = debuggingOptions.observatoryPort ?? 0;
328
      args.add('--observatory-port=$observatoryPort');
Devon Carew's avatar
Devon Carew committed
329
    }
330

331 332
    ProtocolDiscovery observatoryDiscovery;
    if (debuggingOptions.debuggingEnabled)
333
      observatoryDiscovery = ProtocolDiscovery.observatory(
334
          getLogReader(app: package), ipv6: ipv6);
335

336
    // Launch the updated application in the simulator.
337
    try {
338
      await SimControl.instance.launch(id, package.id, args);
339 340
    } catch (error) {
      printError('$error');
341
      return LaunchResult.failed();
342 343
    }

Devon Carew's avatar
Devon Carew committed
344
    if (!debuggingOptions.debuggingEnabled) {
345
      return LaunchResult.succeeded();
346
    }
Devon Carew's avatar
Devon Carew committed
347

348 349 350 351 352
    // 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 {
353
      final Uri deviceUri = await observatoryDiscovery.uri;
354
      return LaunchResult.succeeded(observatoryUri: deviceUri);
355 356
    } catch (error) {
      printError('Error waiting for a debug connection: $error');
357
      return LaunchResult.failed();
358
    } finally {
359
      await observatoryDiscovery.cancel();
Devon Carew's avatar
Devon Carew committed
360
    }
361 362
  }

363
  Future<void> _setupUpdatedApplicationBundle(ApplicationPackage app, BuildInfo buildInfo, String mainPath, bool usesTerminalUi) async {
364
    await _sideloadUpdatedAssetsForInstalledApplicationBundle(app, buildInfo, mainPath);
365

366
    // Step 1: Build the Xcode project.
367
    // The build mode for the simulator is always debug.
368

369
    final BuildInfo debugBuildInfo = BuildInfo(BuildMode.debug, buildInfo.flavor,
370
        trackWidgetCreation: buildInfo.trackWidgetCreation,
371 372
        extraFrontEndOptions: buildInfo.extraFrontEndOptions,
        extraGenSnapshotOptions: buildInfo.extraGenSnapshotOptions,
373
        buildSharedLibrary: buildInfo.buildSharedLibrary);
374

375 376 377 378 379 380 381
    final XcodeBuildResult buildResult = await buildXcodeProject(
      app: app,
      buildInfo: debugBuildInfo,
      targetOverride: mainPath,
      buildForDevice: false,
      usesTerminalUi: usesTerminalUi,
    );
382 383
    if (!buildResult.success)
      throwToolExit('Could not build the application for the simulator.');
384 385

    // Step 2: Assert that the Xcode project was successfully built.
386 387 388
    final IOSApp iosApp = app;
    final Directory bundle = fs.directory(iosApp.simulatorBundlePath);
    final bool bundleExists = bundle.existsSync();
389 390
    if (!bundleExists)
      throwToolExit('Could not find the built application bundle at ${bundle.path}.');
391 392

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

396
  Future<void> _sideloadUpdatedAssetsForInstalledApplicationBundle(ApplicationPackage app, BuildInfo buildInfo, String mainPath) {
397
    // Run compiler to produce kernel file for the application.
398
    return bundle.build(
399
      mainPath: mainPath,
400
      precompiledSnapshot: false,
401 402 403
      trackWidgetCreation: buildInfo.trackWidgetCreation,
    );
  }
404

405 406 407 408 409 410 411
  @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 {
412 413 414
    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');
415 416 417
  }

  @override
418
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
419

420
  @override
421
  Future<String> get sdkNameAndVersion async => category;
422

423
  final RegExp _iosSdkRegExp = RegExp(r'iOS( |-)(\d+)');
424 425 426

  Future<int> get sdkMajorVersion async {
    final Match sdkMatch = _iosSdkRegExp.firstMatch(await sdkNameAndVersion);
427
    return int.parse(sdkMatch?.group(2) ?? 11);
428 429
  }

430
  @override
431
  DeviceLogReader getLogReader({ApplicationPackage app}) {
432
    assert(app is IOSApp);
433
    _logReaders ??= <ApplicationPackage, _IOSSimulatorLogReader>{};
434
    return _logReaders.putIfAbsent(app, () => _IOSSimulatorLogReader(this, app));
435
  }
436

437
  @override
438
  DevicePortForwarder get portForwarder => _portForwarder ??= _IOSSimulatorDevicePortForwarder(this);
439

440
  @override
441
  void clearLogs() {
442
    final File logFile = fs.file(logFilePath);
443
    if (logFile.existsSync()) {
444
      final RandomAccessFile randomFile = logFile.openSync(mode: FileMode.write);
445 446 447 448 449
      randomFile.truncateSync(0);
      randomFile.closeSync();
    }
  }

450
  Future<void> ensureLogsExists() async {
451 452 453 454 455
    if (await sdkMajorVersion < 11) {
      final File logFile = fs.file(logFilePath);
      if (!logFile.existsSync())
        logFile.writeAsBytesSync(<int>[]);
    }
456
  }
Devon Carew's avatar
Devon Carew committed
457

458
  bool get _xcodeVersionSupportsScreenshot {
459
    return xcode.majorVersion > 8 || (xcode.majorVersion == 8 && xcode.minorVersion >= 2);
460
  }
Devon Carew's avatar
Devon Carew committed
461 462

  @override
463
  bool get supportsScreenshot => _xcodeVersionSupportsScreenshot;
Devon Carew's avatar
Devon Carew committed
464

465
  @override
466
  Future<void> takeScreenshot(File outputFile) {
467
    return SimControl.instance.takeScreenshot(id, outputFile.path);
Devon Carew's avatar
Devon Carew committed
468
  }
469 470
}

471 472 473
/// 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.
474
  if (await device.sdkMajorVersion < 11)
475 476 477 478 479 480 481 482 483 484 485
    return runCommand(<String>['tail', '-n', '0', '-F', device.logFilePath]);

  // 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)
  return runCommand(<String>[
    '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.
486
  if (await device.sdkMajorVersion < 11)
487 488 489 490 491 492
    return runCommand(<String>['tail', '-n', '0', '-F', '/private/var/log/system.log']);

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

493
class _IOSSimulatorLogReader extends DeviceLogReader {
494
  _IOSSimulatorLogReader(this.device, IOSApp app) {
495
    _linesController = StreamController<String>.broadcast(
496
      onListen: _start,
Devon Carew's avatar
Devon Carew committed
497 498
      onCancel: _stop
    );
499
    _appName = app == null ? null : app.name.replaceAll('.app', '');
Devon Carew's avatar
Devon Carew committed
500
  }
501 502 503

  final IOSSimulator device;

504 505
  String _appName;

Devon Carew's avatar
Devon Carew committed
506
  StreamController<String> _linesController;
507

Devon Carew's avatar
Devon Carew committed
508
  // We log from two files: the device and the system log.
509 510
  Process _deviceProcess;
  Process _systemProcess;
511

512
  @override
Devon Carew's avatar
Devon Carew committed
513
  Stream<String> get logLines => _linesController.stream;
514

515
  @override
516 517
  String get name => device.name;

518
  Future<void> _start() async {
519
    // Device log.
520
    await device.ensureLogsExists();
521
    _deviceProcess = await launchDeviceLogTool(device);
522 523
    _deviceProcess.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onDeviceLine);
    _deviceProcess.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onDeviceLine);
524 525 526

    // Track system.log crashes.
    // ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
527 528
    _systemProcess = await launchSystemLogTool(device);
    if (_systemProcess != null) {
529 530
      _systemProcess.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onSystemLine);
      _systemProcess.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onSystemLine);
531
    }
532

533 534 535
    // We don't want to wait for the process or its callback. Best effort
    // cleanup in the callback.
    _deviceProcess.exitCode.whenComplete(() { // ignore: unawaited_futures
Devon Carew's avatar
Devon Carew committed
536 537 538
      if (_linesController.hasListener)
        _linesController.close();
    });
539 540 541
  }

  // Match the log prefix (in order to shorten it):
542 543
  // * 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/
544
  static final RegExp _mapRegex = RegExp(r'\S+ +\S+ +\S+ +(\S+ +)?(\S+)\[\d+\]\)?: (\(.*?\))? *(.*)$');
545 546

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

550
  static final RegExp _flutterRunnerRegex = RegExp(r' FlutterRunner\[\d+\] ');
551 552

  String _filterDeviceLine(String string) {
553
    final Match match = _mapRegex.matchAsPrefix(string);
554
    if (match != null) {
555 556 557
      final String category = match.group(2);
      final String tag = match.group(3);
      final String content = match.group(4);
558

559
      // Filter out non-Flutter originated noise from the engine.
560
      if (_appName != null && category != _appName)
561 562
        return null;

563
      if (tag != null && tag != '(Flutter)')
564 565
        return null;

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

570
      // assertion failed: 15G1212 13E230: libxpc.dylib + 57882 [66C28065-C9DB-3C8E-926F-5A40210A6D1B]: 0x7d
571
      if (content.startsWith('assertion failed: ') && content.contains(' libxpc.dylib '))
572 573
        return null;

574
      if (_appName == null)
575 576
        return '$category: $content';
      else if (category == _appName)
577
        return content;
578 579

      return null;
580
    }
581

582 583 584 585 586 587
    if (string.startsWith('Filtering the log data using '))
      return null;

    if (string.startsWith('Timestamp                       (process)[PID]'))
      return null;

588
    if (_lastMessageSingleRegex.matchAsPrefix(string) != null)
589
      return null;
590

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

594 595 596
    return string;
  }

597 598
  String _lastLine;

599
  void _onDeviceLine(String line) {
600
    printTrace('[DEVICE LOG] $line');
601
    final Match multi = _lastMessageMultipleRegex.matchAsPrefix(line);
602 603 604 605 606

    if (multi != null) {
      if (_lastLine != null) {
        int repeat = int.parse(multi.group(1));
        repeat = math.max(0, math.min(100, repeat));
607
        for (int i = 1; i < repeat; i++)
608 609 610 611 612 613 614
          _linesController.add(_lastLine);
      }
    } else {
      _lastLine = _filterDeviceLine(line);
      if (_lastLine != null)
        _linesController.add(_lastLine);
    }
615 616 617
  }

  String _filterSystemLog(String string) {
618
    final Match match = _mapRegex.matchAsPrefix(string);
619 620 621 622
    return match == null ? string : '${match.group(1)}: ${match.group(2)}';
  }

  void _onSystemLine(String line) {
623
    printTrace('[SYS LOG] $line');
624 625 626
    if (!_flutterRunnerRegex.hasMatch(line))
      return;

627
    final String filteredLine = _filterSystemLog(line);
628 629
    if (filteredLine == null)
      return;
630

Devon Carew's avatar
Devon Carew committed
631
    _linesController.add(filteredLine);
632 633
  }

Devon Carew's avatar
Devon Carew committed
634 635 636
  void _stop() {
    _deviceProcess?.kill();
    _systemProcess?.kill();
637 638
  }
}
639 640

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

  int i = 0;
645
  while (i < v1Fragments.length && i < v2Fragments.length) {
646 647
    final int v1Fragment = v1Fragments[i];
    final int v2Fragment = v2Fragments[i];
648 649
    if (v1Fragment != v2Fragment)
      return v1Fragment.compareTo(v2Fragment);
650
    i += 1;
651 652 653 654 655 656 657 658 659 660 661 662 663
  }
  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 =
664
    RegExp(r'com.apple.CoreSimulator.SimDeviceType.iPhone-(\d+)(.*)');
665 666

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

670 671
  final int v1 = int.parse(m1[1]);
  final int v2 = int.parse(m2[1]);
672 673 674 675 676

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

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

679 680
  final int q1 = qualifiers.indexOf(m1[2]);
  final int q2 = qualifiers.indexOf(m2[2]);
681 682
  return q1.compareTo(q2);
}
683 684 685 686 687 688 689 690

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

  final IOSSimulator device;

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

691
  @override
692 693 694 695
  List<ForwardedPort> get forwardedPorts {
    return _ports;
  }

696
  @override
697
  Future<int> forward(int devicePort, {int hostPort}) async {
698
    if (hostPort == null || hostPort == 0) {
699 700 701
      hostPort = devicePort;
    }
    assert(devicePort == hostPort);
702
    _ports.add(ForwardedPort(devicePort, hostPort));
703 704 705
    return hostPort;
  }

706
  @override
707
  Future<void> unforward(ForwardedPort forwardedPort) async {
708 709 710
    _ports.remove(forwardedPort);
  }
}