appium-ios-simulator
Version:
iOS Simulator interface for Appium.
319 lines • 14.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.setPermission = setPermission;
exports.setPermissions = setPermissions;
exports.getPermission = getPermission;
const lodash_1 = __importDefault(require("lodash"));
const support_1 = require("@appium/support");
const teen_process_1 = require("teen_process");
const path_1 = __importDefault(require("path"));
const bluebird_1 = __importDefault(require("bluebird"));
const asyncbox_1 = require("asyncbox");
const STATUS = Object.freeze({
UNSET: 'unset',
NO: 'no',
YES: 'yes',
LIMITED: 'limited',
});
const SPRINGBOARD_BUNDLE_ID = 'com.apple.SpringBoard';
const SPOTLIGHT_BUNDLE_ID = 'com.apple.Spotlight';
const WIX_SIM_UTILS = 'applesimutils';
const SERVICES_NEED_SPRINGBOARD_RESTART = ['notifications'];
const SYSTEM_SERVICE_RESTART_TIMEOUT_MS = 15000;
// `location` permission does not work with WIX/applesimutils.
// Note that except for 'contacts', the Apple's privacy command sets
// permissions properly but it kills the app process while WIX/applesimutils does not.
// In the backward compatibility perspective,
// we'd like to keep the app process as possible.
const PERMISSIONS_APPLIED_VIA_SIMCTL = [
'location',
'location-always'
];
const SERVICES = Object.freeze({
calendar: 'kTCCServiceCalendar',
camera: 'kTCCServiceCamera',
contacts: 'kTCCServiceAddressBook',
homekit: 'kTCCServiceWillow',
microphone: 'kTCCServiceMicrophone',
photos: 'kTCCServicePhotos',
reminders: 'kTCCServiceReminders',
medialibrary: 'kTCCServiceMediaLibrary',
motion: 'kTCCServiceMotion',
health: 'kTCCServiceMSO',
siri: 'kTCCServiceSiri',
speech: 'kTCCServiceSpeechRecognition',
});
/**
* Sets the particular permission to the application bundle. See https://github.com/wix/AppleSimulatorUtils
* or `xcrun simctl privacy` for more details on the available service names and statuses.
*
* @this {CoreSimulatorWithAppPermissions}
* @param {string} bundleId - Application bundle identifier.
* @param {string} permission - Service name to be set.
* @param {string} value - The desired status for the service.
* @throws {Error} If there was an error while changing permission.
*/
async function setPermission(bundleId, permission, value) {
await this.setPermissions(bundleId, { [permission]: value });
}
/**
* Sets the permissions for the particular application bundle.
*
* @this {CoreSimulatorWithAppPermissions}
* @param {string} bundleId - Application bundle identifier.
* @param {Object} permissionsMapping - A mapping where kays
* are service names and values are their corresponding status values.
* See https://github.com/wix/AppleSimulatorUtils or `xcrun simctl privacy`
* for more details on available service names and statuses.
* @throws {Error} If there was an error while changing permissions.
*/
async function setPermissions(bundleId, permissionsMapping) {
this.log.debug(`Setting access for '${bundleId}': ${JSON.stringify(permissionsMapping, null, 2)}`);
await setAccess.bind(this)(bundleId, permissionsMapping);
}
/**
* Retrieves current permission status for the given application bundle.
*
* @this {CoreSimulatorWithAppPermissions}
* @param {string} bundleId - Application bundle identifier.
* @param {string} serviceName - One of available service names.
* @returns {Promise<string>}
* @throws {Error} If there was an error while retrieving permissions.
*/
async function getPermission(bundleId, serviceName) {
const result = await getAccess.bind(this)(bundleId, serviceName);
this.log.debug(`Got ${serviceName} access status for '${bundleId}': ${result}`);
return result;
}
function toInternalServiceName(serviceName) {
if (lodash_1.default.has(SERVICES, lodash_1.default.toLower(serviceName))) {
return SERVICES[lodash_1.default.toLower(serviceName)];
}
throw new Error(`'${serviceName}' is unknown. Only the following service names are supported: ${JSON.stringify(lodash_1.default.keys(SERVICES))}`);
}
function formatStatus(status) {
return [STATUS.UNSET, STATUS.NO].includes(status) ? lodash_1.default.toUpper(status) : status;
}
/**
* Runs a command line sqlite3 query
*
* @this {CoreSimulatorWithAppPermissions}
* @param {string} db - Full path to sqlite database
* @param {string} query - The actual query string
* @returns {Promise<string>} sqlite command stdout
*/
async function execSQLiteQuery(db, query) {
this.log.debug(`Executing SQL query "${query}" on '${db}'`);
try {
return (await (0, teen_process_1.exec)('sqlite3', ['-line', db, query])).stdout;
}
catch (err) {
throw new Error(`Cannot execute SQLite query "${query}" to '${db}'. Original error: ${err.stderr}`);
}
}
/**
* @this {CoreSimulatorWithAppPermissions}
* @param {string[]} args
* @returns {Promise<string>}
*/
async function execWix(args) {
try {
await support_1.fs.which(WIX_SIM_UTILS);
}
catch {
throw new Error(`${WIX_SIM_UTILS} binary has not been found in your PATH. ` +
`Please install it ('brew tap wix/brew && brew install wix/brew/applesimutils') to ` +
`be able to change application permissions`);
}
this.log.debug(`Executing: ${WIX_SIM_UTILS} ${support_1.util.quote(args)}`);
try {
const { stdout } = await (0, teen_process_1.exec)(WIX_SIM_UTILS, args);
this.log.debug(`Command output: ${stdout}`);
return stdout;
}
catch (e) {
throw new Error(`Cannot execute "${WIX_SIM_UTILS} ${support_1.util.quote(args)}". Original error: ${e.stderr || e.message}`);
}
}
/**
* Sets permissions for the given application
*
* @this {CoreSimulatorWithAppPermissions}
* @param {string} bundleId - bundle identifier of the target application.
* @param {Object} permissionsMapping - An object, where keys are service names
* and values are corresponding state values. Services listed in PERMISSIONS_APPLIED_VIA_SIMCTL
* will be set with `xcrun simctl privacy` command by Apple otherwise AppleSimulatorUtils by WIX.
* See the result of `xcrun simctl privacy` and https://github.com/wix/AppleSimulatorUtils
* for more details on available service names and statuses.
* Note that the `xcrun simctl privacy` command kill the app process.
* @throws {Error} If there was an error while changing permissions.
*/
async function setAccess(bundleId, permissionsMapping) {
const /** @type {Record<string, string>} */ wixPermissions = {};
const /** @type {string[]} */ grantPermissions = [];
const /** @type {string[]} */ revokePermissions = [];
const /** @type {string[]} */ resetPermissions = [];
for (const serviceName in permissionsMapping) {
if (!PERMISSIONS_APPLIED_VIA_SIMCTL.includes(serviceName)) {
wixPermissions[serviceName] = permissionsMapping[serviceName];
}
else {
// xcrun simctl privacy expects to be lower case while AppleSimulatorUtils is upper case.
// To keep the compatibility, we should convert here to lower case explicitly.
switch (lodash_1.default.toLower(permissionsMapping[serviceName])) {
case STATUS.YES:
grantPermissions.push(serviceName);
break;
case STATUS.NO:
revokePermissions.push(serviceName);
break;
case STATUS.UNSET:
resetPermissions.push(serviceName);
break;
default:
throw this.log.errorWithException(`${serviceName} does not support ${permissionsMapping[serviceName]}. Please specify 'yes', 'no' or 'unset'.`);
}
}
}
/** @type {Promise[]} */
const permissionPromises = [];
if (!lodash_1.default.isEmpty(grantPermissions)) {
this.log.debug(`Granting ${support_1.util.pluralize('permission', grantPermissions.length, false)} for ${bundleId}: ${grantPermissions}`);
for (const action of grantPermissions) {
permissionPromises.push(this.simctl.grantPermission(bundleId, action));
}
}
if (!lodash_1.default.isEmpty(revokePermissions)) {
this.log.debug(`Revoking ${support_1.util.pluralize('permission', revokePermissions.length, false)} for ${bundleId}: ${revokePermissions}`);
for (const action of revokePermissions) {
permissionPromises.push(this.simctl.revokePermission(bundleId, action));
}
}
if (!lodash_1.default.isEmpty(resetPermissions)) {
this.log.debug(`Resetting ${support_1.util.pluralize('permission', resetPermissions.length, false)} for ${bundleId}: ${resetPermissions}`);
for (const action of resetPermissions) {
permissionPromises.push(this.simctl.resetPermission(bundleId, action));
}
}
if (!lodash_1.default.isEmpty(permissionPromises)) {
await bluebird_1.default.all(permissionPromises);
}
if (!lodash_1.default.isEmpty(wixPermissions)) {
this.log.debug(`Setting permissions for ${bundleId} wit ${WIX_SIM_UTILS} as ${JSON.stringify(wixPermissions)}`);
const permissionsArg = lodash_1.default.toPairs(wixPermissions)
.map((x) => `${x[0]}=${formatStatus(x[1])}`)
.join(',');
const execWixFn = async () => await execWix.bind(this)([
'--byId', this.udid,
'--bundle', bundleId,
'--setPermissions', permissionsArg,
]);
const shouldWaitForSystemReadiness = !lodash_1.default.isEmpty(lodash_1.default.intersection(SERVICES_NEED_SPRINGBOARD_RESTART, lodash_1.default.keys(wixPermissions)));
if (shouldWaitForSystemReadiness) {
const [didTimeout] = await runAndWaitForSystemReadiness.bind(this)(execWixFn, SYSTEM_SERVICE_RESTART_TIMEOUT_MS);
if (didTimeout) {
this.log.warn(`The required system services did not restart after ` +
`${SYSTEM_SERVICE_RESTART_TIMEOUT_MS}ms timeout. This might lead to unexpected consequences later.`);
}
}
else {
await execWixFn();
}
}
return true;
}
/**
* Waiting for springboard restart and applications process end/restart
* triggered by the springboard process restart.
*
* @this {import('../types').CoreSimulator}
* @template {any} T
* @param {() => Promise<T>} fn
* @param {number} timeoutMs
* @returns {Promise<[boolean, T]>}
*/
async function runAndWaitForSystemReadiness(fn, timeoutMs) {
const waitForNewPid = async (initialPid, bundleId, timeoutMs) => {
await (0, asyncbox_1.waitForCondition)(async () => {
try {
const pid = (await this.ps()).find(({ name }) => bundleId === name)?.pid;
return lodash_1.default.isInteger(pid) && initialPid !== pid;
}
catch {
return false;
}
}, { waitMs: timeoutMs, intervalMs: 500 });
};
let initialProcesses = [];
try {
initialProcesses = await this.ps();
}
catch { }
;
const [initialSpringboardPid, initialSpotlightPid] = [
SPRINGBOARD_BUNDLE_ID, SPOTLIGHT_BUNDLE_ID
].map((bundleId) => initialProcesses.find(({ name }) => bundleId === name)?.pid);
const result = await fn();
if (!lodash_1.default.isInteger(initialSpringboardPid) || !lodash_1.default.isInteger(initialSpotlightPid)) {
// there is no point to wait if relevant processes were not running before
return [false, result];
}
try {
// Make sure the springboard process restarted first.
const timer = new support_1.timing.Timer().start();
await waitForNewPid(initialSpringboardPid, SPRINGBOARD_BUNDLE_ID, timeoutMs);
const remainingTimeoutMs = timeoutMs - timer.getDuration().asMilliSeconds;
if (remainingTimeoutMs <= 0) {
// no need to check the SPOTLIGHT_BUNDLE_ID
return [true, result];
}
// Then, checking if the new spring board process refreshes applications.
// Spotlight.app is widely used so the app process can be an indicator to check the refresh.
await waitForNewPid(initialSpotlightPid, SPOTLIGHT_BUNDLE_ID, remainingTimeoutMs);
}
catch {
return [true, result];
}
return [false, result];
}
/**
* Retrieves the current permission status for the given service and application.
*
* @this {CoreSimulatorWithAppPermissions}
* @param {string} bundleId - bundle identifier of the target application.
* @param {string} serviceName - the name of the service. Should be one of
* `SERVICES` keys.
* @returns {Promise<string>} - The current status: yes/no/unset/limited
* @throws {Error} If there was an error while retrieving permissions.
*/
async function getAccess(bundleId, serviceName) {
const internalServiceName = toInternalServiceName(serviceName);
const dbPath = path_1.default.resolve(this.getDir(), 'Library', 'TCC', 'TCC.db');
const getAccessStatus = async (statusPairs, statusKey) => {
for (const [statusValue, status] of statusPairs) {
const sql = `SELECT count(*) FROM 'access' ` +
`WHERE client='${bundleId}' AND ${statusKey}=${statusValue} AND service='${internalServiceName}'`;
const count = await execSQLiteQuery.bind(this)(dbPath, sql);
if (parseInt(count.split('=')[1], 10) > 0) {
return status;
}
}
return STATUS.UNSET;
};
// 'auth_value' existence depends on the OS version rather than Xcode version.
// Thus here check the newer one first, then fallback to the older version way.
try {
// iOS 14+
return await getAccessStatus([['0', STATUS.NO], ['2', STATUS.YES], ['3', STATUS.LIMITED]], 'auth_value');
}
catch {
return await getAccessStatus([['0', STATUS.NO], ['1', STATUS.YES]], 'allowed');
}
}
/**
* @typedef {import('../types').CoreSimulator & import('../types').SupportsAppPermissions} CoreSimulatorWithAppPermissions
*/
//# sourceMappingURL=permissions.js.map