UNPKG

appium-safari-driver

Version:
210 lines (185 loc) 6 kB
import os from 'node:os'; import path from 'node:path'; import {JWProxy, errors} from 'appium/driver'; import {fs, logger, util} from 'appium/support'; import {SubProcess} from 'teen_process'; import {waitForCondition} from 'asyncbox'; import {findAPortNotInUse} from 'portscanner'; import {execSync} from 'node:child_process'; import type {AppiumLogger, StringRecord, HTTPMethod, HTTPBody} from '@appium/types'; const SD_BINARY = 'safaridriver'; const STARTUP_TIMEOUT = 10000; // seconds const SAFARI_PORT_RANGE: [number, number] = [5100, 5200]; // This guard is needed to make sure // we never run multiple Safari driver processes for the same Appium process const SAFARI_SERVER_GUARD = util.getLockFileGuard( path.resolve(os.tmpdir(), 'safari_server_guard.lock'), {timeout: 5, tryRecovery: true}, ); class SafariProxy extends JWProxy { didProcessExit?: boolean; override async proxyCommand(url: string, method: HTTPMethod, body: HTTPBody = null) { if (this.didProcessExit) { throw new errors.InvalidContextError( `'${method} ${url}' cannot be proxied to Safari Driver server because ` + 'the process is not running (probably crashed). Check the server log for more details', ); } return await super.proxyCommand(url, method, body); } } class SafariDriverProcess { public port: number | null = null; public proc: SubProcess | null = null; private readonly log: AppiumLogger; constructor() { this.log = logger.getLogger('SafariDriverProcess'); } get isRunning(): boolean { return !!this.proc?.isRunning; } async init(): Promise<void> { await SAFARI_SERVER_GUARD(async () => { if (this.isRunning) { return; } const [startPort, endPort] = SAFARI_PORT_RANGE; try { this.port = await findAPortNotInUse(startPort, endPort); } catch { throw new Error( `Cannot find any free port in range ${startPort}..${endPort}. ` + `Double check the processes that are locking ports within this range and terminate ` + `these which are not needed anymore`, ); } let safariBin: string; try { safariBin = await fs.which(SD_BINARY); } catch { throw new Error( `${SD_BINARY} binary cannot be found in PATH. ` + `Please make sure it is present on your system`, ); } this.proc = new SubProcess(safariBin, ['-p', String(this.port), '--diagnose']); this.proc.on('output', (stdout, stderr) => { const line = stdout || stderr; this.log.debug(`[${SD_BINARY}] ${line}`); }); this.proc.on('exit', (code, signal) => { this.log.info(`${SD_BINARY} has exited with code ${code}, signal ${signal}`); }); this.log.info(`Starting '${safariBin}' on port ${this.port}`); await this.proc.start(0); }); } async kill(): Promise<void> { if (this.isRunning) { try { await this.proc?.stop('SIGKILL'); } catch {} } } } // Single server process per Appium instance const SAFARI_DRIVER_PROCESS = new SafariDriverProcess(); process.once('exit', () => { if (SAFARI_DRIVER_PROCESS.isRunning) { try { execSync(`kill ${SAFARI_DRIVER_PROCESS.proc?.pid}`); } catch {} } }); export interface SessionOptions { reqBasePath?: string; } export class SafariDriverServer { private _proxy: SafariProxy | null = null; private readonly log: AppiumLogger; constructor(log: AppiumLogger) { this.log = log; } get proxy(): SafariProxy { if (!this._proxy) { throw new Error('Safari driver proxy is not initialized'); } return this._proxy; } get isRunning(): boolean { return !!SAFARI_DRIVER_PROCESS.isRunning; } async start(caps: StringRecord, opts: SessionOptions = {}): Promise<void> { await SAFARI_DRIVER_PROCESS.init(); const proxyOptions: any = { server: '127.0.0.1', port: SAFARI_DRIVER_PROCESS.port, base: '', log: this.log, keepAlive: true, }; if (opts.reqBasePath) { proxyOptions.reqBasePath = opts.reqBasePath; } this._proxy = new SafariProxy(proxyOptions); this._proxy.didProcessExit = false; SAFARI_DRIVER_PROCESS.proc?.on('exit', () => { if (this._proxy) { this._proxy.didProcessExit = true; } }); try { await waitForCondition( async () => { try { await this.proxy.command('/status', 'GET'); return true; } catch (err: any) { if (this.proxy.didProcessExit) { throw new Error(err.message, {cause: err}); } return false; } }, { waitMs: STARTUP_TIMEOUT, intervalMs: 1000, }, ); } catch (e: any) { if (/Condition unmet/.test(e.message)) { if (SAFARI_DRIVER_PROCESS.isRunning) { // avoid "frozen" processes, await SAFARI_DRIVER_PROCESS.kill(); } throw new Error( `Safari Driver server is not listening within ${STARTUP_TIMEOUT}ms timeout. ` + `Make sure it has been executed manually at least once with '--enable' command line argument. ` + `Check the server log for more details`, {cause: e}, ); } throw e; } await this.proxy.command('/session', 'POST', { capabilities: { firstMatch: [{}], alwaysMatch: caps, }, }); } async stop(): Promise<void> { if (!this.isRunning) { this.log.info(`${SD_BINARY} session cannot be stopped, because the server is not running`); return; } if (this._proxy?.sessionId) { try { await this.proxy.command(`/session/${this.proxy.sessionId}`, 'DELETE'); } catch (e: any) { this.log.info(`${SD_BINARY} session cannot be deleted. Original error: ${e.message}`); } } } } export default SafariDriverServer;