first commit

This commit is contained in:
2026-09-08 20:28:54 +08:00
commit 2ada8d3d5a
37380 changed files with 4886169 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
import semver from 'semver';
const DEFAULT_EXEC_TIMEOUT = 10 * 60 * 1000; // ms
const SIM_RUNTIME_NAME = 'com.apple.CoreSimulator.SimRuntime.';
/**
* "Normalize" the version, since iOS uses 'major.minor' but the runtimes can
* be 'major.minor.patch'
*
* @param {string} version - the string version
* @return {string} The version in 'major.minor' form
* @throws {Error} If the version not parseable by the `semver` package
*/
function normalizeVersion (version) {
const semverVersion = semver.coerce(version);
if (!semverVersion) {
throw new Error(`Unable to parse version '${version}'`);
}
return `${semverVersion.major}.${semverVersion.minor}`;
}
export {
DEFAULT_EXEC_TIMEOUT, SIM_RUNTIME_NAME,
normalizeVersion,
};
export const getXcrunBinary = () => process.env.XCRUN_BINARY || 'xcrun';
+17
View File
@@ -0,0 +1,17 @@
import npmlog from 'npmlog';
const LOG_PREFIX = 'simctl';
function getLogger () {
const logger = global._global_npmlog || npmlog;
if (!logger.debug) {
logger.addLevel('debug', 1000, { fg: 'blue', bg: 'black' }, 'dbug');
}
return logger;
}
const log = getLogger();
export { LOG_PREFIX };
export default log;
+171
View File
@@ -0,0 +1,171 @@
import _ from 'lodash';
import subcommands from './subcommands/index.js';
import which from 'which';
import log, { LOG_PREFIX } from './logger';
import {
DEFAULT_EXEC_TIMEOUT, getXcrunBinary,
} from './helpers';
import { exec as tpExec, SubProcess } from 'teen_process';
const SIMCTL_ENV_PREFIX = 'SIMCTL_CHILD_';
const DEFAULT_OPTS = {
xcrun: {
path: null,
},
execTimeout: DEFAULT_EXEC_TIMEOUT,
logErrors: true,
};
/**
* @typedef {Object} ExecOpts
* @property {Array.<string>} args [[]] - The list of additional subcommand arguments.
* It's empty by default.
* @property {Object} env [{}] - Environment variables mapping. All these variables
* will be passed Simulator and used in the executing function.
* @property {boolean} logErrors [true] - Set it to _false_ to throw execution errors
* immediately without logging any additional information.
* @property {boolean} asynchronous [false] - Whether to execute the given command
* 'synchronously' or 'asynchronously'. Affects the returned result of the function.
* @property {?string} encoding - Explicitly sets streams encoding for the executed
* command input and outputs.
*/
/**
* @typedef {Object} SimctlOpts
* @property {?Object} xcrun - The xcrun properties. Currently only one property
* is supported, which is `path` and it by default contains `null`, which enforces
* the instance to automatically detect the full path to `xcrun` tool and to throw
* an exception if it cannot be detected. If the path is set upon instance creation
* then it is going to be used by `exec` and no autodetection will happen.
* @property {?number} execTimeout [600000] - The maximum number of milliseconds
* to wait for single synchronous xcrun command.
* @property {?boolean} logErrors [true] - Whether to wire xcrun error messages
* into debug log before throwing them.
* @property {?string} udid [null] - The unique identifier of the current device, which is
* going to be implicitly passed to all methods, which require it. It can either be set
* upon instance creation if it is already known in advance or later when/if needed via the
* corresponding instance setter.
* @property {?string} devicesSetPath - Full path to the set of devices that you want to manage.
* By default this path usually equals to ~/Library/Developer/CoreSimulator/Devices
*/
class Simctl {
/**
* @param {?SimctlOpts} opts
*/
constructor (opts = {}) {
opts = _.cloneDeep(opts);
_.defaultsDeep(opts, DEFAULT_OPTS);
for (const key of _.keys(DEFAULT_OPTS)) {
this[key] = opts[key];
}
this._udid = _.isNil(opts.udid) ? null : opts.udid;
this._devicesSetPath = _.isNil(opts.devicesSetPath) ? null : opts.devicesSetPath;
}
set udid (value) {
this._udid = value;
}
get udid () {
return this._udid;
}
set devicesSetPath (value) {
this._devicesSetPath = value;
}
get devicesSetPath () {
return this._devicesSetPath;
}
requireUdid (commandName = null) {
if (!this.udid) {
throw new Error(`udid is required to be set for ` +
(commandName ? `the '${commandName}' command` : 'this simctl command'));
}
return this.udid;
}
async requireXcrun () {
const xcrunBinary = getXcrunBinary();
if (!this.xcrun.path) {
try {
this.xcrun.path = await which(xcrunBinary);
} catch (e) {
throw new Error(`${xcrunBinary} tool has not been found in PATH. ` +
`Are Xcode developers tools installed?`);
}
}
return this.xcrun.path;
}
/**
* Execute the particular simctl command.
*
* @param {string} subcommand - One of available simctl subcommands.
* Execute `xcrun simctl` in Terminal to see the full list
* of available subcommands.
* @param {?ExecOpts} opts
* @return {ExecResult|SubProcess} Either the result of teen process's `exec` or
* `SubProcess` instance depending of `opts.asynchronous` value.
* @throws {Error} If the simctl subcommand command returns non-zero return code.
*/
async exec (subcommand, opts = {}) {
let {
args = [],
env = {},
asynchronous = false,
encoding,
logErrors = true,
} = opts;
// run a particular simctl command
args = ['simctl',
...(this.devicesSetPath ? ['--set', this.devicesSetPath] : []),
subcommand,
...args
];
// Prefix all passed in environment variables with 'SIMCTL_CHILD_', simctl
// will then pass these to the child (spawned) process.
env = _.defaults(
_.mapKeys(env,
(value, key) => _.startsWith(key, SIMCTL_ENV_PREFIX) ? key : `${SIMCTL_ENV_PREFIX}${key}`),
process.env);
const execOpts = {
env,
encoding,
};
if (!asynchronous) {
execOpts.timeout = this.execTimeout;
}
const xcrun = await this.requireXcrun();
try {
return asynchronous ? new SubProcess(xcrun, args, execOpts) : await tpExec(xcrun, args, execOpts);
} catch (e) {
if (!this.logErrors || !logErrors) {
// if we don't want to see the errors, just throw and allow the calling
// code do what it wants
} else if (e.stderr) {
const msg = `Error running '${subcommand}': ${e.stderr.trim()}`;
log.debug(LOG_PREFIX, msg);
e.message = msg;
} else {
log.debug(LOG_PREFIX, e.message);
}
throw e;
}
}
}
// add all the subcommands to the Simctl prototype
for (const [fnName, fn] of _.toPairs(subcommands)) {
Simctl.prototype[fnName] = fn;
}
export default Simctl;
export { Simctl };
+20
View File
@@ -0,0 +1,20 @@
const commands = {};
/**
* Add the particular media file to Simulator's library.
* It is required that Simulator is in _booted_ state.
*
* @param {string} filePath - Full path to a media file on the local
* file system.
* @return {ExecResult} Command execution result.
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
commands.addMedia = async function addMedia (filePath) {
return await this.exec('addmedia', {
args: [this.requireUdid('addmedia'), filePath],
});
};
export default commands;
+50
View File
@@ -0,0 +1,50 @@
const commands = {};
/**
* Invoke hidden appinfo subcommand to get the information
* about applications installed on Simulator, including
* system applications ({@link getAppContainer} does not "see" such apps).
* Simulator server should be in 'booted' state for this call to work properly.
* The tool is only available since Xcode SDK 8.1
*
* @param {string} bundleId - The bundle identifier of the target application.
* @return {string} The information about installed application.
*
* Example output for non-existing application container:
* <pre>
* {
* CFBundleIdentifier = "com.apple.MobileSafari";
* GroupContainers = {
* };
* SBAppTags = (
* );
* }
* </pre>
*
* Example output for an existing system application container:
* <pre>
* {
* ApplicationType = Hidden;
* Bundle = "file:///Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/CoreServices/SpringBoard.app";
* CFBundleDisplayName = SpringBoard;
* CFBundleExecutable = SpringBoard;
* CFBundleIdentifier = "com.apple.springboard";
* CFBundleName = SpringBoard;
* CFBundleVersion = 50;
* GroupContainers = {
* };
* Path = "/Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/CoreServices/SpringBoard.app";
* SBAppTags = (
* );
* }
* </pre>
* @throws {Error} If the `udid` instance property is unset
*/
commands.appInfo = async function appInfo (bundleId) {
const {stdout} = await this.exec('appinfo', {
args: [this.requireUdid('appinfo'), bundleId],
});
return (stdout || '').trim();
};
export default commands;
+27
View File
@@ -0,0 +1,27 @@
import _ from 'lodash';
import log, { LOG_PREFIX } from '../logger';
const commands = {};
/**
* Boot the particular Simulator if it is not running.
*
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
commands.bootDevice = async function bootDevice () {
try {
await this.exec('boot', {
args: [this.requireUdid('boot')]
});
} catch (e) {
if (_.includes(e.message, 'Unable to boot device in current state: Booted')) {
throw e;
}
log.debug(LOG_PREFIX, `Simulator already in 'Booted' state. Continuing`);
}
};
export default commands;
+113
View File
@@ -0,0 +1,113 @@
import log from '../logger';
import { waitForCondition } from 'asyncbox';
const commands = {};
/**
* @typedef {Object} BootMonitorOptions
* @property {?number} timeout [240000] - Simulator booting timeout in ms.
* @property {?Function} onWaitingDataMigration - This event is fired when data migration stage starts.
* @property {?Function} onWaitingSystemApp - This event is fired when system app wait stage starts.
* @property {?Function} onFinished - This event is fired when Simulator is fully booted.
* @property {?Function} onError - This event is fired when there was an error while monitoring the booting process
* or when the timeout has expired.
* @property {?boolean} shouldPreboot [false] Whether to preboot the Simulator
* if this command is called and it is not already in booted or booting state.
*/
/**
* Start monitoring for boot status of the particular Simulator.
* If onFinished property is not set then the method will block
* until Simulator booting is completed.
* The method is only available since Xcode8.
*
* @param {?BootMonitorOptions} opts - Monitoring options.
* @returns {SubProcess} The instance of the corresponding monitoring process.
* @throws {Error} If the Simulator fails to finish booting within the given timeout and onFinished
* property is not set.
* @throws {Error} If the `udid` instance property is unset
*/
commands.startBootMonitor = async function startBootMonitor (opts = {}) {
const {
timeout = 240000,
onWaitingDataMigration,
onWaitingSystemApp,
onFinished,
onError,
shouldPreboot,
} = opts;
const udid = this.requireUdid('bootstatus');
let status = '';
let isBootingFinished = false;
let error = null;
let timeoutHandler = null;
const args = [udid];
if (shouldPreboot) {
args.push('-b');
}
const bootMonitor = await this.exec('bootstatus', {
args,
asynchronous: true,
});
bootMonitor.on('output', (stdout, stderr) => {
status += stdout || stderr;
if (stdout) {
if (stdout.includes('Waiting on Data Migration') && onWaitingDataMigration) {
onWaitingDataMigration();
} else if (stdout.includes('Waiting on System App') && onWaitingSystemApp) {
onWaitingSystemApp();
}
}
});
bootMonitor.on('exit', (code, signal) => {
if (timeoutHandler) {
clearTimeout(timeoutHandler);
}
if (code === 0) {
if (onFinished) {
onFinished();
}
isBootingFinished = true;
} else {
status = status || signal;
error = new Error(status);
if (onError) {
onError(error);
}
}
});
await bootMonitor.start(0);
const stopMonitor = async () => {
if (bootMonitor.isRunning) {
try {
await bootMonitor.stop();
} catch (e) {
log.warn(e.message);
}
}
};
const start = process.hrtime();
if (onFinished) {
timeoutHandler = setTimeout(stopMonitor, timeout);
} else {
try {
await waitForCondition(() => {
if (error) {
throw error;
}
return isBootingFinished;
}, {waitMs: timeout, intervalMs: 500});
} catch (err) {
await stopMonitor();
const [seconds] = process.hrtime(start);
throw new Error(
`The simulator ${udid} has failed to finish booting after ${seconds}s. ` +
`Original status: ${status}`);
}
}
return bootMonitor;
};
export default commands;
+117
View File
@@ -0,0 +1,117 @@
import _ from 'lodash';
import log, { LOG_PREFIX } from '../logger';
import { retryInterval } from 'asyncbox';
import { SIM_RUNTIME_NAME, normalizeVersion } from '../helpers';
const SIM_RUNTIME_NAME_SUFFIX_IOS = 'iOS';
const DEFAULT_CREATE_SIMULATOR_TIMEOUT = 10000;
const commands = {};
/**
* @typedef {Object} SimCreationOpts
* @property {string} platform [iOS] - Platform name in order to specify runtime such as 'iOS', 'tvOS', 'watchOS'
* @property {number} timeout [10000] - The maximum number of milliseconds to wait
* unit device creation is completed.
*/
/**
* Create Simulator device with given name for the particular
* platform type and version.
*
* @param {string} name - The device name to be created.
* @param {string} deviceTypeId - Device type, for example 'iPhone 6'.
* @param {string} platformVersion - Platform version, for example '10.3'.
* @param {?SimCreationOpts} opts - Simulator options for creating devices.
* @return {string} The UDID of the newly created device.
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
*/
commands.createDevice = async function createDevice (name, deviceTypeId, platformVersion, opts = {}) {
const {
platform = SIM_RUNTIME_NAME_SUFFIX_IOS,
timeout = DEFAULT_CREATE_SIMULATOR_TIMEOUT
} = opts;
let runtimeIds = [];
// Try getting runtimeId using JSON flag
try {
runtimeIds.push(await this.getRuntimeForPlatformVersionViaJson(platformVersion, platform));
} catch (ign) {}
if (_.isEmpty(runtimeIds)) {
// at first make sure that the runtime id is the right one
// in some versions of Xcode it will be a patch version
let runtimeId;
try {
runtimeId = await this.getRuntimeForPlatformVersion(platformVersion, platform);
} catch (err) {
log.warn(`Unable to find runtime for iOS '${platformVersion}'. Continuing`);
runtimeId = platformVersion;
}
// get the possible runtimes, which will be iterated over
// start with major-minor version
let potentialRuntimeIds = [normalizeVersion(runtimeId)];
if (runtimeId.split('.').length === 3) {
// add patch version if it exists
potentialRuntimeIds.push(runtimeId);
}
// add modified versions, since modern Xcodes use this, then the bare
// versions, to accomodate older Xcodes
runtimeIds.push(
...(potentialRuntimeIds.map((id) => `${SIM_RUNTIME_NAME}${platform}-${id.replace(/\./g, '-')}`)),
...potentialRuntimeIds
);
}
// go through the runtime ids and try to create a simulator with each
let udid;
for (const runtimeId of runtimeIds) {
log.debug(LOG_PREFIX,
`Creating simulator with name '${name}', device type id '${deviceTypeId}' and runtime id '${runtimeId}'`);
try {
const {stdout} = await this.exec('create', {
args: [name, deviceTypeId, runtimeId]
});
udid = stdout.trim();
break;
} catch (ign) {
// the error gets logged in `simExec`
}
}
if (!udid) {
throw new Error(`Could not create simulator with name '${name}', device ` +
`type id '${deviceTypeId}', with runtime ids ` +
`${runtimeIds.map((id) => `'${id}'`).join(', ')}`);
}
// make sure that it gets out of the "Creating" state
const retries = parseInt(timeout / 1000, 10);
await retryInterval(retries, 1000, async () => {
const devices = _.values(await this.getDevices());
for (const deviceArr of _.values(devices)) {
for (const device of deviceArr) {
if (device.udid === udid) {
if (device.state === 'Creating') {
// need to retry
throw new Error(`Device with udid '${udid}' still being created`);
} else {
// stop looking, we're done
return;
}
}
}
}
throw new Error(`Device with udid '${udid}' not yet created`);
});
return udid;
};
export default commands;
+16
View File
@@ -0,0 +1,16 @@
const commands = {};
/**
* Delete the particular Simulator from available devices list.
*
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
commands.deleteDevice = async function deleteDevice () {
await this.exec('delete', {
args: [this.requireUdid('delete')]
});
};
export default commands;
+25
View File
@@ -0,0 +1,25 @@
import { retryInterval } from 'asyncbox';
const commands = {};
/**
* Reset the content and settings of the particular Simulator.
* It is required that Simulator is in _shutdown_ state.
*
* @param {number} timeout [10000] - The maximum number of milliseconds to wait
* unit device reset is completed.
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
commands.eraseDevice = async function eraseDevice (timeout = 1000) {
// retry erase with a sleep in between because it's flakey
const retries = parseInt(timeout / 200, 10);
await retryInterval(retries, 200,
async () => await this.exec('erase', {
args: [this.requireUdid('erase')]
})
);
};
export default commands;
+29
View File
@@ -0,0 +1,29 @@
const commands = {};
/**
* Get the full path to the particular application container
* on the local file system. Note, that this subcommand throws
* an error if bundle id of a system application is provided,
* like 'com.apple.springboard'.
* It is required that Simulator is in _booted_ state.
*
* @param {string} bundleId - Bundle identifier of an application.
* @param {?string} containerType - Which container type to return. Possible values
* are 'app', 'data', 'groups', '<A specific App Group container>'.
* The default value is 'app'.
* @return {string} Full path to the given application container on the local
* file system.
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
commands.getAppContainer = async function getAppContainer (bundleId, containerType = null) {
const args = [this.requireUdid('get_app_container'), bundleId];
if (containerType) {
args.push(containerType);
}
const {stdout} = await this.exec('get_app_container', {args});
return (stdout || '').trim();
};
export default commands;
+20
View File
@@ -0,0 +1,20 @@
const commands = {};
/**
* Retrieves the value of a Simulator environment variable
*
* @param {string} varName - The name of the variable to be retrieved
* @returns {?string} The value of the variable or null if the given variable
* is not present in the Simulator environment
* @throws {Error} If there was an error while running the command
* @throws {Error} If the `udid` instance property is unset
*/
commands.getEnv = async function getEnv (varName) {
const {stdout, stderr} = await this.exec('getenv', {
args: [this.requireUdid('getenv'), varName],
logErrors: false,
});
return stderr ? null : stdout;
};
export default commands;
+54
View File
@@ -0,0 +1,54 @@
import addmediaCommands from './addmedia';
import appinfoCommands from './appinfo';
import bootCommands from './boot';
import bootstatusCommands from './bootstatus';
import createCommands from './create';
import deleteCommands from './delete';
import eraseCommands from './erase';
import getappcontainerCommands from './get_app_container';
import installCommands from './install';
import ioCommands from './io';
import keychainCommands from './keychain';
import launchCommands from './launch';
import listCommands from './list';
import openurlCommands from './openurl';
import pbcopyCommands from './pbcopy';
import pbpasteCommands from './pbpaste';
import privacyCommands from './privacy';
import pushCommands from './push';
import envCommands from './getenv';
import shutdownCommands from './shutdown';
import spawnCommands from './spawn';
import terminateCommands from './terminate';
import uiCommands from './ui';
import uninstallCommands from './uninstall';
// xcrun simctl --help
const subcommands = Object.assign({},
addmediaCommands,
appinfoCommands,
bootCommands,
bootstatusCommands,
createCommands,
deleteCommands,
eraseCommands,
getappcontainerCommands,
installCommands,
ioCommands,
keychainCommands,
launchCommands,
listCommands,
openurlCommands,
pbcopyCommands,
pbpasteCommands,
privacyCommands,
pushCommands,
envCommands,
shutdownCommands,
spawnCommands,
terminateCommands,
uiCommands,
uninstallCommands,
);
export default subcommands;
+19
View File
@@ -0,0 +1,19 @@
const commands = {};
/**
* Install the particular application package on Simulator.
* It is required that Simulator is in _booted_ state.
*
* @param {string} appPath - Full path to .app package, which is
* going to be installed.
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
commands.installApp = async function installApp (appPath) {
await this.exec('install', {
args: [this.requireUdid('install'), appPath],
});
};
export default commands;
+35
View File
@@ -0,0 +1,35 @@
import rimraf from 'rimraf';
import { v4 as uuidV4 } from 'uuid';
import path from 'path';
import os from 'os';
import fs from 'fs';
import B from 'bluebird';
const commands = {};
const rimrafAsync = B.promisify(rimraf);
const readFileAsync = B.promisify(fs.readFile);
/**
* Gets base64 screenshot for device
* It is required that Simulator is in _booted_ state.
*
* @since Xcode SDK 8.1
* @return {string} Base64-encoded Simulator screenshot.
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
commands.getScreenshot = async function getScreenshot () {
const udid = this.requireUdid('io screenshot');
const pathToScreenshotPng = path.resolve(os.tmpdir(), `${uuidV4()}.png`);
try {
await this.exec('io', {
args: [udid, 'screenshot', pathToScreenshotPng],
});
return (await readFileAsync(pathToScreenshotPng)).toString('base64');
} finally {
await rimrafAsync(pathToScreenshotPng);
}
};
export default commands;
+101
View File
@@ -0,0 +1,101 @@
import os from 'os';
import fs from 'fs';
import B from 'bluebird';
import { v4 as uuidV4 } from 'uuid';
import path from 'path';
import _ from 'lodash';
import rimraf from 'rimraf';
const commands = {};
const rimrafAsync = B.promisify(rimraf);
const writeFileAsync = B.promisify(fs.writeFile);
async function handleRawPayload (payload, onPayloadStored) {
const filePath = path.resolve(os.tmpdir(), `${uuidV4()}.pem`);
try {
if (_.isBuffer(payload)) {
await writeFileAsync(filePath, payload);
} else {
await writeFileAsync(filePath, payload, 'utf8');
}
await onPayloadStored(filePath);
} finally {
await rimrafAsync(filePath);
}
}
/**
* @typedef {Object} CertOptions
* @property {boolean} raw [false] - whether the `cert` argument
* is the path to the certificate on the local file system or
* a raw certificate content
*/
/**
* Adds the given certificate to the Trusted Root Store on the simulator
*
* @since Xcode 11.4 SDK
* @param {string} cert the full path to a valid .cert file containing
* the certificate content or the certificate content itself, depending on
* options
* @param {CertOptions} opts
* @throws {Error} if the current SDK version does not support the command
* or there was an error while adding the certificate
* @throws {Error} If the `udid` instance property is unset
*/
commands.addRootCertificate = async function addRootCertificate (cert, opts = {}) {
const {
raw = false,
} = opts;
const execMethod = async (certPath) => await this.exec('keychain', {
args: [this.requireUdid('keychain add-root-cert'), 'add-root-cert', certPath],
});
if (raw) {
await handleRawPayload(cert, execMethod);
} else {
await execMethod(cert);
}
};
/**
* Adds the given certificate to the Keychain Store on the simulator
*
* @since Xcode 11.4 SDK
* @param {string} cert the full path to a valid .cert file containing
* the certificate content or the certificate content itself, depending on
* options
* @param {CertOptions} opts
* @throws {Error} if the current SDK version does not support the command
* or there was an error while adding the certificate
* @throws {Error} If the `udid` instance property is unset
*/
commands.addCertificate = async function addCertificate (cert, opts = {}) {
const {
raw = false,
} = opts;
const execMethod = async (certPath) => await this.exec('keychain', {
args: [this.requireUdid('keychain add-cert'), 'add-cert', certPath],
});
if (raw) {
await handleRawPayload(cert, execMethod);
} else {
await execMethod(cert);
}
};
/**
* Resets the simulator keychain
*
* @since Xcode 11.4 SDK
* @throws {Error} if the current SDK version does not support the command
* or there was an error while resetting the keychain
* @throws {Error} If the `udid` instance property is unset
*/
commands.resetKeychain = async function resetKeychain () {
await this.exec('keychain', {
args: [this.requireUdid('keychain reset'), 'reset'],
});
};
export default commands;
+29
View File
@@ -0,0 +1,29 @@
import _ from 'lodash';
import { retryInterval } from 'asyncbox';
const commands = {};
/**
* Execute the particular application package on Simulator.
* It is required that Simulator is in _booted_ state and
* the application with given bundle identifier is already installed.
*
* @param {string} bundleId - Bundle identifier of the application,
* which is going to be removed.
* @param {number} tries [5] - The maximum number of retries before
* throwing an exception.
* @return {string} the actual command output
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
commands.launchApp = async function launchApp (bundleId, tries = 5) {
return await retryInterval(tries, 1000, async () => {
const {stdout} = await this.exec('launch', {
args: [this.requireUdid('launch'), bundleId],
});
return _.trim(stdout);
});
};
export default commands;
+302
View File
@@ -0,0 +1,302 @@
import _ from 'lodash';
import { SIM_RUNTIME_NAME, normalizeVersion } from '../helpers';
import log, { LOG_PREFIX } from '../logger';
const commands = {};
/**
* @typedef {Object} DeviceInfo
* @property {string} name - The device name.
* @property {string} udid - The device UDID.
* @property {string} state - The current Simulator state, for example 'booted' or 'shutdown'.
* @property {string} sdk - The SDK version, for example '10.3'.
*/
/**
* Parse the list of existing Simulator devices to represent
* it as convenient mapping.
*
* @param {?string} platform - The platform name, for example 'watchOS'.
* @return {Object} The resulting mapping. Each key is platform version,
* for example '10.3' and the corresponding value is an
* array of the matching {@link DeviceInfo} instances.
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
*/
commands.getDevicesByParsing = async function getDevicesByParsing (platform) {
// get the list of devices
const {stdout} = await this.exec('list', {
args: ['devices'],
});
// expect to get a listing like
// -- iOS 8.1 --
// iPhone 4s (3CA6E7DD-220E-45E5-B716-1E992B3A429C) (Shutdown)
// ...
// -- iOS 8.2 --
// iPhone 4s (A99FFFC3-8E19-4DCF-B585-7D9D46B4C16E) (Shutdown)
// ...
// so, get the `-- iOS X.X --` line to find the sdk (X.X)
// and the rest of the listing in order to later find the devices
const deviceSectionRe = _.isEmpty(platform)
? new RegExp(`\\-\\-\\s+(\\S+)\\s+(\\S+)\\s+\\-\\-(\\n\\s{4}.+)*`, 'mgi')
: new RegExp(`\\-\\-\\s+${_.escapeRegExp(platform)}\\s+(\\S+)\\s+\\-\\-(\\n\\s{4}.+)*`, 'mgi');
const matches = [];
let match;
// make an entry for each sdk version
while ((match = deviceSectionRe.exec(stdout))) {
matches.push(match);
}
if (_.isEmpty(matches)) {
throw new Error('Could not find device section');
}
const lineRe = /([^\s].+) \((\w+-.+\w+)\) \((\w+\s?\w+)\)/; // https://regex101.com/r/lG7mK6/3
// get all the devices for each sdk
const devices = {};
for (match of matches) {
const sdk = platform ? match[1] : match[2];
devices[sdk] = devices[sdk] || [];
// split the full match into lines and remove the first
for (const line of match[0].split('\n').slice(1)) {
if (line.includes('(unavailable, ')) {
continue;
}
// a line is something like
// iPhone 4s (A99FFFC3-8E19-4DCF-B585-7D9D46B4C16E) (Shutdown)
// retrieve:
// iPhone 4s
// A99FFFC3-8E19-4DCF-B585-7D9D46B4C16E
// Shutdown
const lineMatch = lineRe.exec(line);
if (!lineMatch) {
throw new Error(`Could not match line: ${line}`);
}
// save the whole thing as ab object in the list for this sdk
devices[sdk].push({
name: lineMatch[1],
udid: lineMatch[2],
state: lineMatch[3],
sdk,
platform: platform || match[1],
});
}
}
return devices;
};
/**
* Parse the list of existing Simulator devices to represent
* it as convenient mapping for the particular platform version.
*
* @param {?string} forSdk - The sdk version,
* for which the devices list should be parsed,
* for example '10.3'.
* @param {?string} platform - The platform name, for example 'watchOS'.
* @return {Object|Array<DeviceInfo>} If _forSdk_ is set then the list
* of devices for the particular platform version.
* Otherwise the same result as for {@link getDevicesByParsing}
* function.
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code or if no matching
* platform version is found in the system.
*/
commands.getDevices = async function getDevices (forSdk, platform) {
let devices = {};
try {
const {stdout} = await this.exec('list', {
args: ['devices', '-j'],
});
/* JSON should be
* {
* "devices" : {
* "iOS <sdk>" : [ // or
* "com.apple.CoreSimulator.SimRuntime.iOS-<sdk> : [
* {
* "state" : "Booted",
* "availability" : "(available)",
* "isAvailable" : true,
* "name" : "iPhone 6",
* "udid" : "75E34140-18E8-4D1A-9F45-AAC735DF75DF"
* }
* ]
* }
* }
*/
const versionMatchRe = _.isEmpty(platform)
? new RegExp(`^([^\\s-]+)[\\s-](\\S+)`, 'i')
: new RegExp(`^${_.escapeRegExp(platform)}[\\s-](\\S+)`, 'i');
for (let [sdkName, entries] of _.toPairs(JSON.parse(stdout).devices)) {
// there could be a longer name, so remove it
sdkName = sdkName.replace(SIM_RUNTIME_NAME, '');
const versionMatch = versionMatchRe.exec(sdkName);
if (!versionMatch) {
continue;
}
// the sdk can have dashes (`12-2`) or dots (`12.1`)
const sdk = (platform ? versionMatch[1] : versionMatch[2]).replace('-', '.');
devices[sdk] = devices[sdk] || [];
devices[sdk].push(...entries.filter((el) => _.isUndefined(el.isAvailable) || el.isAvailable)
.map((el) => {
delete el.availability;
return {
sdk,
...el,
platform: platform || versionMatch[1],
};
})
);
}
} catch (err) {
log.debug(LOG_PREFIX, `Unable to get JSON device list: ${err.stack}`);
log.debug(LOG_PREFIX, 'Falling back to manual parsing');
devices = await this.getDevicesByParsing(platform);
}
if (!forSdk) {
return devices;
}
// if a `forSdk` was passed in, return only the corresponding list
if (devices[forSdk]) {
return devices[forSdk];
}
let errMsg = `'${forSdk}' does not exist in the list of simctl SDKs.`;
const availableSDKs = _.keys(devices);
errMsg += availableSDKs.length
? ` Only the following Simulator SDK versions are available on your system: ${availableSDKs.join(', ')}`
: ` No Simulator SDK versions are available on your system. Please install some via Xcode preferences.`;
throw new Error(errMsg);
};
/**
* Get the runtime for the particular platform version using --json flag
*
* @param {string} platformVersion - The platform version name,
* for example '10.3'.
* @param {?string} platform - The platform name, for example 'watchOS'.
* @return {string} The corresponding runtime name for the given
* platform version.
*/
commands.getRuntimeForPlatformVersionViaJson = async function getRuntimeForPlatformVersionViaJson (
platformVersion, platform = 'iOS') {
const {stdout} = await this.exec('list', {
args: ['runtimes', '--json'],
});
for (const {version, identifier, name} of JSON.parse(stdout).runtimes) {
if (normalizeVersion(version) === normalizeVersion(platformVersion)
&& name.toLowerCase().startsWith(platform.toLowerCase())) {
return identifier;
}
}
throw new Error(`Could not use --json flag to parse platform version`);
};
/**
* Get the runtime for the particular platform version.
*
* @param {string} platformVersion - The platform version name,
* for example '10.3'.
* @param {?string} platform - The platform name, for example 'watchOS'.
* @return {string} The corresponding runtime name for the given
* platform version.
*/
commands.getRuntimeForPlatformVersion = async function getRuntimeForPlatformVersion (
platformVersion, platform = 'iOS') {
// Try with parsing
try {
const {stdout} = await this.exec('list', {
args: ['runtimes'],
});
// https://regex101.com/r/UykjQZ/1
const runtimeRe =
new RegExp(`${_.escapeRegExp(platform)}\\s+(\\d+\\.\\d+)\\s+\\((\\d+\\.\\d+\\.*\\d*)`, 'i');
for (const line of stdout.split('\n')) {
const match = runtimeRe.exec(line);
if (match && match[1] === platformVersion) {
return match[2];
}
}
} catch (ign) {}
// if nothing was found, pass platform version back
return platformVersion;
};
/**
* Get the list of device types available in the current Xcode installation
*
* @return {Array<string>} List of the types of devices available
* @throws {Error} If the corresponding simctl command fails
*/
commands.getDeviceTypes = async function getDeviceTypes () {
const {stdout} = await this.exec('list', {
args: ['devicetypes', '-j'],
});
/*
* JSON will be like:
* {
* "devicetypes" : [
* {
* "name" : "iPhone 4s",
* "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-4s"
* },
* ...
* }
*/
try {
const deviceTypes = JSON.parse(stdout.trim());
return deviceTypes.devicetypes.map((type) => type.name);
} catch (err) {
throw new Error(`Unable to get list of device types: ${err.message}`);
}
};
/**
* Get the full list of runtimes, devicetypes, devices and pairs as Object
*
* @return {Object} Object containing device types, runtimes devices and pairs.
* The resulting JSON will be like:
* {
* "devicetypes" : [
* {
* "name" : "iPhone 4s",
* "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-4s"
* },
* ...
* ],
* "runtimes" : [
* {
* "version" : '13.0',
* "bundlePath" : '/Applications/Xcode11beta4.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime',
* "isAvailable" : true,
* "name" : 'iOS 13.0',
* "identifier" : 'com.apple.CoreSimulator.SimRuntime.iOS-13-0',
* "buildversion" : '17A5534d'
* },
* ...
* },
* "devices" :
* {
* 'com.apple.CoreSimulator.SimRuntime.iOS-13-0': [ [Object], [Object] ] },
* ...
* },
* "pairs" : {} }
*
* }
* @throws {Error} If the corresponding simctl command fails
*/
commands.list = async function list () {
const {stdout} = await this.exec('list', {
args: ['-j'],
});
try {
return JSON.parse(stdout.trim());
} catch (e) {
throw new Error(`Unable to parse simctl list: ${e.message}`);
}
};
export default commands;
+21
View File
@@ -0,0 +1,21 @@
const commands = {};
/**
* Open URL scheme on Simulator. iOS will automatically try
* to find a matching application, which supports the given scheme.
* It is required that Simulator is in _booted_ state.
*
* @param {string} url - The URL scheme to open, for example http://appiom.io
* will be opened by the built-in mobile browser.
* @return {ExecResult} Command execution result.
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
commands.openUrl = async function openUrl (url) {
return await this.exec('openurl', {
args: [this.requireUdid('openurl'), url],
});
};
export default commands;
+29
View File
@@ -0,0 +1,29 @@
const commands = {};
/**
* Set the content of Simulator pasteboard.
* It is required that Simulator is in _booted_ state.
*
* @since Xcode SDK 8.1
* @param {string} content - The actual string content to be set.
* @param {string} encoding [utf'] - The encoding of the given pasteboard content.
* UTF-8 by default.
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
commands.setPasteboard = async function setPasteboard (content, encoding = 'utf8') {
const pbCopySubprocess = await this.exec('pbcopy', {
args: [this.requireUdid('pbcopy')],
asynchronous: true,
});
await pbCopySubprocess.start(0);
const exitCodeVerifier = pbCopySubprocess.join();
const stdin = pbCopySubprocess.proc.stdin;
stdin.setEncoding(encoding);
stdin.write(content);
stdin.end();
await exitCodeVerifier;
};
export default commands;
+23
View File
@@ -0,0 +1,23 @@
const commands = {};
/**
* Get the content of Simulator pasteboard.
* It is required that Simulator is in _booted_ state.
*
* @since Xcode 8.1 SDK
* @param {string} encoding ['utf-8'] - The encoding of the returned pasteboard content.
* UTF-8 by default.
* @return {string} Current content of Simulator pasteboard or an empty string.
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
commands.getPasteboard = async function getPasteboard (encoding = 'utf8') {
const {stdout} = await this.exec('pbpaste', {
args: [this.requireUdid('pbpaste')],
encoding,
});
return stdout;
};
export default commands;
+69
View File
@@ -0,0 +1,69 @@
const commands = {};
/**
* Grants the given permission on the app with the given bundle identifier
*
* @since Xcode 11.4 SDK
* @param {string} bundleId the identifier of the application whose
* privacy settings are going to be changed
* @param {string} perm one of possible permission values:
* - all: Apply the action to all services.
* - calendar: Allow access to calendar.
* - contacts-limited: Allow access to basic contact info.
* - contacts: Allow access to full contact details.
* - location: Allow access to location services when app is in use.
* - location-always: Allow access to location services at all times.
* - photos-add: Allow adding photos to the photo library.
* - photos: Allow full access to the photo library.
* - media-library: Allow access to the media library.
* - microphone: Allow access to audio input.
* - motion: Allow access to motion and fitness data.
* - reminders: Allow access to reminders.
* - siri: Allow use of the app with Siri.
* @throws {Error} if the current SDK version does not support the command
* or there was an error while granting the permission
* @throws {Error} If the `udid` instance property is unset
*/
commands.grantPermission = async function grantPermission (bundleId, perm) {
await this.exec('privacy', {
args: [this.requireUdid('privacy grant'), 'grant', perm, bundleId],
});
};
/**
* Revokes the given permission on the app with the given bundle identifier
* after it has been granted
*
* @since Xcode 11.4 SDK
* @param {string} bundleId the identifier of the application whose
* privacy settings are going to be changed
* @param {string} perm one of possible permission values (see `grantPermission`)
* @throws {Error} if the current SDK version does not support the command
* or there was an error while revoking the permission
* @throws {Error} If the `udid` instance property is unset
*/
commands.revokePermission = async function revokePermission (bundleId, perm) {
await this.exec('privacy', {
args: [this.requireUdid('privacy revoke'), 'revoke', perm, bundleId],
});
};
/**
* Resets the given permission on the app with the given bundle identifier
* to its default state
*
* @since Xcode 11.4 SDK
* @param {string} bundleId the identifier of the application whose
* privacy settings are going to be changed
* @param {string} perm one of possible permission values (see `grantPermission`)
* @throws {Error} if the current SDK version does not support the command
* or there was an error while resetting the permission
* @throws {Error} If the `udid` instance property is unset
*/
commands.resetPermission = async function resetPermission (bundleId, perm) {
await this.exec('privacy', {
args: [this.requireUdid('private reset'), 'reset', perm, bundleId],
});
};
export default commands;
+44
View File
@@ -0,0 +1,44 @@
import rimraf from 'rimraf';
import { v4 as uuidV4 } from 'uuid';
import path from 'path';
import os from 'os';
import fs from 'fs';
import B from 'bluebird';
const commands = {};
const rimrafAsync = B.promisify(rimraf);
const writeFileAsync = B.promisify(fs.writeFile);
/**
* Send a simulated push notification
*
* @since Xcode 11.4 SDK
* @param {Object} payload - The object that describes Apple push notification content.
* It must contain a top-level "Simulator Target Bundle" key with a string value matching
* the target applications bundle identifier and "aps" key with valid Apple Push Notification values.
* For example:
* {
* "Simulator Target Bundle": "com.apple.Preferences",
* "aps": {
* "alert": "This is a simulated notification!",
* "badge": 3,
* "sound": "default"
* }
* }
* @throws {Error} if the current SDK version does not support the command
* or there was an error while pushing the notification
* @throws {Error} If the `udid` instance property is unset
*/
commands.pushNotification = async function pushNotification (payload) {
const dstPath = path.resolve(os.tmpdir(), `${uuidV4()}.json`);
try {
await writeFileAsync(dstPath, JSON.stringify(payload), 'utf8');
await this.exec('push', {
args: [this.requireUdid('push'), dstPath],
});
} finally {
await rimrafAsync(dstPath);
}
};
export default commands;
+26
View File
@@ -0,0 +1,26 @@
import _ from 'lodash';
import log, { LOG_PREFIX } from '../logger';
const commands = {};
/**
* Shutdown the given Simulator if it is running.
*
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
commands.shutdownDevice = async function shutdownDevice () {
try {
await this.exec('shutdown', {
args: [this.requireUdid('shutdown')],
});
} catch (e) {
if (!_.includes(e.message, 'current state: Shutdown')) {
throw e;
}
log.debug(LOG_PREFIX, `Simulator already in 'Shutdown' state. Continuing`);
}
};
export default commands;
+49
View File
@@ -0,0 +1,49 @@
import _ from 'lodash';
const commands = {};
/**
* Spawn the particular process on Simulator.
* It is required that Simulator is in _booted_ state.
*
* @param {string|Array<string>} args - Spawn arguments
* @param {object} env [{}] - Additional environment variables mapping.
* @return {ExecResult} Command execution result.
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
commands.spawnProcess = async function spawnProcess (args, env = {}) {
if (_.isEmpty(args)) {
throw new Error('Spawn arguments are required');
}
return await this.exec('spawn', {
args: [this.requireUdid('spawn'), ...(_.isArray(args) ? args : [args])],
env,
});
};
/**
* Prepare SubProcess instance for a new process, which is going to be spawned
* on Simulator.
*
* @param {string|Array<string>} args - Spawn arguments
* @param {object} env [{}] - Additional environment variables mapping.
* @return {SubProcess} The instance of the process to be spawned.
* @throws {Error} If the `udid` instance property is unset
*/
commands.spawnSubProcess = async function spawnSubProcess (args, env = {}) {
if (_.isEmpty(args)) {
throw new Error('Spawn arguments are required');
}
return await this.exec('spawn', {
args: [this.requireUdid('spawn'), ...(_.isArray(args) ? args : [args])],
env,
asynchronous: true,
});
};
export default commands;
+19
View File
@@ -0,0 +1,19 @@
const commands = {};
/**
* Terminate the given running application on Simulator.
* It is required that Simulator is in _booted_ state.
*
* @param {string} bundleId - Bundle identifier of the application,
* which is going to be terminated.
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
commands.terminateApp = async function terminateApp (bundleId) {
await this.exec('terminate', {
args: [this.requireUdid('terminate'), bundleId],
});
};
export default commands;
+37
View File
@@ -0,0 +1,37 @@
import _ from 'lodash';
const commands = {};
/**
* Retrieves the current UI appearance value from the given simulator
*
* @since Xcode 11.4 SDK
* @return {string} the appearance value, for example 'light' or 'dark'
* @throws {Error} if the current SDK version does not support the command
* or there was an error while getting the value
* @throws {Error} If the `udid` instance property is unset
*/
commands.getAppearance = async function getAppearance () {
const {stdout} = await this.exec('ui', {
args: [this.requireUdid('ui'), 'appearance'],
});
return _.trim(stdout);
};
/**
* Sets the UI appearance to the given style
*
* @since Xcode 11.4 SDK
* @param {string} appearance valid appearance value, for example 'light' or 'dark'
* @throws {Error} if the current SDK version does not support the command
* or there was an error while getting the value
* @throws {Error} If the `udid` instance property is unset
*/
commands.setAppearance = async function setAppearance (appearance) {
await this.exec('ui', {
args: [this.requireUdid('ui'), 'appearance', appearance],
});
};
export default commands;
+20
View File
@@ -0,0 +1,20 @@
const commands = {};
/**
* Remove the particular application package from Simulator.
* It is required that Simulator is in _booted_ state and
* the application with given bundle identifier is already installed.
*
* @param {string} bundleId - Bundle identifier of the application,
* which is going to be removed.
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
* @throws {Error} If the `udid` instance property is unset
*/
commands.removeApp = async function removeApp (bundleId) {
await this.exec('uninstall', {
args: [this.requireUdid('uninstall'), bundleId],
});
};
export default commands;