blob: 7d28f4eaf05786a546ebe327dd6dc16a71e6f496 [file] [log] [blame]
// 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';
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
import '../application_package.dart';
import '../base/common.dart';
import '../base/context.dart';
import '../base/process.dart';
import '../build_configuration.dart';
import '../device.dart';
import '../flx.dart' as flx;
import '../globals.dart';
import '../service_protocol.dart';
import '../toolchain.dart';
import 'mac.dart';
const String _xcrunPath = '/usr/bin/xcrun';
/// Test device created by Flutter when no other device is available.
const String _kFlutterTestDeviceSuffix = '(Flutter)';
class IOSSimulators extends PollingDeviceDiscovery {
IOSSimulators() : super('IOSSimulators');
@override
bool get supportsPlatform => Platform.isMacOS;
@override
List<Device> pollingGetDevices() => IOSSimulatorUtils.instance.getAttachedDevices();
}
class IOSSimulatorUtils {
/// Returns [IOSSimulatorUtils] active in the current app context (i.e. zone).
static IOSSimulatorUtils get instance {
return context[IOSSimulatorUtils] ?? (context[IOSSimulatorUtils] = new IOSSimulatorUtils());
}
List<IOSSimulator> getAttachedDevices() {
if (!XCode.instance.isInstalledAndMeetsVersionCheck)
return <IOSSimulator>[];
return SimControl.instance.getConnectedDevices().map((SimDevice device) {
return new IOSSimulator(device.udid, name: device.name);
}).toList();
}
}
/// A wrapper around the `simctl` command line tool.
class SimControl {
/// Returns [SimControl] active in the current app context (i.e. zone).
static SimControl get instance => context[SimControl] ?? (context[SimControl] = new SimControl());
Future<bool> boot({String deviceName}) async {
if (_isAnyConnected())
return true;
if (deviceName == null) {
SimDevice testDevice = _createTestDevice();
if (testDevice == null) {
return false;
}
deviceName = testDevice.name;
}
// `xcrun instruments` requires a template (-t). @yjbanov has no idea what
// "template" is but the built-in 'Blank' seems to work. -l causes xcrun to
// quit after a time limit without killing the simulator. We quit after
// 1 second.
List<String> args = [_xcrunPath, 'instruments', '-w', deviceName, '-t', 'Blank', '-l', '1'];
printTrace(args.join(' '));
runDetached(args);
printStatus('Waiting for iOS Simulator to boot...');
bool connected = false;
int attempted = 0;
while (!connected && attempted < 20) {
connected = await _isAnyConnected();
if (!connected) {
printStatus('Still waiting for iOS Simulator to boot...');
await new Future<Null>.delayed(new Duration(seconds: 1));
}
attempted++;
}
if (connected) {
printStatus('Connected to iOS Simulator.');
return true;
} else {
printStatus('Timed out waiting for iOS Simulator to boot.');
return false;
}
}
SimDevice _createTestDevice() {
SimDeviceType deviceType = _findSuitableDeviceType();
if (deviceType == null)
return null;
String runtime = _findSuitableRuntime();
if (runtime == null)
return null;
// Delete any old test devices
getDevices()
.where((SimDevice d) => d.name.endsWith(_kFlutterTestDeviceSuffix))
.forEach(_deleteDevice);
// Create new device
String deviceName = '${deviceType.name} $_kFlutterTestDeviceSuffix';
List<String> args = [_xcrunPath, 'simctl', 'create', deviceName, deviceType.identifier, runtime];
printTrace(args.join(' '));
runCheckedSync(args);
return getDevices().firstWhere((SimDevice d) => d.name == deviceName);
}
SimDeviceType _findSuitableDeviceType() {
List<Map<String, dynamic>> allTypes = _list(SimControlListSection.devicetypes);
List<Map<String, dynamic>> usableTypes = allTypes
.where((Map<String, dynamic> info) => info['name'].startsWith('iPhone'))
.toList()
..sort((Map<String, dynamic> r1, Map<String, dynamic> r2) => -compareIphoneVersions(r1['identifier'], r2['identifier']));
if (usableTypes.isEmpty) {
printError(
'No suitable device type found.\n'
'You may launch an iOS Simulator manually and Flutter will attempt to use it.'
);
}
return new SimDeviceType(
usableTypes.first['name'],
usableTypes.first['identifier']
);
}
String _findSuitableRuntime() {
List<Map<String, dynamic>> allRuntimes = _list(SimControlListSection.runtimes);
List<Map<String, dynamic>> usableRuntimes = allRuntimes
.where((Map<String, dynamic> info) => info['name'].startsWith('iOS'))
.toList()
..sort((Map<String, dynamic> r1, Map<String, dynamic> r2) => -compareIosVersions(r1['version'], r2['version']));
if (usableRuntimes.isEmpty) {
printError(
'No suitable iOS runtime found.\n'
'You may launch an iOS Simulator manually and Flutter will attempt to use it.'
);
}
return usableRuntimes.first['identifier'];
}
void _deleteDevice(SimDevice device) {
try {
List<String> args = <String>[_xcrunPath, 'simctl', 'delete', device.name];
printTrace(args.join(' '));
runCheckedSync(args);
} catch(e) {
printError(e);
}
}
/// Runs `simctl list --json` and returns the JSON of the corresponding
/// [section].
///
/// The return type depends on the [section] being listed but is usually
/// either a [Map] or a [List].
dynamic _list(SimControlListSection section) {
// Sample output from `simctl list --json`:
//
// {
// "devicetypes": { ... },
// "runtimes": { ... },
// "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"
// },
// ...
// },
// "pairs": { ... },
List<String> args = <String>['simctl', 'list', '--json', section.name];
printTrace('$_xcrunPath ${args.join(' ')}');
ProcessResult results = Process.runSync(_xcrunPath, args);
if (results.exitCode != 0) {
printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
return <String, Map<String, dynamic>>{};
}
return JSON.decode(results.stdout)[section.name];
}
/// Returns a list of all available devices, both potential and connected.
List<SimDevice> getDevices() {
List<SimDevice> devices = <SimDevice>[];
Map<String, dynamic> devicesSection = _list(SimControlListSection.devices);
for (String deviceCategory in devicesSection.keys) {
List<Map<String, String>> devicesData = devicesSection[deviceCategory];
for (Map<String, String> data in devicesData) {
devices.add(new SimDevice(deviceCategory, data));
}
}
return devices;
}
/// Returns all the connected simulator devices.
List<SimDevice> getConnectedDevices() {
return getDevices().where((SimDevice device) => device.isBooted).toList();
}
StreamController<List<SimDevice>> _trackDevicesControler;
/// Listens to changes in the set of connected devices. The implementation
/// currently uses polling. Callers should be careful to call cancel() on any
/// stream subscription when finished.
///
/// TODO(devoncarew): We could investigate using the usbmuxd protocol directly.
Stream<List<SimDevice>> trackDevices() {
if (_trackDevicesControler == null) {
Timer timer;
Set<String> deviceIds = new Set<String>();
_trackDevicesControler = new StreamController<List<SimDevice>>.broadcast(
onListen: () {
timer = new Timer.periodic(new Duration(seconds: 4), (Timer timer) {
List<SimDevice> devices = getConnectedDevices();
if (_updateDeviceIds(devices, deviceIds))
_trackDevicesControler.add(devices);
});
}, onCancel: () {
timer?.cancel();
deviceIds.clear();
}
);
}
return _trackDevicesControler.stream;
}
/// Update the cached set of device IDs and return whether there were any changes.
bool _updateDeviceIds(List<SimDevice> devices, Set<String> deviceIds) {
Set<String> newIds = new Set<String>.from(devices.map((SimDevice device) => device.udid));
bool changed = false;
for (String id in newIds) {
if (!deviceIds.contains(id))
changed = true;
}
for (String id in deviceIds) {
if (!newIds.contains(id))
changed = true;
}
deviceIds.clear();
deviceIds.addAll(newIds);
return changed;
}
bool _isAnyConnected() => getConnectedDevices().isNotEmpty;
void install(String deviceId, String appPath) {
runCheckedSync([_xcrunPath, 'simctl', 'install', deviceId, appPath]);
}
void launch(String deviceId, String appIdentifier, [List<String> launchArgs]) {
List<String> args = [_xcrunPath, 'simctl', 'launch', deviceId, appIdentifier];
if (launchArgs != null)
args.addAll(launchArgs);
runCheckedSync(args);
}
}
/// Enumerates all data sections of `xcrun simctl list --json` command.
class SimControlListSection {
const SimControlListSection._(this.name);
final String name;
static const SimControlListSection devices = const SimControlListSection._('devices');
static const SimControlListSection devicetypes = const SimControlListSection._('devicetypes');
static const SimControlListSection runtimes = const SimControlListSection._('runtimes');
static const SimControlListSection pairs = const SimControlListSection._('pairs');
}
/// 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;
}
class SimDevice {
SimDevice(this.category, this.data);
final String category;
final Map<String, String> data;
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 {
IOSSimulator(String id, { this.name }) : super(id);
@override
final String name;
@override
bool get isLocalEmulator => true;
_IOSSimulatorLogReader _logReader;
_IOSSimulatorDevicePortForwarder _portForwarder;
String get xcrunPath => path.join('/usr', 'bin', 'xcrun');
String _getSimulatorPath() {
return path.join(homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', id);
}
String _getSimulatorAppHomeDirectory(ApplicationPackage app) {
String simulatorPath = _getSimulatorPath();
if (simulatorPath == null)
return null;
return path.join(simulatorPath, 'data');
}
@override
bool installApp(ApplicationPackage app) {
try {
SimControl.instance.install(id, app.localPath);
return true;
} catch (e) {
return false;
}
}
@override
bool isSupported() {
if (!Platform.isMacOS) {
_supportMessage = "Not supported on a non Mac host";
return false;
}
// Step 1: Check if the device is part of a blacklisted category.
// We do not support WatchOS or tvOS devices.
RegExp blacklist = new RegExp(r'Apple (TV|Watch)', caseSensitive: false);
if (blacklist.hasMatch(name)) {
_supportMessage = "Flutter does not support either the Apple TV or Watch. Choose an iPhone 5s or above.";
return false;
}
// Step 2: Check if the device must be rejected because of its version.
// There is an artitifical check on older simulators where arm64
// targetted applications cannot be run (even though the
// Flutter runner on the simulator is completely different).
RegExp versionExp = new RegExp(r'iPhone ([0-9])+');
Match match = versionExp.firstMatch(name);
if (match == null) {
// Not an iPhone. All available non-iPhone simulators are compatible.
return true;
}
if (int.parse(match.group(1)) > 5) {
// iPhones 6 and above are always fine.
return true;
}
// The 's' subtype of 5 is compatible.
if (name.contains('iPhone 5s')) {
return true;
}
_supportMessage = "The simulator version is too old. Choose an iPhone 5s or above.";
return false;
}
String _supportMessage;
@override
String supportMessage() {
if (isSupported())
return "Supported";
return _supportMessage != null ? _supportMessage : "Unknown";
}
@override
bool isAppInstalled(ApplicationPackage app) {
try {
String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
return FileSystemEntity.isDirectorySync(simulatorHomeDirectory);
} catch (e) {
return false;
}
}
@override
Future<bool> startApp(
ApplicationPackage app,
Toolchain toolchain, {
String mainPath,
String route,
bool checked: true,
bool clearLogs: false,
bool startPaused: false,
int debugPort: observatoryDefaultPort,
Map<String, dynamic> platformArgs
}) async {
printTrace('Building ${app.name} for $id.');
if (clearLogs)
this.clearLogs();
if (!(await _setupUpdatedApplicationBundle(app, toolchain)))
return false;
ServiceProtocolDiscovery serviceProtocolDiscovery =
new ServiceProtocolDiscovery(logReader);
// We take this future here but do not wait for completion until *after* we
// start the application.
Future<int> scrapeServicePort = serviceProtocolDiscovery.nextPort();
// Prepare launch arguments.
List<String> args = <String>[
"--flx=${path.absolute(path.join('build', 'app.flx'))}",
"--dart-main=${path.absolute(mainPath)}",
"--packages=${path.absolute('.packages')}",
];
if (checked)
args.add("--enable-checked-mode");
if (startPaused)
args.add("--start-paused");
if (debugPort != observatoryDefaultPort)
args.add("--observatory-port=$debugPort");
// Launch the updated application in the simulator.
try {
SimControl.instance.launch(id, app.id, args);
} catch (error) {
printError('$error');
return false;
}
// 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 {
int devicePort = await scrapeServicePort.timeout(new Duration(seconds: 12));
printTrace('service protocol port = $devicePort');
printStatus('Observatory listening on http://127.0.0.1:$devicePort');
return true;
} catch (error) {
if (error is TimeoutException)
printError('Timed out while waiting for a debug connection.');
else
printError('Error waiting for a debug connection: $error');
return false;
}
}
bool _applicationIsInstalledAndRunning(ApplicationPackage app) {
bool isInstalled = exitsHappy([
'xcrun',
'simctl',
'get_app_container',
'booted',
app.id,
]);
bool isRunning = exitsHappy([
'/usr/bin/killall',
'Runner',
]);
return isInstalled && isRunning;
}
Future<bool> _setupUpdatedApplicationBundle(ApplicationPackage app, Toolchain toolchain) async {
bool sideloadResult = await _sideloadUpdatedAssetsForInstalledApplicationBundle(app, toolchain);
if (!sideloadResult)
return false;
if (!_applicationIsInstalledAndRunning(app))
return _buildAndInstallApplicationBundle(app);
return true;
}
Future<bool> _buildAndInstallApplicationBundle(ApplicationPackage app) async {
// Step 1: Build the Xcode project.
bool buildResult = await buildIOSXcodeProject(app, buildForDevice: false);
if (!buildResult) {
printError('Could not build the application for the simulator.');
return false;
}
// Step 2: Assert that the Xcode project was successfully built.
Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphonesimulator', 'Runner.app'));
bool bundleExists = await bundle.exists();
if (!bundleExists) {
printError('Could not find the built application bundle at ${bundle.path}.');
return false;
}
// Step 3: Install the updated bundle to the simulator.
SimControl.instance.install(id, path.absolute(bundle.path));
return true;
}
Future<bool> _sideloadUpdatedAssetsForInstalledApplicationBundle(
ApplicationPackage app, Toolchain toolchain) async {
return (await flx.build(toolchain, precompiledSnapshot: true)) == 0;
}
@override
Future<bool> stopApp(ApplicationPackage app) async {
// Currently we don't have a way to stop an app running on iOS.
return false;
}
Future<bool> pushFile(
ApplicationPackage app, String localFile, String targetFile) async {
if (Platform.isMacOS) {
String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
runCheckedSync(<String>['cp', localFile, path.join(simulatorHomeDirectory, targetFile)]);
return true;
}
return false;
}
String get logFilePath {
return path.join(homeDirectory, 'Library', 'Logs', 'CoreSimulator', id, 'system.log');
}
@override
TargetPlatform get platform => TargetPlatform.ios;
@override
DeviceLogReader get logReader {
if (_logReader == null)
_logReader = new _IOSSimulatorLogReader(this);
return _logReader;
}
@override
DevicePortForwarder get portForwarder {
if (_portForwarder == null)
_portForwarder = new _IOSSimulatorDevicePortForwarder(this);
return _portForwarder;
}
@override
void clearLogs() {
File logFile = new File(logFilePath);
if (logFile.existsSync()) {
RandomAccessFile randomFile = logFile.openSync(mode: FileMode.WRITE);
randomFile.truncateSync(0);
randomFile.closeSync();
}
}
void ensureLogsExists() {
File logFile = new File(logFilePath);
if (!logFile.existsSync())
logFile.writeAsBytesSync(<int>[]);
}
@override
bool get supportsScreenshot => true;
@override
Future<bool> takeScreenshot(File outputFile) async {
String homeDirPath = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
Directory desktopDir = new Directory(path.join(homeDirPath, 'Desktop'));
// 'Simulator Screen Shot Mar 25, 2016, 2.59.43 PM.png'
Set<File> getScreenshots() {
return new Set<File>.from(desktopDir.listSync().where((FileSystemEntity entity) {
String name = path.basename(entity.path);
return entity is File && name.startsWith('Simulator') && name.endsWith('.png');
}));
};
Set<File> existingScreenshots = getScreenshots();
runSync(<String>[
'osascript',
'-e',
'activate application "Simulator"\n'
'tell application "System Events" to keystroke "s" using command down'
]);
// There is some latency here from the applescript call.
await new Future<Null>.delayed(new Duration(seconds: 1));
Set<File> shots = getScreenshots().difference(existingScreenshots);
if (shots.isEmpty) {
printError('Unable to locate the screenshot file.');
return false;
}
File shot = shots.first;
outputFile.writeAsBytesSync(shot.readAsBytesSync());
shot.delete();
return true;
}
}
class _IOSSimulatorLogReader extends DeviceLogReader {
_IOSSimulatorLogReader(this.device);
final IOSSimulator device;
final StreamController<String> _linesStreamController =
new StreamController<String>.broadcast();
bool _lastWasFiltered = false;
// We log from two logs: the device and the system log.
Process _deviceProcess;
StreamSubscription<String> _deviceStdoutSubscription;
StreamSubscription<String> _deviceStderrSubscription;
Process _systemProcess;
StreamSubscription<String> _systemStdoutSubscription;
StreamSubscription<String> _systemStderrSubscription;
@override
Stream<String> get lines => _linesStreamController.stream;
@override
String get name => device.name;
@override
bool get isReading => (_deviceProcess != null) && (_systemProcess != null);
@override
Future<int> get finished {
return (_deviceProcess != null) ? _deviceProcess.exitCode : new Future<int>.value(0);
}
@override
Future<Null> start() async {
if (isReading) {
throw new StateError(
'_IOSSimulatorLogReader must be stopped before it can be started.'
);
}
// TODO(johnmccutchan): Add a ProcessSet abstraction that handles running
// N processes and merging their output.
// Device log.
device.ensureLogsExists();
_deviceProcess = await runCommand(
<String>['tail', '-n', '+0', '-F', device.logFilePath]);
_deviceStdoutSubscription =
_deviceProcess.stdout.transform(UTF8.decoder)
.transform(const LineSplitter()).listen(_onDeviceLine);
_deviceStderrSubscription =
_deviceProcess.stderr.transform(UTF8.decoder)
.transform(const LineSplitter()).listen(_onDeviceLine);
_deviceProcess.exitCode.then(_onDeviceExit);
// Track system.log crashes.
// ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
_systemProcess = await runCommand(
<String>['tail', '-F', '/private/var/log/system.log']);
_systemStdoutSubscription =
_systemProcess.stdout.transform(UTF8.decoder)
.transform(const LineSplitter()).listen(_onSystemLine);
_systemStderrSubscription =
_systemProcess.stderr.transform(UTF8.decoder)
.transform(const LineSplitter()).listen(_onSystemLine);
_systemProcess.exitCode.then(_onSystemExit);
}
@override
Future<Null> stop() async {
if (!isReading) {
throw new StateError(
'_IOSSimulatorLogReader must be started before it can be stopped.'
);
}
if (_deviceProcess != null) {
await _deviceProcess.kill();
_deviceProcess = null;
}
_onDeviceExit(0);
if (_systemProcess != null) {
await _systemProcess.kill();
_systemProcess = null;
}
_onSystemExit(0);
}
void _onDeviceExit(int exitCode) {
_deviceStdoutSubscription?.cancel();
_deviceStdoutSubscription = null;
_deviceStderrSubscription?.cancel();
_deviceStderrSubscription = null;
_deviceProcess = null;
}
void _onSystemExit(int exitCode) {
_systemStdoutSubscription?.cancel();
_systemStdoutSubscription = null;
_systemStderrSubscription?.cancel();
_systemStderrSubscription = null;
_systemProcess = null;
}
// Match the log prefix (in order to shorten it):
// 'Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: ...'
final RegExp _mapRegex =
new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$');
// Jan 31 19:23:28 --- last message repeated 1 time ---
final RegExp _lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ --- (.*) ---$');
final RegExp _flutterRunnerRegex = new RegExp(r' FlutterRunner\[\d+\] ');
String _filterDeviceLine(String string) {
Match match = _mapRegex.matchAsPrefix(string);
if (match != null) {
_lastWasFiltered = true;
// Filter out some messages that clearly aren't related to Flutter.
if (string.contains(': could not find icon for representation -> com.apple.'))
return null;
String category = match.group(1);
String content = match.group(2);
if (category == 'Game Center' || category == 'itunesstored' ||
category == 'nanoregistrylaunchd' || category == 'mstreamd' ||
category == 'syncdefaultsd' || category == 'companionappd' ||
category == 'searchd')
return null;
_lastWasFiltered = false;
if (category == 'Runner')
return content;
return '$category: $content';
}
match = _lastMessageRegex.matchAsPrefix(string);
if (match != null && !_lastWasFiltered)
return '(${match.group(1)})';
return string;
}
void _onDeviceLine(String line) {
String filteredLine = _filterDeviceLine(line);
if (filteredLine == null)
return;
_linesStreamController.add(filteredLine);
}
String _filterSystemLog(String string) {
Match match = _mapRegex.matchAsPrefix(string);
return match == null ? string : '${match.group(1)}: ${match.group(2)}';
}
void _onSystemLine(String line) {
if (!_flutterRunnerRegex.hasMatch(line))
return;
String filteredLine = _filterSystemLog(line);
if (filteredLine == null)
return;
_linesStreamController.add(filteredLine);
}
@override
int get hashCode => device.logFilePath.hashCode;
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! _IOSSimulatorLogReader)
return false;
return other.device.logFilePath == device.logFilePath;
}
}
int compareIosVersions(String v1, String v2) {
List<int> v1Fragments = v1.split('.').map(int.parse).toList();
List<int> v2Fragments = v2.split('.').map(int.parse).toList();
int i = 0;
while(i < v1Fragments.length && i < v2Fragments.length) {
int v1Fragment = v1Fragments[i];
int v2Fragment = v2Fragments[i];
if (v1Fragment != v2Fragment)
return v1Fragment.compareTo(v2Fragment);
i++;
}
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 =
new RegExp(r'com.apple.CoreSimulator.SimDeviceType.iPhone-(\d+)(.*)');
int compareIphoneVersions(String id1, String id2) {
Match m1 = _iosDeviceTypePattern.firstMatch(id1);
Match m2 = _iosDeviceTypePattern.firstMatch(id2);
int v1 = int.parse(m1[1]);
int v2 = int.parse(m2[1]);
if (v1 != v2)
return v1.compareTo(v2);
// Sorted in the least preferred first order.
const List<String> qualifiers = const <String>['-Plus', '', 's-Plus', 's'];
int q1 = qualifiers.indexOf(m1[2]);
int q2 = qualifiers.indexOf(m2[2]);
return q1.compareTo(q2);
}
class _IOSSimulatorDevicePortForwarder extends DevicePortForwarder {
_IOSSimulatorDevicePortForwarder(this.device);
final IOSSimulator device;
final List<ForwardedPort> _ports = <ForwardedPort>[];
@override
List<ForwardedPort> get forwardedPorts {
return _ports;
}
@override
Future<int> forward(int devicePort, {int hostPort: null}) async {
if ((hostPort == null) || (hostPort == 0)) {
hostPort = devicePort;
}
assert(devicePort == hostPort);
_ports.add(new ForwardedPort(devicePort, hostPort));
return hostPort;
}
@override
Future<Null> unforward(ForwardedPort forwardedPort) async {
_ports.remove(forwardedPort);
}
}