• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:math' as math;
7
8import '../application_package.dart';
9import '../base/common.dart';
10import '../base/context.dart';
11import '../base/file_system.dart';
12import '../base/io.dart';
13import '../base/platform.dart';
14import '../base/process.dart';
15import '../base/process_manager.dart';
16import '../base/utils.dart';
17import '../build_info.dart';
18import '../bundle.dart';
19import '../convert.dart';
20import '../device.dart';
21import '../globals.dart';
22import '../macos/xcode.dart';
23import '../project.dart';
24import '../protocol_discovery.dart';
25import 'ios_workflow.dart';
26import 'mac.dart';
27import 'plist_parser.dart';
28
29const String _xcrunPath = '/usr/bin/xcrun';
30const String iosSimulatorId = 'apple_ios_simulator';
31
32class 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
45class IOSSimulatorUtils {
46  /// 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    final List<SimDevice> connected = await SimControl.instance.getConnectedDevices();
54    return connected.map<IOSSimulator>((SimDevice device) {
55      return IOSSimulator(device.udid, name: device.name, simulatorCategory: device.category);
56    }).toList();
57  }
58}
59
60/// A wrapper around the `simctl` command line tool.
61class SimControl {
62  /// Returns [SimControl] active in the current app context (i.e. zone).
63  static SimControl get instance => context.get<SimControl>();
64
65  /// Runs `simctl list --json` and returns the JSON of the corresponding
66  /// [section].
67  Future<Map<String, dynamic>> _list(SimControlListSection section) async {
68    // Sample output from `simctl list --json`:
69    //
70    // {
71    //   "devicetypes": { ... },
72    //   "runtimes": { ... },
73    //   "devices" : {
74    //     "com.apple.CoreSimulator.SimRuntime.iOS-8-2" : [
75    //       {
76    //         "state" : "Shutdown",
77    //         "availability" : " (unavailable, runtime profile not found)",
78    //         "name" : "iPhone 4s",
79    //         "udid" : "1913014C-6DCB-485D-AC6B-7CD76D322F5B"
80    //       },
81    //       ...
82    //   },
83    //   "pairs": { ... },
84
85    final List<String> command = <String>[_xcrunPath, 'simctl', 'list', '--json', section.name];
86    printTrace(command.join(' '));
87    final ProcessResult results = await processManager.run(command);
88    if (results.exitCode != 0) {
89      printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
90      return <String, Map<String, dynamic>>{};
91    }
92    try {
93      final Object decodeResult = json.decode(results.stdout?.toString())[section.name];
94      if (decodeResult is Map<String, dynamic>) {
95        return decodeResult;
96      }
97      printError('simctl returned unexpected JSON response: ${results.stdout}');
98      return <String, dynamic>{};
99    } on FormatException {
100      // We failed to parse the simctl output, or it returned junk.
101      // One known message is "Install Started" isn't valid JSON but is
102      // returned sometimes.
103      printError('simctl returned non-JSON response: ${results.stdout}');
104      return <String, dynamic>{};
105    }
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 (String deviceCategory in devicesSection.keys) {
115      final Object devicesData = devicesSection[deviceCategory];
116      if (devicesData != null && devicesData is List<dynamic>) {
117        for (Map<String, dynamic> data in devicesData.map<Map<String, dynamic>>(castStringKeyedMap)) {
118          devices.add(SimDevice(deviceCategory, data));
119        }
120      }
121    }
122
123    return devices;
124  }
125
126  /// Returns all the connected simulator devices.
127  Future<List<SimDevice>> getConnectedDevices() async {
128    final List<SimDevice> simDevices = await getDevices();
129    return simDevices.where((SimDevice device) => device.isBooted).toList();
130  }
131
132  Future<bool> isInstalled(String deviceId, String appId) {
133    return exitsHappyAsync(<String>[
134      _xcrunPath,
135      'simctl',
136      'get_app_container',
137      deviceId,
138      appId,
139    ]);
140  }
141
142  Future<RunResult> install(String deviceId, String appPath) {
143    Future<RunResult> result;
144    try {
145      result = runCheckedAsync(<String>[_xcrunPath, 'simctl', 'install', deviceId, appPath]);
146    } on ProcessException catch (exception) {
147      throwToolExit('Unable to install $appPath on $deviceId:\n$exception');
148    }
149    return result;
150  }
151
152  Future<RunResult> uninstall(String deviceId, String appId) {
153    Future<RunResult> result;
154    try {
155      result = runCheckedAsync(<String>[_xcrunPath, 'simctl', 'uninstall', deviceId, appId]);
156    } on ProcessException catch (exception) {
157      throwToolExit('Unable to uninstall $appId from $deviceId:\n$exception');
158    }
159    return result;
160  }
161
162  Future<RunResult> launch(String deviceId, String appIdentifier, [ List<String> launchArgs ]) {
163    Future<RunResult> result;
164    try {
165      result = runCheckedAsync(<String>[
166        _xcrunPath,
167        'simctl',
168        'launch',
169        deviceId,
170        appIdentifier,
171        ...?launchArgs,
172      ]);
173    } on ProcessException catch (exception) {
174      throwToolExit('Unable to launch $appIdentifier on $deviceId:\n$exception');
175    }
176    return result;
177  }
178
179  Future<void> takeScreenshot(String deviceId, String outputPath) async {
180    try {
181      await runCheckedAsync(<String>[_xcrunPath, 'simctl', 'io', deviceId, 'screenshot', outputPath]);
182    } on ProcessException catch (exception) {
183      throwToolExit('Unable to take screenshot of $deviceId:\n$exception');
184    }
185  }
186}
187
188/// Enumerates all data sections of `xcrun simctl list --json` command.
189class SimControlListSection {
190  const SimControlListSection._(this.name);
191
192  final String name;
193
194  static const SimControlListSection devices = SimControlListSection._('devices');
195  static const SimControlListSection devicetypes = SimControlListSection._('devicetypes');
196  static const SimControlListSection runtimes = SimControlListSection._('runtimes');
197  static const SimControlListSection pairs = SimControlListSection._('pairs');
198}
199
200/// A simulated device type.
201///
202/// Simulated device types can be listed using the command
203/// `xcrun simctl list devicetypes`.
204class SimDeviceType {
205  SimDeviceType(this.name, this.identifier);
206
207  /// The name of the device type.
208  ///
209  /// Examples:
210  ///
211  ///     "iPhone 6s"
212  ///     "iPhone 6 Plus"
213  final String name;
214
215  /// The identifier of the device type.
216  ///
217  /// Examples:
218  ///
219  ///     "com.apple.CoreSimulator.SimDeviceType.iPhone-6s"
220  ///     "com.apple.CoreSimulator.SimDeviceType.iPhone-6-Plus"
221  final String identifier;
222}
223
224class SimDevice {
225  SimDevice(this.category, this.data);
226
227  final String category;
228  final Map<String, dynamic> data;
229
230  String get state => data['state']?.toString();
231  String get availability => data['availability']?.toString();
232  String get name => data['name']?.toString();
233  String get udid => data['udid']?.toString();
234
235  bool get isBooted => state == 'Booted';
236}
237
238class IOSSimulator extends Device {
239  IOSSimulator(String id, { this.name, this.simulatorCategory }) : super(
240      id,
241      category: Category.mobile,
242      platformType: PlatformType.ios,
243      ephemeral: true,
244  );
245
246  @override
247  final String name;
248
249  final String simulatorCategory;
250
251  @override
252  Future<bool> get isLocalEmulator async => true;
253
254  @override
255  Future<String> get emulatorId async => iosSimulatorId;
256
257  @override
258  bool get supportsHotReload => true;
259
260  @override
261  bool get supportsHotRestart => true;
262
263  Map<ApplicationPackage, _IOSSimulatorLogReader> _logReaders;
264  _IOSSimulatorDevicePortForwarder _portForwarder;
265
266  String get xcrunPath => fs.path.join('/usr', 'bin', 'xcrun');
267
268  @override
269  Future<bool> isAppInstalled(ApplicationPackage app) {
270    return SimControl.instance.isInstalled(id, app.id);
271  }
272
273  @override
274  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;
275
276  @override
277  Future<bool> installApp(covariant IOSApp app) async {
278    try {
279      final IOSApp iosApp = app;
280      await SimControl.instance.install(id, iosApp.simulatorBundlePath);
281      return true;
282    } catch (e) {
283      return false;
284    }
285  }
286
287  @override
288  Future<bool> uninstallApp(ApplicationPackage app) async {
289    try {
290      await SimControl.instance.uninstall(id, app.id);
291      return true;
292    } catch (e) {
293      return false;
294    }
295  }
296
297  @override
298  bool isSupported() {
299    if (!platform.isMacOS) {
300      _supportMessage = 'iOS devices require a Mac host machine.';
301      return false;
302    }
303
304    // Check if the device is part of a blacklisted category.
305    // We do not yet support WatchOS or tvOS devices.
306    final RegExp blacklist = RegExp(r'Apple (TV|Watch)', caseSensitive: false);
307    if (blacklist.hasMatch(name)) {
308      _supportMessage = 'Flutter does not support Apple TV or Apple Watch.';
309      return false;
310    }
311    return true;
312  }
313
314  String _supportMessage;
315
316  @override
317  String supportMessage() {
318    if (isSupported())
319      return 'Supported';
320
321    return _supportMessage ?? 'Unknown';
322  }
323
324  @override
325  Future<LaunchResult> startApp(
326    covariant IOSApp package, {
327    String mainPath,
328    String route,
329    DebuggingOptions debuggingOptions,
330    Map<String, dynamic> platformArgs,
331    bool prebuiltApplication = false,
332    bool usesTerminalUi = true,
333    bool ipv6 = false,
334  }) async {
335    if (!prebuiltApplication && package is BuildableIOSApp) {
336      printTrace('Building ${package.name} for $id.');
337
338      try {
339        await _setupUpdatedApplicationBundle(package, debuggingOptions.buildInfo, mainPath, usesTerminalUi);
340      } on ToolExit catch (e) {
341        printError(e.message);
342        return LaunchResult.failed();
343      }
344    } else {
345      if (!await installApp(package))
346        return LaunchResult.failed();
347    }
348
349    // Prepare launch arguments.
350    final List<String> args = <String>['--enable-dart-profiling'];
351
352    if (debuggingOptions.debuggingEnabled) {
353      if (debuggingOptions.buildInfo.isDebug)
354        args.addAll(<String>[
355          '--enable-checked-mode',
356          '--verify-entry-points',
357        ]);
358      if (debuggingOptions.startPaused)
359        args.add('--start-paused');
360      if (debuggingOptions.disableServiceAuthCodes)
361        args.add('--disable-service-auth-codes');
362      if (debuggingOptions.skiaDeterministicRendering)
363        args.add('--skia-deterministic-rendering');
364      if (debuggingOptions.useTestFonts)
365        args.add('--use-test-fonts');
366      final int observatoryPort = debuggingOptions.observatoryPort ?? 0;
367      args.add('--observatory-port=$observatoryPort');
368    }
369
370    ProtocolDiscovery observatoryDiscovery;
371    if (debuggingOptions.debuggingEnabled)
372      observatoryDiscovery = ProtocolDiscovery.observatory(
373          getLogReader(app: package), ipv6: ipv6);
374
375    // Launch the updated application in the simulator.
376    try {
377      // Use the built application's Info.plist to get the bundle identifier,
378      // which should always yield the correct value and does not require
379      // parsing the xcodeproj or configuration files.
380      // See https://github.com/flutter/flutter/issues/31037 for more information.
381      final String plistPath = fs.path.join(package.simulatorBundlePath, 'Info.plist');
382      final String bundleIdentifier = PlistParser.instance.getValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
383
384      await SimControl.instance.launch(id, bundleIdentifier, args);
385    } catch (error) {
386      printError('$error');
387      return LaunchResult.failed();
388    }
389
390    if (!debuggingOptions.debuggingEnabled) {
391      return LaunchResult.succeeded();
392    }
393
394    // Wait for the service protocol port here. This will complete once the
395    // device has printed "Observatory is listening on..."
396    printTrace('Waiting for observatory port to be available...');
397
398    try {
399      final Uri deviceUri = await observatoryDiscovery.uri;
400      return LaunchResult.succeeded(observatoryUri: deviceUri);
401    } catch (error) {
402      printError('Error waiting for a debug connection: $error');
403      return LaunchResult.failed();
404    } finally {
405      await observatoryDiscovery.cancel();
406    }
407  }
408
409  Future<void> _setupUpdatedApplicationBundle(covariant BuildableIOSApp app, BuildInfo buildInfo, String mainPath, bool usesTerminalUi) async {
410    await _sideloadUpdatedAssetsForInstalledApplicationBundle(app, buildInfo, mainPath);
411
412    // Step 1: Build the Xcode project.
413    // The build mode for the simulator is always debug.
414
415    final BuildInfo debugBuildInfo = BuildInfo(BuildMode.debug, buildInfo.flavor,
416        trackWidgetCreation: buildInfo.trackWidgetCreation,
417        extraFrontEndOptions: buildInfo.extraFrontEndOptions,
418        extraGenSnapshotOptions: buildInfo.extraGenSnapshotOptions);
419
420    final XcodeBuildResult buildResult = await buildXcodeProject(
421      app: app,
422      buildInfo: debugBuildInfo,
423      targetOverride: mainPath,
424      buildForDevice: false,
425      usesTerminalUi: usesTerminalUi,
426    );
427    if (!buildResult.success)
428      throwToolExit('Could not build the application for the simulator.');
429
430    // Step 2: Assert that the Xcode project was successfully built.
431    final Directory bundle = fs.directory(app.simulatorBundlePath);
432    final bool bundleExists = bundle.existsSync();
433    if (!bundleExists)
434      throwToolExit('Could not find the built application bundle at ${bundle.path}.');
435
436    // Step 3: Install the updated bundle to the simulator.
437    await SimControl.instance.install(id, fs.path.absolute(bundle.path));
438  }
439
440  Future<void> _sideloadUpdatedAssetsForInstalledApplicationBundle(ApplicationPackage app, BuildInfo buildInfo, String mainPath) {
441    // Run compiler to produce kernel file for the application.
442    return BundleBuilder().build(
443      mainPath: mainPath,
444      precompiledSnapshot: false,
445      trackWidgetCreation: buildInfo.trackWidgetCreation,
446    );
447  }
448
449  @override
450  Future<bool> stopApp(ApplicationPackage app) async {
451    // Currently we don't have a way to stop an app running on iOS.
452    return false;
453  }
454
455  String get logFilePath {
456    return platform.environment.containsKey('IOS_SIMULATOR_LOG_FILE_PATH')
457        ? platform.environment['IOS_SIMULATOR_LOG_FILE_PATH'].replaceAll('%{id}', id)
458        : fs.path.join(homeDirPath, 'Library', 'Logs', 'CoreSimulator', id, 'system.log');
459  }
460
461  @override
462  Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
463
464  @override
465  Future<String> get sdkNameAndVersion async => simulatorCategory;
466
467  final RegExp _iosSdkRegExp = RegExp(r'iOS( |-)(\d+)');
468
469  Future<int> get sdkMajorVersion async {
470    final Match sdkMatch = _iosSdkRegExp.firstMatch(await sdkNameAndVersion);
471    return int.parse(sdkMatch?.group(2) ?? '11');
472  }
473
474  @override
475  DeviceLogReader getLogReader({ covariant IOSApp app }) {
476    assert(app is IOSApp);
477    _logReaders ??= <ApplicationPackage, _IOSSimulatorLogReader>{};
478    return _logReaders.putIfAbsent(app, () => _IOSSimulatorLogReader(this, app));
479  }
480
481  @override
482  DevicePortForwarder get portForwarder => _portForwarder ??= _IOSSimulatorDevicePortForwarder(this);
483
484  @override
485  void clearLogs() {
486    final File logFile = fs.file(logFilePath);
487    if (logFile.existsSync()) {
488      final RandomAccessFile randomFile = logFile.openSync(mode: FileMode.write);
489      randomFile.truncateSync(0);
490      randomFile.closeSync();
491    }
492  }
493
494  Future<void> ensureLogsExists() async {
495    if (await sdkMajorVersion < 11) {
496      final File logFile = fs.file(logFilePath);
497      if (!logFile.existsSync())
498        logFile.writeAsBytesSync(<int>[]);
499    }
500  }
501
502  bool get _xcodeVersionSupportsScreenshot {
503    return xcode.majorVersion > 8 || (xcode.majorVersion == 8 && xcode.minorVersion >= 2);
504  }
505
506  @override
507  bool get supportsScreenshot => _xcodeVersionSupportsScreenshot;
508
509  @override
510  Future<void> takeScreenshot(File outputFile) {
511    return SimControl.instance.takeScreenshot(id, outputFile.path);
512  }
513
514  @override
515  bool isSupportedForProject(FlutterProject flutterProject) {
516    return flutterProject.ios.existsSync();
517  }
518}
519
520/// Launches the device log reader process on the host.
521Future<Process> launchDeviceLogTool(IOSSimulator device) async {
522  // Versions of iOS prior to iOS 11 log to the simulator syslog file.
523  if (await device.sdkMajorVersion < 11)
524    return runCommand(<String>['tail', '-n', '0', '-F', device.logFilePath]);
525
526  // For iOS 11 and above, use /usr/bin/log to tail process logs.
527  // Run in interactive mode (via script), otherwise /usr/bin/log buffers in 4k chunks. (radar: 34420207)
528  return runCommand(<String>[
529    'script', '/dev/null', '/usr/bin/log', 'stream', '--style', 'syslog', '--predicate', 'processImagePath CONTAINS "${device.id}"',
530  ]);
531}
532
533Future<Process> launchSystemLogTool(IOSSimulator device) async {
534  // Versions of iOS prior to 11 tail the simulator syslog file.
535  if (await device.sdkMajorVersion < 11)
536    return runCommand(<String>['tail', '-n', '0', '-F', '/private/var/log/system.log']);
537
538  // For iOS 11 and later, all relevant detail is in the device log.
539  return null;
540}
541
542class _IOSSimulatorLogReader extends DeviceLogReader {
543  _IOSSimulatorLogReader(this.device, IOSApp app) {
544    _linesController = StreamController<String>.broadcast(
545      onListen: _start,
546      onCancel: _stop,
547    );
548    _appName = app == null ? null : app.name.replaceAll('.app', '');
549  }
550
551  final IOSSimulator device;
552
553  String _appName;
554
555  StreamController<String> _linesController;
556
557  // We log from two files: the device and the system log.
558  Process _deviceProcess;
559  Process _systemProcess;
560
561  @override
562  Stream<String> get logLines => _linesController.stream;
563
564  @override
565  String get name => device.name;
566
567  Future<void> _start() async {
568    // Device log.
569    await device.ensureLogsExists();
570    _deviceProcess = await launchDeviceLogTool(device);
571    _deviceProcess.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onDeviceLine);
572    _deviceProcess.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onDeviceLine);
573
574    // Track system.log crashes.
575    // ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
576    _systemProcess = await launchSystemLogTool(device);
577    if (_systemProcess != null) {
578      _systemProcess.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onSystemLine);
579      _systemProcess.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(_onSystemLine);
580    }
581
582    // We don't want to wait for the process or its callback. Best effort
583    // cleanup in the callback.
584    unawaited(_deviceProcess.exitCode.whenComplete(() {
585      if (_linesController.hasListener)
586        _linesController.close();
587    }));
588  }
589
590  // Match the log prefix (in order to shorten it):
591  // * Xcode 8: Sep 13 15:28:51 cbracken-macpro localhost Runner[37195]: (Flutter) Observatory listening on http://127.0.0.1:57701/
592  // * Xcode 9: 2017-09-13 15:26:57.228948-0700  localhost Runner[37195]: (Flutter) Observatory listening on http://127.0.0.1:57701/
593  static final RegExp _mapRegex = RegExp(r'\S+ +\S+ +\S+ +(\S+ +)?(\S+)\[\d+\]\)?: (\(.*?\))? *(.*)$');
594
595  // Jan 31 19:23:28 --- last message repeated 1 time ---
596  static final RegExp _lastMessageSingleRegex = RegExp(r'\S+ +\S+ +\S+ --- last message repeated 1 time ---$');
597  static final RegExp _lastMessageMultipleRegex = RegExp(r'\S+ +\S+ +\S+ --- last message repeated (\d+) times ---$');
598
599  static final RegExp _flutterRunnerRegex = RegExp(r' FlutterRunner\[\d+\] ');
600
601  String _filterDeviceLine(String string) {
602    final Match match = _mapRegex.matchAsPrefix(string);
603    if (match != null) {
604      final String category = match.group(2);
605      final String tag = match.group(3);
606      final String content = match.group(4);
607
608      // Filter out non-Flutter originated noise from the engine.
609      if (_appName != null && category != _appName)
610        return null;
611
612      if (tag != null && tag != '(Flutter)')
613        return null;
614
615      // Filter out some messages that clearly aren't related to Flutter.
616      if (string.contains(': could not find icon for representation -> com.apple.'))
617        return null;
618
619      // assertion failed: 15G1212 13E230: libxpc.dylib + 57882 [66C28065-C9DB-3C8E-926F-5A40210A6D1B]: 0x7d
620      if (content.startsWith('assertion failed: ') && content.contains(' libxpc.dylib '))
621        return null;
622
623      if (_appName == null)
624        return '$category: $content';
625      else if (category == _appName)
626        return content;
627
628      return null;
629    }
630
631    if (string.startsWith('Filtering the log data using '))
632      return null;
633
634    if (string.startsWith('Timestamp                       (process)[PID]'))
635      return null;
636
637    if (_lastMessageSingleRegex.matchAsPrefix(string) != null)
638      return null;
639
640    if (RegExp(r'assertion failed: .* libxpc.dylib .* 0x7d$').matchAsPrefix(string) != null)
641      return null;
642
643    return string;
644  }
645
646  String _lastLine;
647
648  void _onDeviceLine(String line) {
649    printTrace('[DEVICE LOG] $line');
650    final Match multi = _lastMessageMultipleRegex.matchAsPrefix(line);
651
652    if (multi != null) {
653      if (_lastLine != null) {
654        int repeat = int.parse(multi.group(1));
655        repeat = math.max(0, math.min(100, repeat));
656        for (int i = 1; i < repeat; i++)
657          _linesController.add(_lastLine);
658      }
659    } else {
660      _lastLine = _filterDeviceLine(line);
661      if (_lastLine != null)
662        _linesController.add(_lastLine);
663    }
664  }
665
666  String _filterSystemLog(String string) {
667    final Match match = _mapRegex.matchAsPrefix(string);
668    return match == null ? string : '${match.group(1)}: ${match.group(2)}';
669  }
670
671  void _onSystemLine(String line) {
672    printTrace('[SYS LOG] $line');
673    if (!_flutterRunnerRegex.hasMatch(line))
674      return;
675
676    final String filteredLine = _filterSystemLog(line);
677    if (filteredLine == null)
678      return;
679
680    _linesController.add(filteredLine);
681  }
682
683  void _stop() {
684    _deviceProcess?.kill();
685    _systemProcess?.kill();
686  }
687}
688
689int compareIosVersions(String v1, String v2) {
690  final List<int> v1Fragments = v1.split('.').map<int>(int.parse).toList();
691  final List<int> v2Fragments = v2.split('.').map<int>(int.parse).toList();
692
693  int i = 0;
694  while (i < v1Fragments.length && i < v2Fragments.length) {
695    final int v1Fragment = v1Fragments[i];
696    final int v2Fragment = v2Fragments[i];
697    if (v1Fragment != v2Fragment)
698      return v1Fragment.compareTo(v2Fragment);
699    i += 1;
700  }
701  return v1Fragments.length.compareTo(v2Fragments.length);
702}
703
704/// Matches on device type given an identifier.
705///
706/// Example device type identifiers:
707///   ✓ com.apple.CoreSimulator.SimDeviceType.iPhone-5
708///   ✓ com.apple.CoreSimulator.SimDeviceType.iPhone-6
709///   ✓ com.apple.CoreSimulator.SimDeviceType.iPhone-6s-Plus
710///   ✗ com.apple.CoreSimulator.SimDeviceType.iPad-2
711///   ✗ com.apple.CoreSimulator.SimDeviceType.Apple-Watch-38mm
712final RegExp _iosDeviceTypePattern =
713    RegExp(r'com.apple.CoreSimulator.SimDeviceType.iPhone-(\d+)(.*)');
714
715int compareIphoneVersions(String id1, String id2) {
716  final Match m1 = _iosDeviceTypePattern.firstMatch(id1);
717  final Match m2 = _iosDeviceTypePattern.firstMatch(id2);
718
719  final int v1 = int.parse(m1[1]);
720  final int v2 = int.parse(m2[1]);
721
722  if (v1 != v2)
723    return v1.compareTo(v2);
724
725  // Sorted in the least preferred first order.
726  const List<String> qualifiers = <String>['-Plus', '', 's-Plus', 's'];
727
728  final int q1 = qualifiers.indexOf(m1[2]);
729  final int q2 = qualifiers.indexOf(m2[2]);
730  return q1.compareTo(q2);
731}
732
733class _IOSSimulatorDevicePortForwarder extends DevicePortForwarder {
734  _IOSSimulatorDevicePortForwarder(this.device);
735
736  final IOSSimulator device;
737
738  final List<ForwardedPort> _ports = <ForwardedPort>[];
739
740  @override
741  List<ForwardedPort> get forwardedPorts {
742    return _ports;
743  }
744
745  @override
746  Future<int> forward(int devicePort, { int hostPort }) async {
747    if (hostPort == null || hostPort == 0) {
748      hostPort = devicePort;
749    }
750    assert(devicePort == hostPort);
751    _ports.add(ForwardedPort(devicePort, hostPort));
752    return hostPort;
753  }
754
755  @override
756  Future<void> unforward(ForwardedPort forwardedPort) async {
757    _ports.remove(forwardedPort);
758  }
759}
760