mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
808 lines
24 KiB
Dart
808 lines
24 KiB
Dart
// 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 '../toolchain.dart';
|
|
import 'mac.dart';
|
|
|
|
const String _xcrunPath = '/usr/bin/xcrun';
|
|
|
|
/// Test device created by Flutter when no other device is available.
|
|
const String _kFlutterTestDevice = 'flutter.test.device';
|
|
|
|
class IOSSimulators extends PollingDeviceDiscovery {
|
|
IOSSimulators() : super('IOSSimulators');
|
|
|
|
bool get supportsPlatform => Platform.isMacOS;
|
|
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.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() {
|
|
String deviceType = _findSuitableDeviceType();
|
|
if (deviceType == null) {
|
|
return null;
|
|
}
|
|
|
|
String runtime = _findSuitableRuntime();
|
|
if (runtime == null) {
|
|
return null;
|
|
}
|
|
|
|
// Delete any old test devices
|
|
getDevices()
|
|
.where((d) => d.name == _kFlutterTestDevice)
|
|
.forEach(_deleteDevice);
|
|
|
|
// Create new device
|
|
List<String> args = [_xcrunPath, 'simctl', 'create', _kFlutterTestDevice, deviceType, runtime];
|
|
printTrace(args.join(' '));
|
|
runCheckedSync(args);
|
|
|
|
return getDevices().firstWhere((d) => d.name == _kFlutterTestDevice);
|
|
}
|
|
|
|
String _findSuitableDeviceType() {
|
|
List<Map<String, dynamic>> allTypes = _list(SimControlListSection.devicetypes);
|
|
List<Map<String, dynamic>> usableTypes = allTypes
|
|
.where((info) => info['name'].startsWith('iPhone'))
|
|
.toList()
|
|
..sort((r1, 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 usableTypes.first['identifier'];
|
|
}
|
|
|
|
String _findSuitableRuntime() {
|
|
List<Map<String, dynamic>> allRuntimes = _list(SimControlListSection.runtimes);
|
|
List<Map<String, dynamic>> usableRuntimes = allRuntimes
|
|
.where((info) => info['name'].startsWith('iOS'))
|
|
.toList()
|
|
..sort((r1, 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 = [_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<dynamic> 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.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 {
|
|
static const devices = const SimControlListSection._('devices');
|
|
static const devicetypes = const SimControlListSection._('devicetypes');
|
|
static const runtimes = const SimControlListSection._('runtimes');
|
|
static const pairs = const SimControlListSection._('pairs');
|
|
|
|
final String name;
|
|
const SimControlListSection._(this.name);
|
|
}
|
|
|
|
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);
|
|
|
|
final String name;
|
|
|
|
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;
|
|
|
|
// Prepare launch arguments.
|
|
List<String> args = <String>[
|
|
"--flx=${path.absolute(path.join('build', 'app.flx'))}",
|
|
"--dart-main=${path.absolute(mainPath)}",
|
|
"--package-root=${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;
|
|
}
|
|
|
|
printTrace('Successfully started ${app.name} on $id.');
|
|
|
|
return true;
|
|
}
|
|
|
|
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.iOSSimulator;
|
|
|
|
DeviceLogReader get logReader {
|
|
if (_logReader == null)
|
|
_logReader = new _IOSSimulatorLogReader(this);
|
|
|
|
return _logReader;
|
|
}
|
|
|
|
DevicePortForwarder get portForwarder {
|
|
if (_portForwarder == null)
|
|
_portForwarder = new _IOSSimulatorDevicePortForwarder(this);
|
|
|
|
return _portForwarder;
|
|
}
|
|
|
|
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>[]);
|
|
}
|
|
}
|
|
|
|
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 _deviceStdoutSubscription;
|
|
StreamSubscription _deviceStderrSubscription;
|
|
|
|
Process _systemProcess;
|
|
StreamSubscription _systemStdoutSubscription;
|
|
StreamSubscription _systemStderrSubscription;
|
|
|
|
Stream<String> get lines => _linesStreamController.stream;
|
|
|
|
String get name => device.name;
|
|
|
|
bool get isReading => (_deviceProcess != null) && (_systemProcess != null);
|
|
|
|
Future get finished =>
|
|
(_deviceProcess != null) ? _deviceProcess.exitCode : new Future.value(0);
|
|
|
|
Future 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);
|
|
}
|
|
|
|
Future 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);
|
|
}
|
|
|
|
int get hashCode => device.logFilePath.hashCode;
|
|
|
|
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 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>[];
|
|
|
|
List<ForwardedPort> get forwardedPorts {
|
|
return _ports;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
Future unforward(ForwardedPort forwardedPort) async {
|
|
_ports.remove(forwardedPort);
|
|
}
|
|
}
|