
* This makes the turnaround times < 2 seconds on the iOS simulator. * Also bumps the engine to use the new engine required to support the flags.
529 lines
16 KiB
Dart
529 lines
16 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' show JSON;
|
|
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';
|
|
|
|
const String _simulatorPath =
|
|
'/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator';
|
|
|
|
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.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 deviceId}) async {
|
|
if (_isAnyConnected())
|
|
return true;
|
|
|
|
if (deviceId == null) {
|
|
runDetached([_simulatorPath]);
|
|
Future<bool> checkConnection([int attempts = 20]) async {
|
|
if (attempts == 0) {
|
|
printStatus('Timed out waiting for iOS Simulator to boot.');
|
|
return false;
|
|
}
|
|
if (!_isAnyConnected()) {
|
|
printStatus('Waiting for iOS Simulator to boot...');
|
|
return await new Future.delayed(new Duration(milliseconds: 500),
|
|
() => checkConnection(attempts - 1)
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
return await checkConnection();
|
|
} else {
|
|
try {
|
|
runCheckedSync([_xcrunPath, 'simctl', 'boot', deviceId]);
|
|
return true;
|
|
} catch (e) {
|
|
printError('Unable to boot iOS Simulator $deviceId: ', e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// Returns a list of all available devices, both potential and connected.
|
|
List<SimDevice> getDevices() {
|
|
// {
|
|
// "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"
|
|
// },
|
|
// ...
|
|
|
|
List<String> args = <String>['simctl', 'list', '--json', 'devices'];
|
|
printTrace('$_xcrunPath ${args.join(' ')}');
|
|
ProcessResult results = Process.runSync(_xcrunPath, args);
|
|
if (results.exitCode != 0) {
|
|
printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
|
|
return <SimDevice>[];
|
|
}
|
|
|
|
List<SimDevice> devices = <SimDevice>[];
|
|
|
|
Map<String, Map<String, dynamic>> data = JSON.decode(results.stdout);
|
|
Map<String, dynamic> devicesSection = data['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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
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 {
|
|
if (_applicationIsInstalledAndRunning(app)) {
|
|
return _sideloadUpdatedAssetsForInstalledApplicationBundle(app, toolchain);
|
|
} else {
|
|
return _buildAndInstallApplicationBundle(app);
|
|
}
|
|
}
|
|
|
|
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 createLogReader() => new _IOSSimulatorLogReader(this);
|
|
|
|
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;
|
|
|
|
bool _lastWasFiltered = false;
|
|
|
|
String get name => device.name;
|
|
|
|
Future<int> logs({ bool clear: false, bool showPrefix: false }) async {
|
|
if (clear)
|
|
device.clearLogs();
|
|
|
|
device.ensureLogsExists();
|
|
|
|
// Match the log prefix (in order to shorten it):
|
|
// 'Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: ...'
|
|
RegExp mapRegex = new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$');
|
|
// Jan 31 19:23:28 --- last message repeated 1 time ---
|
|
RegExp lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ --- (.*) ---$');
|
|
|
|
// This filter matches many Flutter lines in the log:
|
|
// new RegExp(r'(FlutterRunner|flutter.runner.Runner|$id)'), but it misses
|
|
// a fair number, including ones that would be useful in diagnosing crashes.
|
|
// For now, we're not filtering the log file (but do clear it with each run).
|
|
|
|
Future<int> result = runCommandAndStreamOutput(
|
|
<String>['tail', '-n', '+0', '-F', device.logFilePath],
|
|
prefix: showPrefix ? '[$name] ' : '',
|
|
mapFunction: (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;
|
|
}
|
|
);
|
|
|
|
// Track system.log crashes.
|
|
// ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
|
|
runCommandAndStreamOutput(
|
|
<String>['tail', '-F', '/private/var/log/system.log'],
|
|
prefix: showPrefix ? '[$name] ' : '',
|
|
filter: new RegExp(r' FlutterRunner\[\d+\] '),
|
|
mapFunction: (String string) {
|
|
Match match = mapRegex.matchAsPrefix(string);
|
|
return match == null ? string : '${match.group(1)}: ${match.group(2)}';
|
|
}
|
|
);
|
|
|
|
return await result;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|