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