UNPKG

appium-ios-simulator

Version:
206 lines (189 loc) 6.75 kB
import {log} from './logger'; import {exec} from 'teen_process'; import {waitForCondition} from 'asyncbox'; import {getVersion} from 'appium-xcode'; import type {XcodeVersion} from 'appium-xcode'; import path from 'node:path'; import {Simctl} from 'node-simctl'; import type {StringRecord} from '@appium/types'; // it's a hack needed to stub getDevices in tests import * as utilsModule from './utils'; const DEFAULT_SIM_SHUTDOWN_TIMEOUT_MS = 30000; export const SAFARI_STARTUP_TIMEOUT_MS = 25 * 1000; export const MOBILE_SAFARI_BUNDLE_ID = 'com.apple.mobilesafari'; export const SIMULATOR_APP_NAME = 'Simulator.app'; export const MIN_SUPPORTED_XCODE_VERSION = 14; export interface SimulatorInfoOptions { devicesSetPath?: string | null; } /** * @param timeout - Timeout in milliseconds (default: DEFAULT_SIM_SHUTDOWN_TIMEOUT_MS). * @returns Promise that resolves when all simulators are killed. */ export async function killAllSimulators( timeout: number = DEFAULT_SIM_SHUTDOWN_TIMEOUT_MS, ): Promise<void> { log.debug('Killing all iOS Simulators'); const xcodeVersion = await getVersion(true); if (typeof xcodeVersion === 'string') { return; } const appName = path.parse(SIMULATOR_APP_NAME).name; const version = xcodeVersion as XcodeVersion; // later versions are slower to close timeout = timeout * (version.major >= 8 ? 2 : 1); try { await exec('xcrun', ['simctl', 'shutdown', version.major > 8 ? 'all' : 'booted'], {timeout}); } catch {} const pids: string[] = []; try { const {stdout} = await exec('pgrep', ['-f', `${appName}.app/Contents/MacOS/`]); if (stdout.trim()) { pids.push(...stdout.trim().split(/\s+/)); } } catch (e: any) { if (e.code === 1) { log.debug(`${appName} is not running. Continuing...`); return; } if (pids.length === 0) { log.warn( `pgrep error ${e.code} while detecting whether ${appName} is running. Trying to kill anyway.`, ); } } if (pids.length > 0) { log.debug(`Killing processes: ${pids.join(', ')}`); try { await exec('kill', ['-9', ...pids.map((pid) => `${pid}`)]); } catch {} } log.debug(`Using pkill to kill application: ${appName}`); try { await pkill(appName, true); } catch {} // wait for all the devices to be shutdown before Continuing // but only print out the failed ones when they are actually fully failed let remainingDevices: string[] = []; async function allSimsAreDown(): Promise<boolean> { remainingDevices = []; const devicesRecord = await utilsModule.getDevices(); const devices = Object.values(devicesRecord).flat(); return devices.every((sim: any) => { const state = sim.state.toLowerCase(); const done = ['shutdown', 'unavailable', 'disconnected'].includes(state); if (!done) { remainingDevices.push( `${sim.name} (${sim.sdk}, udid: ${sim.udid}) is still in state '${state}'`, ); } return done; }); } try { await waitForCondition(allSimsAreDown, { waitMs: timeout, intervalMs: 200, }); } catch (err) { if (remainingDevices.length > 0) { log.warn(`The following devices are still not in the correct state after ${timeout} ms:`); for (const device of remainingDevices) { log.warn(` ${device}`); } } throw err; } } /** * @param udid - The simulator UDID. * @param opts - Options including devicesSetPath. * @returns Promise that resolves to simulator info or undefined if not found. */ export async function getSimulatorInfo( udid: string, opts: SimulatorInfoOptions = {}, ): Promise<any> { const {devicesSetPath} = opts; // see the README for github.com/appium/node-simctl for example output of getDevices() const devices = Object.values(await utilsModule.getDevices({devicesSetPath})).flat(); return devices.find((sim: any) => sim.udid === udid); } /** * @param udid - The simulator UDID. * @returns Promise that resolves to true if simulator exists, false otherwise. */ export async function simExists(udid: string): Promise<boolean> { return !!(await getSimulatorInfo(udid)); } /** * @returns Promise that resolves to the developer root path. */ export async function getDeveloperRoot(): Promise<string> { const {stdout} = await exec('xcode-select', ['-p']); return stdout.trim(); } /** * Asserts that the Xcode version meets the minimum supported version requirement. * * @template V - The Xcode version type. * @param xcodeVersion - The Xcode version to check. * @returns The same Xcode version if it meets the requirement. * @throws {Error} If the Xcode version is below the minimum supported version. */ export function assertXcodeVersion<V extends XcodeVersion>(xcodeVersion: V): V { if (xcodeVersion.major < MIN_SUPPORTED_XCODE_VERSION) { throw new Error( `Tried to use an iOS simulator with xcode version ${xcodeVersion.versionString} but only Xcode version ` + `${MIN_SUPPORTED_XCODE_VERSION} and up are supported`, ); } return xcodeVersion; } /** * @param simctlOpts - Optional simctl options * @returns Promise that resolves to a record of devices grouped by SDK version */ export async function getDevices(simctlOpts?: StringRecord): Promise<Record<string, any[]>> { return await new Simctl(simctlOpts).getDevices(); } /** * Checks whether the given value is a plain object. */ export function isPlainObject(value: unknown): value is Record<string, any> { if (value === null || typeof value !== 'object' || Array.isArray(value)) { return false; } const proto = Object.getPrototypeOf(value); return proto === null || proto === Object.prototype; } /** * Escapes regexp control characters in a string. */ export function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * @param appName - The application name to kill. * @param forceKill - Whether to force kill the process. * @returns Promise that resolves to 0 on success. */ async function pkill(appName: string, forceKill: boolean = false): Promise<number> { const args = forceKill ? ['-9'] : []; args.push('-x', appName); try { await exec('pkill', args); return 0; } catch (err: any) { // pgrep/pkill exit codes: // 0 One or more processes were matched. // 1 No processes were matched. // 2 Invalid options were specified on the command line. // 3 An internal error occurred. if (err.code !== undefined) { throw new Error(`Cannot forcefully terminate ${appName}. pkill error code: ${err.code}`); } log.error(`Received unexpected error while trying to kill ${appName}: ${err.message}`); throw err; } }