ioslib
Version:
iOS Utility Library
1,357 lines (1,185 loc) • 71.6 kB
JavaScript
/**
* Detects iOS developer and distribution certificates and the WWDR certificate.
*
* @module simulator
*
* @copyright
* Copyright (c) 2014-2018 by Appcelerator, Inc. All Rights Reserved.
*
* @license
* Licensed under the terms of the Apache Public License.
* Please see the LICENSE included with this distribution for details.
*/
'use strict';
const appc = require('node-appc');
const async = require('async');
const EventEmitter = require('events').EventEmitter;
const magik = require('./utilities').magik;
const fs = require('fs');
const net = require('net');
const path = require('path');
const readPlist = require('./utilities').readPlist;
const simctl = require('./simctl');
const spawn = require('child_process').spawn;
const Tail = require('always-tail');
const xcode = require('./xcode');
const __ = appc.i18n(__dirname).__;
let cache;
exports.detect = detect;
exports.findSimulators = findSimulators;
exports.launch = launch;
exports.stop = stop;
exports.SimHandle = SimHandle;
exports.SimulatorCrash = SimulatorCrash;
/**
* @class
* @classdesc An exception for when an app crashes in the iOS Simulator.
* @constructor
* @param {Array|Object} [crashFiles] - The crash details.
*/
function SimulatorCrash(crashFiles) {
this.name = 'SimulatorCrash';
this.message = __('App crashed in the iOS Simulator');
this.crashFiles = Array.isArray(crashFiles) ? crashFiles : crashFiles ? [ crashFiles ] : null;
}
SimulatorCrash.prototype = Object.create(Error.prototype);
SimulatorCrash.prototype.constructor = SimulatorCrash;
function SimHandle(obj) {
appc.util.mix(this, obj);
}
exports.deviceState = {
DOES_NOT_EXIST: -1,
CREATING: 0,
SHUTDOWN: 1,
BOOTING: 2,
BOOTED: 3,
SHUTTING_DOWN: 4
};
exports.deviceStateNames = {
0: 'Creating',
1: 'Shutdown',
2: 'Booting',
3: 'Booted',
4: 'Shutting Down'
};
/**
* Helper function for comparing two simulators based on the model name.
*
* @param {Object} a - A simulator handle.
* @param {Object} b - Another simulator handle.
*
* @returns {Number} - Returns -1 if a < b, 1 if a > b, and 0 if they are equal.
*/
function compareSims(a, b) {
return a.model < b.model ? -1 : a.model > b.model ? 1 : 0;
}
/**
* Detects iOS simulators.
*
* @param {Object} [options] - An object containing various settings.
* @param {Boolean} [options.bypassCache=false] - When true, re-detects all iOS simulators.
* @param {Function} [callback(err, results)] - A function to call with the simulator information.
*
* @emits module:simulator#detected
* @emits module:simulator#error
*
* @returns {Handle}
*/
function detect(options, callback) {
return magik(options, callback, function (emitter, options, callback) {
if (cache && !options.bypassCache) {
var dupe = JSON.parse(JSON.stringify(cache));
emitter.emit('detected', dupe);
return callback(null, dupe);
}
function fakeWatchSim(name, udid, model, xcodes) {
return {
udid: udid,
name: name,
version: '1.0',
type: 'watchos',
simctl: null,
simulator: null,
deviceType: null,
deviceName: name,
deviceDir: null,
model: model,
family: 'watch',
supportsXcode: xcodes,
supportsWatch: {},
watchCompanion: {},
runtime: null,
runtimeName: 'watchOS 1.0',
systemLog: null,
dataDir: null
};
}
var results = {
simulators: {
ios: {},
watchos: {},
crashDir: appc.fs.resolvePath('~/Library/Logs/DiagnosticReports'),
},
issues: []
};
xcode.detect(options, function (err, xcodeInfo) {
if (err) {
emitter.emit('error', err);
return callback(err);
}
var xcodeIds = Object
.keys(xcodeInfo.xcode)
.filter(function (ver) { return xcodeInfo.xcode[ver].supported; })
.sort(function (a, b) {
var v1 = xcodeInfo.xcode[a].version;
var v2 = xcodeInfo.xcode[b].version;
return xcodeInfo.xcode[a].selected || appc.version.lt(v1, v2) ? -1 : appc.version.eq(v1, v2) ? 0 : 1;
});
// if we have Xcode 6.2, 6.3, or 6.4, then inject some fake devices for WatchKit 1.x
xcodeIds.some(function (id) {
var xc = xcodeInfo.xcode[id];
if (appc.version.satisfies(xc.version, '>=6.2 <7.0')) {
var xcodes = {};
xcodeIds.forEach(function (id) {
if (appc.version.satisfies(xcodeInfo.xcode[id].version, '>=6.2 <7.0')) {
xcodes[id] = true;
}
});
results.simulators.watchos['1.0'] = [
fakeWatchSim('Apple Watch - 38mm', '58045222-F0C1-41F7-A4BD-E2EDCFBCF5B9', 'Watch0,1', xcodes),
fakeWatchSim('Apple Watch - 42mm', 'D5C1DA2F-7A74-49C8-809A-906E554021B0', 'Watch0,2', xcodes)
];
return true;
}
});
if (!xcodeInfo.selectedXcode || !xcodeInfo.selectedXcode.eulaAccepted) {
emitter.emit('detected', results);
return callback(null, results);
}
const typeRE = /iOS|watchOS/i;
const deviceTypeLookup = {};
const runtimeLookup = {};
xcodeIds.forEach(function (xcodeId) {
var xc = xcodeInfo.xcode[xcodeId];
Object.keys(xc.simDeviceTypes).forEach(function (id) {
if (!deviceTypeLookup[id]) {
deviceTypeLookup[id] = {
name: xc.simDeviceTypes[id].name,
model: xc.simDeviceTypes[id].model,
supportsWatch: xc.simDeviceTypes[id].supportsWatch
};
}
});
Object.keys(xc.simRuntimes).forEach(function (id) {
if (typeRE.test(id)) {
if (!runtimeLookup[id]) {
runtimeLookup[id] = {
name: xc.simRuntimes[id].name,
version: xc.simRuntimes[id].version,
simctl: xc.executables.simctl,
simulator: xc.executables[/watch/i.test(xc.simRuntimes[id].name) ? 'watchsimulator' : 'simulator'],
xcodeIds: []
};
}
if (runtimeLookup[id].xcodeIds.indexOf(xcodeId) === -1) {
runtimeLookup[id].xcodeIds.push(xcodeId);
}
}
});
});
list(options, function (err, info) {
if (err) {
return callback(err);
}
// find the missing global devicetypes and runtimes from simctl
info.devicetypes.forEach(function (deviceType) {
if (!deviceTypeLookup[deviceType.identifier]) {
deviceTypeLookup[deviceType.identifier] = {
name: deviceType.name,
model: deviceType.model,
supportsWatch: deviceType.supportsWatch
};
}
});
info.runtimes.forEach(function (runtime) {
if (typeRE.test(runtime.identifier)) {
var rt = runtimeLookup[runtime.identifier];
if (!rt) {
rt = runtimeLookup[runtime.identifier] = {
name: runtime.name,
version: runtime.version,
simctl: null,
simulator: null,
xcodeIds: []
};
}
xcodeIds.forEach(function (xcodeId) {
var xc = xcodeInfo.xcode[xcodeId];
if (xc.simRuntimes[runtime.version]) {
if (rt.xcodeIds.indexOf(xcodeId) === -1) {
rt.xcodeIds.push(xcodeId);
}
if (!rt.simctl) {
rt.simctl = xc.executables.simctl;
}
if (!rt.simulator) {
rt.simulator = xc.executables[/watch/i.test(xc.simRuntimes[runtime.version].name) ? 'watchsimulator' : 'simulator'];
}
}
});
// if we didn't find a valid Xcode for this runtime, then remove it
if (!rt.simctl || !rt.simulator) {
delete runtimeLookup[runtime.identifier];
}
}
});
var coreSimDir = appc.fs.resolvePath('~/Library/Developer/CoreSimulator/Devices');
var familyRE = /^(iphone|ipad|ios|watch|watchos)$/;
Object.keys(info.devices).forEach(function (type) {
info.devices[type].forEach(function (device) {
var plist = readPlist(path.join(coreSimDir, device.udid, 'device.plist'));
if (!plist) {
return;
}
var deviceType = deviceTypeLookup[plist.deviceType];
var runtime = runtimeLookup[plist.runtime];
if (!deviceType || !runtime) {
// we have no idea what this simulator is nor are there any Xcodes
// capable of running it
return;
}
var family = deviceType.model && deviceType.model.replace(/[\W0-9]/g, '').toLowerCase();
if (!family || !familyRE.test(family)) {
// unsupported, could be an Apple TV device
return;
}
var simType = family === 'iphone' || family === 'ipad' ? 'ios' : 'watchos';
// This code finds the sim runtime and builds the list of associated
// iOS SDKs which may be different based which Xcode's simctl is run.
// For example, sim runtime 10.3 is associated with iOS 10.3 and 10.3.1.
// Because of this, we define the same simulator for each associated
// iOS SDK version.
runtime.versions = [ runtime.version ];
if (runtimeLookup[plist.runtime]) {
var ver = runtimeLookup[plist.runtime].version;
if (ver !== runtime.version) {
runtime.versions.push(ver);
}
}
// for each runtime iOS SDK version, define the simulator
runtime.versions.forEach(function (runtimeVersion) {
var sim;
results.simulators[simType][runtimeVersion] || (results.simulators[simType][runtimeVersion] = []);
results.simulators[simType][runtimeVersion].some(function (s) {
if (s.udid === plist.UDID) {
sim = s;
return true;
}
});
if (!sim) {
results.simulators[simType][runtimeVersion].push(sim = {
udid: plist.UDID,
name: plist.name,
version: runtimeVersion,
type: simType,
simctl: runtime.simctl,
simulator: runtime.simulator,
deviceType: plist.deviceType,
deviceName: deviceType.name,
deviceDir: path.join(coreSimDir, device.udid),
model: deviceType.model,
family: family,
supportsXcode: {},
supportsWatch: {},
watchCompanion: {},
runtime: plist.runtime,
runtimeName: runtime.name,
systemLog: appc.fs.resolvePath('~/Library/Logs/CoreSimulator/' + device.udid + '/system.log'),
dataDir: path.join(coreSimDir, device.udid, 'data')
});
}
runtime.xcodeIds.forEach(function (xcodeId) {
sim.supportsXcode[xcodeId] = true;
if (simType === 'ios') {
sim.supportsWatch[xcodeId] = deviceType.supportsWatch;
}
});
});
});
});
// this is pretty nasty, but necessary...
// basically this will populate the watchCompanion property for each iOS Simulator
// so that it makes choosing simulator pairs way easier
Object.keys(results.simulators.ios).forEach(function (iosSimVersion) { // 13.0
results.simulators.ios[iosSimVersion].forEach(function (iosSim) { // sim handle
Object.keys(iosSim.supportsWatch).forEach(function (xcodeId) { // 11.0:11A419c
if (iosSim.supportsWatch[xcodeId]) {
var xc = xcodeInfo.xcode[xcodeId];
Object.keys(xc.simDevicePairs).forEach(function (iOSRange) { // 13.x
if (appc.version.satisfies(iosSim.version, iOSRange)) {
Object.keys(xc.simDevicePairs[iOSRange]).forEach(function (watchOSRange) { // 6.x
if (xc.simDevicePairs[iOSRange][watchOSRange]) {
Object.keys(results.simulators.watchos).forEach(function (watchosSDK) { // 6.x
if (appc.version.satisfies(watchosSDK, watchOSRange)) {
results.simulators.watchos[watchosSDK].forEach(function (watchSim) { // watch sim handle
if (appc.version.satisfies(watchSim.version, watchOSRange)) {
iosSim.watchCompanion[xcodeId] || (iosSim.watchCompanion[xcodeId] = {});
iosSim.watchCompanion[xcodeId][watchSim.udid] = watchSim;
}
});
}
});
}
});
}
});
}
});
});
});
// sort the simulators
['ios', 'watchos'].forEach(function (type) {
Object.keys(results.simulators[type]).forEach(function (ver) {
results.simulators[type][ver].sort(compareSims);
});
});
// the cache must be a clean copy that we'll clone for subsequent detect() calls
// because we can't allow the cache to be modified by reference
cache = JSON.parse(JSON.stringify(results));
emitter.emit('detected', results);
callback(null, results);
});
});
});
};
/**
* Finds the specified app's bundle identifier. If a watch app name is specified,
* then it will attempt to find the watch app's bundle identifier.
*
* @param {String} appPath - The path to the compiled .app directory
* @param {String|Boolean} [watchAppName] - The name of the watch app to find. If value is true, then it will choose the first watch app.
*
* @returns {Object} An object containing the app's id and if `watchAppName` is specified, the watch app's id, OS version, and min OS version.
*/
function getAppInfo(appPath, watchAppName) {
// validate the specified appPath
if (!fs.existsSync(appPath)) {
throw new Error(__('App path does not exist: ' + appPath));
}
// get the app's id
var infoPlist = path.join(appPath, 'Info.plist');
if (!fs.existsSync(infoPlist)) {
throw new Error(__('Unable to find Info.plist in root of specified app path: ' + infoPlist));
}
var plist = readPlist(infoPlist);
if (!plist || !plist.CFBundleIdentifier) {
throw new Error(__('Failed to parse app\'s Info.plist: ' + infoPlist));
}
var results = {
appId: plist.CFBundleIdentifier,
appName: path.basename(appPath).replace(/\.app$/, '')
};
if (watchAppName) {
// look for WatchKit v1 apps
var pluginsDir = path.join(appPath, 'PlugIns');
fs.existsSync(pluginsDir) && fs.readdirSync(pluginsDir).some(function (name) {
var extDir = path.join(pluginsDir, name);
if (fs.existsSync(extDir) && fs.statSync(extDir).isDirectory() && /\.appex$/.test(name)) {
return fs.readdirSync(extDir).some(function (name) {
var appDir = path.join(extDir, name);
if (fs.existsSync(appDir) && fs.statSync(appDir).isDirectory() && /\.app$/.test(name)) {
var plist = readPlist(path.join(appDir, 'Info.plist'));
if (plist && plist.WKWatchKitApp && (typeof watchAppName !== 'string' || fs.existsSync(path.join(appDir, watchAppName)))) {
results.watchAppName = path.basename(appDir).replace(/\.app$/, '');
results.watchAppId = plist.CFBundleIdentifier;
results.watchOSVersion = '1.0';
results.watchMinOSVersion = '1.0';
return true;
}
}
});
}
});
if (!results.watchAppId) {
// look for WatchKit v2 apps
var watchDir = path.join(appPath, 'Watch');
fs.existsSync(watchDir) && fs.readdirSync(watchDir).some(function (name) {
var plist = readPlist(path.join(watchDir, name, 'Info.plist'));
if (plist && (plist.DTPlatformName === 'watchos' || plist.WKWatchKitApp) && (typeof watchAppName !== 'string' || fs.existsSync(path.join(watchDir, watchAppName)))) {
results.watchAppName = name.replace(/\.app$/, '');
results.watchAppId = plist.CFBundleIdentifier;
results.watchOSVersion = plist.DTPlatformVersion;
results.watchMinOSVersion = plist.MinimumOSVersion;
return true;
}
});
}
if (!results.watchAppId) {
if (typeof watchAppName === 'string') {
throw new Error(__('Unable to find a watch app named "%s".', watchAppName));
} else {
throw new Error(__('The launch watch app flag was set, however unable to find a watch app.'));
}
}
}
return results;
}
/**
* Finds a iOS Simulator and/or Watch Simulator as well as the supported Xcode based on the specified options.
*
* @param {Object} [options] - An object containing various settings.
* @param {String} [options.appBeingInstalled] - The path to the iOS app to install after launching the iOS Simulator.
* @param {Boolean} [options.bypassCache=false] - When true, re-detects Xcode and all simulators.
* @param {Function} [options.logger] - A function to log debug messages to.
* @param {String} [options.iosVersion] - The iOS version of the app so that ioslib picks the appropriate Xcode.
* @param {String} [options.minIosVersion] - The minimum iOS SDK to detect.
* @param {String} [options.minWatchosVersion] - The minimum watchOS SDK to detect.
* @param {String|Array<String>} [options.searchPath] - One or more path to scan for Xcode installations.
* @param {String|SimHandle} simHandleOrUDID - A iOS sim handle or the UDID of the iOS Simulator to launch or null if you want ioslib to pick one.
* @param {String} [options.simType=iphone] - The type of simulator to launch. Must be either "iphone" or "ipad". Only applicable when udid is not specified.
* @param {String} [options.simVersion] - The iOS version to boot. Defaults to the most recent version.
* @param {String} [options.supportedVersions] - A string with a version number or range to check if an Xcode install is supported.
* @param {Boolean} [options.watchAppBeingInstalled] - The id of the watch app. Required in order to find a watch simulator.
* @param {String} [options.watchHandleOrUDID] - A watch sim handle or UDID of the Watch Simulator to launch or null if your app has a watch app and you want ioslib to pick one.
* @param {String} [options.watchMinOSVersion] - The min Watch OS version supported by the specified watch app id.
* @param {Function} callback(err, simHandle, watchSimHandle, selectedXcode, simInfo, xcodeInfo) - A function to call with the simulators found.
*/
function findSimulators(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
} else if (typeof options !== 'object') {
options = {};
}
typeof callback === 'function' || (callback = function () {});
// detect xcodes
xcode.detect(options, function (err, xcodeInfo) {
if (err) {
return callback(err);
}
function compareXcodes(a, b) {
var v1 = xcodeInfo.xcode[a].version;
var v2 = xcodeInfo.xcode[b].version;
if (options.iosVersion && appc.version.eq(options.iosVersion, v1)) {
return -1;
}
if (options.iosVersion && appc.version.eq(options.iosVersion, v2)) {
return 1;
}
if (xcodeInfo.xcode[a].selected) {
return -1;
}
if (xcodeInfo.xcode[b].selected) {
return 1;
}
return appc.version.gt(v1, v2) ? -1 : appc.version.eq(v1, v2) ? 0 : 1;
}
// find an Xcode installation that matches the iOS SDK or fall back to the selected Xcode or the latest
var xcodeIds = Object
.keys(xcodeInfo.xcode)
.filter(function (id) {
if (!xcodeInfo.xcode[id].supported) {
return false;
}
if (options.iosVersion && !xcodeInfo.xcode[id].sdks.some(function (ver) { return appc.version.eq(ver, options.iosVersion); })) {
return false;
}
return true;
})
.sort(compareXcodes);
if (!xcodeIds.length) {
if (options.iosVersion) {
return callback(new Error(__('Unable to find any Xcode installations that supports iOS SDK %s.', options.iosVersion)));
} else {
return callback(new Error(__('Unable to find any supported Xcode installations. Please install the latest Xcode.')));
}
}
var xcodeId = xcodeIds[0];
var selectedXcode = xcodeInfo.xcode[xcodeId];
if (!selectedXcode.eulaAccepted) {
var eulaErr = new Error(__(`Xcode ${selectedXcode.version} end-user license agreement has not been accepted. Please launch "${selectedXcode.xcodeapp}" or run "sudo xcodebuild -license" to accept the license`));
return callback(eulaErr);
}
// detect the simulators
detect(options, function (err, simInfo) {
if (err) {
return callback(err);
}
var logger = typeof options.logger === 'function' ? options.logger : function () {},
simHandle = options.simHandleOrUDID instanceof SimHandle ? options.simHandleOrUDID : null,
watchSimHandle = options.watchHandleOrUDID instanceof SimHandle ? options.watchHandleOrUDID : null;
if (options.simHandleOrUDID) {
// validate the udid
if (!(options.simHandleOrUDID instanceof SimHandle)) {
var vers = Object.keys(simInfo.simulators.ios);
logger(__('Validating iOS Simulator UDID %s', options.simHandleOrUDID));
for (var i = 0, l = vers.length; !simHandle && i < l; i++) {
var sims = simInfo.simulators.ios[vers[i]];
for (var j = 0, k = sims.length; j < k; j++) {
if (sims[j].udid === options.simHandleOrUDID) {
logger(__('Found iOS Simulator UDID %s', options.simHandleOrUDID));
simHandle = new SimHandle(sims[j]);
break;
}
}
}
if (!simHandle) {
return callback(new Error(__('Unable to find an iOS Simulator with the UDID "%s".', options.simHandleOrUDID)));
}
}
if (options.minIosVersion && appc.version.lt(simHandle.version, options.minIosVersion)) {
return callback(new Error(__('The selected iOS %s Simulator is less than the minimum iOS version %s.', simHandle.version, options.minIosVersion)));
}
if (options.watchAppBeingInstalled) {
var watchXcodeId = Object
.keys(simHandle.watchCompanion)
.filter(function (xcodeId) {
return xcodeInfo.xcode[xcodeId].supported;
})
.sort(compareXcodes)
.pop();
if (!watchXcodeId) {
return callback(new Error(__('Unable to find any Watch Simulators that can be paired with the specified iOS Simulator %s.', simHandle.udid)));
}
if (!options.watchHandleOrUDID) {
logger(__('Watch app present, autoselecting a Watch Simulator'));
var companions = simHandle.watchCompanion[watchXcodeId];
var companionUDID = Object.keys(companions)
.sort(function (a, b) {
return companions[a].model.localeCompare(companions[b].model);
})
.pop();
watchSimHandle = new SimHandle(companions[companionUDID]);
if (!watchSimHandle) {
return callback(new Error(__('Specified iOS Simulator "%s" does not support Watch apps.', options.simHandleOrUDID)));
}
} else if (!(options.watchHandleOrUDID instanceof SimHandle)) {
logger(__('Watch app present, validating Watch Simulator UDID %s', options.watchHandleOrUDID));
Object.keys(simInfo.simulators.watchos).some(function (ver) {
return simInfo.simulators.watchos[ver].some(function (sim) {
if (sim.udid === options.watchHandleOrUDID) {
logger(__('Found Watch Simulator UDID %s', options.watchHandleOrUDID));
watchSimHandle = new SimHandle(sim);
return true;
}
});
});
if (!watchSimHandle) {
return callback(new Error(__('Unable to find a Watch Simulator with the UDID "%s".', options.watchHandleOrUDID)));
}
}
}
// double check
if (watchSimHandle && !simHandle.watchCompanion[watchXcodeId][watchSimHandle.udid]) {
return callback(new Error(__('Specified Watch Simulator "%s" is not compatible with iOS Simulator "%s".', watchSimHandle.udid, simHandle.udid)));
}
if (options.watchAppBeingInstalled && !options.watchHandleOrUDID && !watchSimHandle) {
if (options.watchMinOSVersion) {
return callback(new Error(__('Unable to find a Watch Simulator that supports watchOS %s.', options.watchMinOSVersion)));
} else {
return callback(new Error(__('Unable to find a Watch Simulator.')));
}
}
logger(__('Selected iOS Simulator: %s', simHandle.name));
logger(__(' UDID = %s', simHandle.udid));
logger(__(' iOS = %s', simHandle.version));
if (watchSimHandle) {
if (options.watchAppBeingInstalled && options.watchHandleOrUDID) {
logger(__('Selected watchOS Simulator: %s', watchSimHandle.name));
} else {
logger(__('Autoselected watchOS Simulator: %s', watchSimHandle.name));
}
logger(__(' UDID = %s', watchSimHandle.udid));
logger(__(' watchOS = %s', watchSimHandle.version));
}
logger(__('Autoselected Xcode: %s', selectedXcode.version));
} else {
logger(__('No iOS Simulator UDID specified, searching for best match'));
if (options.watchAppBeingInstalled && options.watchHandleOrUDID) {
logger(__('Validating Watch Simulator UDID %s', options.watchHandleOrUDID));
Object.keys(simInfo.simulators.watchos).some(function (ver) {
return simInfo.simulators.watchos[ver].some(function (sim) {
if (sim.udid === options.watchHandleOrUDID) {
watchSimHandle = new SimHandle(sim);
logger(__('Found Watch Simulator UDID %s', options.watchHandleOrUDID));
return true;
}
});
});
if (!watchSimHandle) {
return callback(new Error(__('Unable to find a Watch Simulator with the UDID "%s".', options.watchHandleOrUDID)));
}
}
// pick one
logger(__('Scanning Xcodes: %s', xcodeIds.join(' ')));
// loop through xcodes
for (var i = 0; !simHandle && i < xcodeIds.length; i++) {
var xc = xcodeInfo.xcode[xcodeIds[i]];
var simVersMap = {};
Object.keys(simInfo.simulators.ios)
.forEach(function (ver) {
Object.keys(xc.simDevicePairs)
.some(function (iosRange) {
if (appc.version.satisfies(ver, iosRange)) {
simVersMap[ver] = xc.simDevicePairs[iosRange];
return true;
}
});
});
var simVers = appc.version.sort(Object.keys(simVersMap)).reverse();
logger(__('Scanning Xcode %s sims: %s', xcodeIds[i], simVers.join(', ')));
// loop through each xcode simulators
for (var j = 0; !simHandle && j < simVers.length; j++) {
if (!options.minIosVersion || appc.version.gte(simVers[j], options.minIosVersion)) {
var sims = simInfo.simulators.ios[simVers[j]];
sims.sort(compareSims).reverse();
// loop through each simulator
for (var k = 0; !simHandle && k < sims.length; k++) {
if (options.simType && sims[k].family !== options.simType) {
continue;
}
// if we're installing a watch extension, make sure we pick a simulator that supports the watch
if (options.watchAppBeingInstalled) {
if (watchSimHandle) {
Object.keys(sims[k].supportsWatch).forEach(function (xcodeVer) {
if (watchSimHandle.supportsXcode[xcodeVer]) {
selectedXcode = xcodeInfo.xcode[xcodeVer];
simHandle = new SimHandle(sims[k]);
return true;
}
});
} else if (sims[k].supportsWatch[xcodeIds[i]]) {
// make sure this version of Xcode has a watch simulator that supports the watch app version
Object.keys(simInfo.simulators.watchos).some(function (watchosVer) {
return Object.keys(simVersMap[simVers[j]])
.some(function (watchosRange) { // 4.x, 5.x, etc
if (appc.version.satisfies(watchosVer, watchosRange) && appc.version.gte(watchosVer, options.watchMinOSVersion)) {
simHandle = new SimHandle(sims[k]);
selectedXcode = xcodeInfo.xcode[xcodeIds[i]];
const watchSim = simInfo.simulators.watchos[watchosVer].sort(compareSims).reverse()[0];
watchSimHandle = new SimHandle(watchSim);
return true;
}
});
});
}
} else {
// no watch app
logger(__('No watch app being installed, so picking first Simulator'));
simHandle = new SimHandle(sims[k]);
// fallback to the newest supported Xcode version
xcodeIds.some(function (id) {
if (simHandle.supportsXcode[id]) {
selectedXcode = xcodeInfo.xcode[id];
return true;
}
});
}
}
}
}
}
if (!simHandle) {
const helpText = '\n\nPlease open Xcode, navigate to "Window > Devices and Simulators" and create a new Simulator with your preferred configuration.';
// user experience!
if (options.simVersion) {
return callback(new Error(__(`Unable to find an iOS Simulator running iOS %s. ${helpText}`, options.simVersion)));
} else {
return callback(new Error(__(`Unable to find an iOS Simulator. ${helpText}`)));
}
} else if (options.watchAppBeingInstalled && !watchSimHandle) {
return callback(new Error(__('Unable to find a watchOS Simulator that supports watchOS %s', options.watchMinOSVersion)));
}
logger(__('Autoselected iOS Simulator: %s', simHandle.name));
logger(__(' UDID = %s', simHandle.udid));
logger(__(' iOS = %s', simHandle.version));
if (watchSimHandle) {
if (options.watchAppBeingInstalled && options.watchHandleOrUDID) {
logger(__('Selected watchOS Simulator: %s', watchSimHandle.name));
} else {
logger(__('Autoselected watchOS Simulator: %s', watchSimHandle.name));
}
logger(__(' UDID = %s', watchSimHandle.udid));
logger(__(' watchOS = %s', watchSimHandle.version));
}
logger(__('Autoselected Xcode: %s', selectedXcode.version));
}
callback(null, simHandle, watchSimHandle, selectedXcode, simInfo, xcodeInfo);
});
});
}
/**
* Launches the specified iOS Simulator or picks one automatically.
*
* @param {String|SimHandle} simHandleOrUDID - A iOS sim handle or the UDID of the iOS Simulator to launch or null if you want ioslib to pick one.
* @param {Object} [options] - An object containing various settings.
* @param {String} [options.appPath] - The path to the iOS app to install after launching the iOS Simulator.
* @param {Boolean} [options.autoExit=false] - When "appPath" has been specified, causes the iOS Simulator to exit when the autoExitToken has been emitted to the log output.
* @param {String} [options.autoExitToken=AUTO_EXIT] - A string to watch for to know when to quit the iOS simulator when "appPath" has been specified.
* @param {Boolean} [options.bypassCache=false] - When true, re-detects Xcode and all simulators.
* @param {Boolean} [options.focus=true] - Focus the iOS Simulator after launching. Overrides the "hide" option.
* @param {Boolean} [options.hide=false] - Hide the iOS Simulator after launching. Useful for testing. Ignored if "focus" option is set to true.
* @param {String} [options.iosVersion] - The iOS version of the app so that ioslib picks the appropriate Xcode.
* @param {Boolean} [options.killIfRunning] - Kill the iOS Simulator if already running.
* @param {String} [options.launchBundleId] - Launches a specific app when the simulator loads. When installing an app, defaults to the app's id unless `launchWatchApp` is set to true.
* @param {Boolean} [options.launchWatchApp=false] - When true, launches the specified app's watch app on an external display and the main app.
* @param {Boolean} [options.launchWatchAppOnly=false] - When true, launches the specified app's watch app on an external display and not the main app.
* @param {String} [options.logFilename] - The name of the log file to search for in the iOS Simulator's "Documents" folder. This file is created after the app is started.
* @param {Number} [options.logServerPort] - The TCP port to connect to get log messages.
* @param {String} [options.minIosVersion] - The minimum iOS SDK to detect.
* @param {String} [options.minWatchosVersion] - The minimum watchOS SDK to detect.
* @param {String|Array<String>} [options.searchPath] - One or more path to scan for Xcode installations.
* @param {String} [options.simType=iphone] - The type of simulator to launch. Must be either "iphone" or "ipad". Only applicable when udid is not specified.
* @param {String} [options.simVersion] - The iOS version to boot. Defaults to the most recent version.
* @param {String} [options.supportedVersions] - A string with a version number or range to check if an Xcode install is supported.
* @param {Boolean} [options.uninstallApp=false] - When true and `appPath` is specified, uninstalls the app before installing the new app. If app is not installed already, it continues.
* @param {String} [options.watchAppName] - The name of the watch app to install. If omitted, automatically picks the watch app.
* @param {String} [options.watchHandleOrUDID] - A watch sim handle or the UDID of the Watch Simulator to launch or null if your app has a watch app and you want ioslib to pick one.
* @param {Function} [callback(err, simHandle)] - A function to call when the simulator has launched.
*
* @emits module:simulator#app-quit
* @emits module:simulator#app-started
* @emits module:simulator#error
* @emits module:simulator#exit
* @emits module:simulator#launched
* @emits module:simulator#log
* @emits module:simulator#log-debug
* @emits module:simulator#log-error
* @emits module:simulator#log-file
* @emits module:simulator#log-raw
*
* @returns {Handle}
*/
function launch(simHandleOrUDID, options, callback) {
return magik(options, callback, function (emitter, options, callback) {
emitter.stop = function () {}; // for stopping logging
if (!options.appPath && (options.launchWatchApp || options.launchWatchAppOnly)) {
var err = new Error(
options.launchWatchAppOnly
? __('You must specify an appPath when launchWatchApp is true.')
: __('You must specify an appPath when launchWatchAppOnly is true.')
);
emitter.emit('error', err);
return callback(err);
}
if (options.logServerPort && (typeof options.logServerPort !== 'number' || options.logServerPort < 1 || options.logServerPort > 65535)) {
var err = new Error(__('Log server port must be a number between 1 and 65535'));
emitter.emit('error', err);
return callback(err);
}
var appId,
watchAppId,
findSimOpts = appc.util.mix({
simHandleOrUDID: simHandleOrUDID,
logger: function (msg) {
emitter.emit('log-debug', msg);
}
}, options);
if (options.appPath) {
findSimOpts.appBeingInstalled = true;
try {
var ids = getAppInfo(options.appPath, options.watchAppName || !!options.launchWatchApp || !!options.launchWatchAppOnly);
if (!options.launchBundleId) {
appId = ids.appId;
}
if (ids.watchAppId) {
watchAppId = ids.watchAppId;
if (findSimOpts) {
findSimOpts.watchAppBeingInstalled = true;
findSimOpts.watchMinOSVersion = ids.watchMinOSVersion;
}
emitter.emit('log-debug', __('Found watchOS %s app: %s', ids.watchOSVersion, watchAppId));
}
} catch (ex) {
emitter.emit('error', ex);
return callback(ex);
}
} else if (options.launchBundleId) {
appId = options.launchBundleId;
}
findSimulators(findSimOpts, function (err, simHandle, watchSimHandle, selectedXcode, detectedSimInfo) {
if (err) {
emitter.emit('error', err);
return callback(err);
}
if (!selectedXcode.eulaAccepted) {
var eulaErr = new Error(__('Xcode must be launched and the EULA must be accepted before a simulator can be launched.'));
emitter.emit('error', eulaErr);
return callback(eulaErr);
}
var crashFileRegExp,
existingCrashes = getCrashes(),
findLogTimer = null,
logFileTail;
if (options.appPath) {
crashFileRegExp = new RegExp('^' + ids.appName + '_\\d{4}\\-\\d{2}\\-\\d{2}\\-\\d{6}_.*\.crash$'),
simHandle.appName = ids.appName;
watchSimHandle && (watchSimHandle.appName = ids.watchAppName);
}
// sometimes the simulator doesn't remove old log files in which case we get
// our logging jacked - we need to remove them before running the simulator
if (options.logFilename && simHandle.dataDir) {
(function walk(dir) {
var logFile = path.join(dir, 'Documents', options.logFilename);
if (fs.existsSync(logFile)) {
emitter.emit('log-debug', __('Removing old log file: %s', logFile));
fs.unlinkSync(logFile);
return true;
}
if (fs.existsSync(dir)) {
return fs.readdirSync(dir).some(function (name) {
var subdir = path.join(dir, name);
if (!fs.existsSync(subdir)) {
return;
}
var subdirStats = fs.lstatSync(subdir);
if (subdirStats.isDirectory() && !subdirStats.isSymbolicLink()) {
return walk(subdir);
}
});
}
}(simHandle.dataDir));
}
var cleanupOnce = false;
function cleanupAndEmit(evt) {
if (!cleanupOnce) {
cleanupOnce = true;
}
simHandle.systemLogTail && simHandle.systemLogTail.unwatch();
simHandle.systemLogTail = null;
if (watchSimHandle) {
watchSimHandle.systemLogTail && watchSimHandle.systemLogTail.unwatch();
watchSimHandle.systemLogTail = null;
}
emitter.emit.apply(emitter, arguments);
}
function getCrashes() {
if (crashFileRegExp && fs.existsSync(detectedSimInfo.simulators.crashDir)) {
return fs.readdirSync(detectedSimInfo.simulators.crashDir).filter(function (n) { return crashFileRegExp.test(n); });
}
return [];
}
function checkIfCrashed() {
var crashes = getCrashes(),
diffCrashes = crashes
.filter(function (file) {
return existingCrashes.indexOf(file) === -1;
})
.map(function (file) {
return path.join(detectedSimInfo.simulators.crashDir, file);
})
.sort();
if (diffCrashes.length) {
// when a crash occurs, we need to provide the plist crash information as a result object
diffCrashes.forEach(function (crashFile) {
emitter.emit('log-debug', __('Detected crash file: %s', crashFile));
});
cleanupAndEmit('app-quit', new SimulatorCrash(diffCrashes));
return true;
}
return false;
}
function startSimulator(handle) {
var booted = false,
simEmitter = new EventEmitter;
function simExited(code, signal) {
if (code || code === 0) {
emitter.emit('log-debug', __('%s Simulator has exited with code %s', handle.name, code));
} else {
emitter.emit('log-debug', __('%s Simulator has exited', handle.name));
}
handle.systemLogTail && handle.systemLogTail.unwatch();
handle.systemLogTail = null;
simEmitter.emit('stop', code);
}
async.series([
function checkIfRunningAndBooted(next) {
emitter.emit('log-debug', __('Checking if the simulator %s is already running', handle.simulator));
isSimulatorRunning(handle.simulator, function (err, pid, udid) {
if (err) {
emitter.emit('log-debug', __('Failed to check if the simulator is running: %s', err.message || err.toString()));
return next(err);
}
if (!pid) {
emitter.emit('log-debug', __('Simulator is not running'));
return next();
}
emitter.emit('log-debug', __('Simulator is running (pid %s)', pid));
// if Xcode 8 or older and the udid doesn't match the running version, then we need to kill the simulator before continuing
if (appc.version.lt(selectedXcode.version, '9.0') && udid !== handle.udid) {
emitter.emit('log-debug', __('%s Simulator is running, but not the UDID we want, stopping simulator', handle.name));
stop(handle, next);
return;
}
simctl.getSim({
simctl: handle.simctl,
udid: handle.udid
}, function (err, sim) {
if (err) {
return next(err);
}
if (!sim) {
// this should never happen
return next(new Error(__('Unable to find simulator %s', handle.udid)));
}
function waitToBoot() {
emitter.emit('log-debug', __('Waiting for simulator to boot...'));
simctl.waitUntilBooted({ simctl: handle.simctl, udid: handle.udid, timeout: 30000 }, function (err, _booted) {
if (err && err.code !== 666) {
emitter.emit('log-debug', __('Error while waiting for simulator to boot: %s', err.message || err.toString()));
return next(err);
}
booted = _booted;
emitter.emit('log-debug', booted ? __('Simulator is booted!') : __('Simulator is NOT booted!'));
if (err || !booted) {
emitter.emit('log-debug', __('%s Simulator is running, but not in a booted state, stopping simulator', handle.name));
stop(handle, next);
return;
}
emitter.emit('log-debug', __('%s Simulator already running with the correct UDID', handle.name));
// because we didn't start the simulator, we have no child process to
// listen for when it exits, so we need to monitor it ourselves
setTimeout(function check() {
appc.subprocess.run('ps', ['-p', pid], function (code, out, err) {
if (code) {
simExited();
} else {
setTimeout(check, 1000);
}
});
}, 1000);
next();
});
}
if (appc.version.lt(selectedXcode.version, '9.0')) {
if (/^shutdown/i.test(sim.state)) {
// the udid that is supposed to be running isn't, kill the simulator
emitter.emit('log-debug', __('%s Simulator is running, but UDID %s is shut down, stopping simulator', handle.name, handle.udid));
stop(handle, next);
return;
}
return waitToBoot();
}
// Xcode 9+ path
if (/^booted/i.test(sim.state)) {
return waitToBoot();
}
emitter.emit('log-debug', __('Getting all running simulator runtimes'));
getRunningSimulatorDevices(function (err, sims) {
if (err) {
return next(err);
}
if (sims.some(function (s) { return s.udid === handle.udid; } )) {
return waitToBoot();
}
simctl.boot({ simctl: handle.simctl, udid: handle.udid }, function (err) {
if (err) {
return next(err);
}
waitToBoot();
});
});
});
});
},
function tailSystemLog(next) {
if (!handle.systemLog) {
return next();
}
// make sure the system log exists
if (!fs.existsSync(handle.systemLog)) {
var dir = path.dirname(handle.systemLog);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(handle.systemLog, '');
}
var systemLogRegExp = new RegExp(' ' + handle.appName + '\\[(\\d+)\\]: (.*)'),
watchLogMsgRegExp = handle.type === 'ios' && watchAppId ? new RegExp('companionappd\\[(\\d+)\\]: \\((.+)\\) WatchKit: application \\(' + watchAppId + '\\),?\w*(.*)') : null,
xcode73WatchLogMsgRegExp = handle.type === 'ios' && watchAppId ? new RegExp('Installation of ' + watchAppId + ' (.*).') : null,
watchInstallRegExp = /install status: (\d+), message: (.*)$/,
successRegExp = /succeeded|success/i,
crash1RegExp = /^\*\*\* Terminating app/, // objective-c issue
crash2RegExp = new RegExp(' (SpringBoard|Carousel)\\[(\\d+)\\]: Application \'UIKitApplication:' + appId + '\\[(\\w+)\\]\' crashed'), // c++ issue
crash3RegExp = new RegExp('launchd_sim\\[(\\d+)\\] \\(UIKitApplication:' + appId + '\\[(\\w+)\\]'), // killed by ios or seg fault
autoExitToken = options.autoExitToken || 'AUTO_EXIT',
detectedCrash = false;
emitter.emit('log-debug', __('Tailing %s Simulator system log: %s', handle.name, handle.systemLog));
// tail the simulator's system log.
// as we do this, we want to look for specific things like the watch being installed,
// and the app starting.
handle.systemLogTail = new Tail(handle.systemLog, '\n', { interval: 500 }, /* fromBeginning */ false );
handle.systemLogTail.on('line', function (line) {
var m;
emitter.emit('log-raw', line, handle);
if (!booted || !handle.installing) {
return;
}
if (xcode73WatchLogMsgRegExp) {
if (m = line.match(xcode73WatchLogMsgRegExp)) {
if (m[1] === 'acknowledged') {
emitter.emit('log-debug', __('Watch App installed successfully!'));
handle.installed = true;
} else {
simEmitter.emit('error', new Error(__('Watch App installation failure')));
}
return;
}
}
if (watchLogMsgRegExp) {
if (m = line.match(watchLogMsgRegExp)) {
// refine our regex now that we have the pid
watchLogMsgRegExp = new RegExp('companionappd\\[(' + m[1] + ')\\]: \\((.+)\\) WatchKit: (.*)$');
var type = m[2].trim().toLowerCase(),
msg = m[3].trim();
if (type === 'note') {
// did the watch app install succeed?
if (!handle.installed && (m = msg.match(watchInstallRegExp)) && parseInt(m[1]) === 2 && successRegExp.test(m[2])) {
emitter.emit('log-debug', __('Watch App installed successfully!'));
handle.installed = true;
}
} else if (type === 'error') {
// did the watch app install fail?
simEmitter.emit('error', new Error(__('Watch App installation failure: %s', msg)));
}
return;
}
}
if (handle.appStarted) {
m = line.match(systemLogRegExp);
if (m) {
if (handle.type === 'watchos' && m[2].indexOf('(Error) WatchKit:') !== -1) {
emitter.emit('log-error', m[2], handle);
return;
}
// if we have a log server port and we're currently the iOS Simulator,
// then ignore all messages in the system.log in favor of the log server
if (!options.logServerPort || handle.type === 'watchos') {
emitter.emit('log', m[2], handle);
}
if (options.autoExit && m[2].indexOf(autoExitToken) !== -1) {
emitter.emit('log-debug', __('Found "%s" token, stopping simulator', autoExitToken));
// stopping the simulator will cause the "close" event to fire
stop(handle, function () {
cleanupAndEmit('app-quit');
});
return;
}
}
// check for an iPhone app crash
if (!detectedCrash && handle.type === 'ios' && ((m && crash1RegExp.test(m[2])) || crash2RegExp.test(line) || crash3RegExp.test(line))) {
detectedCrash = true;
// wait 1 second for the potential crash log to be written
setTimeout(function () {
// did we crash?
if (!checkIfCrashed()) {
// well something happened, exit
emitter.emit('log-debug', __('Detected crash, but no crash file'));
cleanupAndEmit('app-quit');
}
}, 1000);
}
}
});
handle.systemLogTail.watch();
next();
},
function shutdownJustInCase(next) {
if (booted) {
return next();
}
simctl.shutdown({ simctl: handle.simctl, udid: handle.udid }, next);
},
function startTheSimulator(next) {
if (booted) {
return next();
}
if (!handle.simulator) {
emitter.emit('log-debug', __('Cannot run simulator %s because executable was not found', handle.udid));
return next();
}
// not running, start the simulator
emitter.emit('log-debug', __('Running: %s', handle.simulator + ' -CurrentDeviceUDID ' + handle.udid));
var child = spawn(handle.simulator, ['-CurrentDeviceUDID', handle.udid], { detached: true, stdio: 'ignore' });
child.on('close', simExited);
child.unref();
// wait for the simulator to boot
async.whilst(
function (cb) { return cb(null, !booted); },
function (cb) {
list(options, function (err, info) {
Object.keys(info.devices).some(function (type) {
return info.devices[type].some(function (sim) {
if (sim.udid === handle.udid) {
if (/^booted$/i.test(sim.state)) {
booted = true;
}
return true;
}
});
});
if (booted) {
emitter.emit('log-debug', __('Simulator is booted'));
return cb();
}
setTimeout(function () {
cb();
}, 250);
});
},
function (err) {
if (!err) {
emitter.emit('log-debug', __('%s Simulator started', handle.name));
}
next(err);
}
);
}
], function (err) {
simEmitter.emit('start', err);
});
return simEmitter;
}
async.series([
function stopIosSim(next) {
// check if we need to stop the iOS simulator
if (options.killIfRunning !== false) {
emitter.emit('log-debug', __('Stopping iOS Simulator, if running'));
stop(simHandle, next);
} else {
next();
}
},
function stopWatchSim(next) {
// check if we need to stop the watchOS simulator
if (watchSimHandle && options.killIfRunning !== false && appc.version.gte(watchSimHandle.version, '2.0')) {
emitter.emit('log-debug', __('Stopping watchOS Simulator, if running'));
stop(watchSimHandle, next);
} else {
next();
}
},
function pairIosAndWatchSims(next) {
// check if we need to pair devices
if (!watchSimHandle) {
// no need to pair
return next();
}
if (appc.version.lt(watchSimHandle.version, '2.0')) {
// no need to pair
emitter.emit('log-debug', __('No need to pair WatchKit 1.x app'));
return next();
}
list(options, function (err, info) {
if (err) {
return next(err);
}
var found = info.iosSimToWatchSimToPair[simHandle.udid] && info.iosSimToWatchSimToPair[simHandle.udid][watchSimHandle.udid];
if (found && found.active) {
emitter.emit('log-debug', __('iOS and watchOS simulators already paired and active'));
return next();
}
if (found) {
emitter.emit('log-debug', __('Activating iOS and watchOS simulator pair: %s', found.udid));
return simctl.activatePair({ simctl: simHandle.simctl, udid: found.udid }, next);
}