UNPKG

appium-ios-simulator

Version:
319 lines 14.3 kB
"use strict"; 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