UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

280 lines 10.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RemoteXPCFacade = void 0; const utils_1 = require("../../utils"); const module_loader_1 = require("./module-loader"); const usbmux_utils_1 = require("./usbmux-utils"); const utils_2 = require("./utils"); const TUNNEL_REGISTRY_PORT_PROBE_TIMEOUT_MS = 3000; /** * Per-driver-session RemoteXPC availability state. * * Initialized lazily on the first remotexpc operation. When the initial tunnel registry probe * fails with {@link isTunnelAvailabilityError}, remotexpc is disabled for the remainder of the * session so callers fall back without re-probing on every service call. * Later per-operation tunnel failures do not flip that cached state. */ class RemoteXPCFacade { udid; platformVersion; log; isRealDevice; initPromise = null; initialized = false; enabled = false; module = null; services = null; useUsbMuxPath = false; sessionFallbackLogged = false; lastImportError = null; constructor(udid, platformVersion, log, isRealDevice) { this.udid = udid; this.platformVersion = platformVersion; this.log = log; this.isRealDevice = isRealDevice; } get eligible() { return this.isRealDevice && (0, utils_1.isIos18OrNewerPlatform)(this.platformVersion); } static async tryGetServicesStatic(platformVersion) { if (platformVersion && !(0, utils_1.isIos18OrNewerPlatform)(platformVersion)) { return null; } const mod = await (0, module_loader_1.tryLoadRemoteXPCModule)(); if (!mod) { return null; } return mod.Services; } /** * Whether remotexpc should be used for service operations in this session. */ async determineAvailability() { await this.ensureInitialized(); return this.enabled; } /** * Loaded remotexpc Services export when {@link determineAvailability} is true. */ async getServices() { await this.ensureInitialized(); if (!this.enabled || !this.services) { throw new Error(`RemoteXPC is not available for device '${this.udid}' in this session`); } return this.services; } async tryGetServices() { if (!(await this.determineAvailability())) { return null; } return this.services; } /** * XCTestRunner class from the loaded module. */ async getXCTestRunner() { return (await this.requireModule()).XCTestRunner; } /** * XCTestAttachment class from the loaded module. */ async getXCTestAttachment() { return (await this.requireModule()).XCTestAttachment; } /** * Resolves which RemoteXPC lockdown connector to use for this device. * * @throws When RemoteXPC lockdown cannot be used */ async resolveLockdownStrategy() { if (!this.eligible) { throw new utils_2.RemoteXPCUnavailableError(); } try { await this.requireModule(); } catch { throw new utils_2.RemoteXPCUnavailableError(); } if (this.useUsbMuxPath) { return 'remotexpc-usbmux'; } return 'remotexpc-tunnel'; } /** * Creates a RemoteXPC device port forwarder when eligible and available. * * @throws When RemoteXPC port forwarding cannot be used */ async createDevicePortForwarder(localPort, devicePort) { if (!this.eligible) { throw new utils_2.RemoteXPCUnavailableError(); } let mod; try { mod = await this.requireModule(); } catch { throw new utils_2.RemoteXPCUnavailableError(); } if (this.useUsbMuxPath) { this.log.debug(`Using RemoteXPC usbmux strategy for '${this.udid}'`); return new mod.DevicePortForwarder(localPort, devicePort, { primaryConnector: () => mod.connectViaUsbmux(this.udid, devicePort), }); } if (!(await this.determineAvailability())) { throw (0, utils_2.wrapRemoteXPCConnectionError)(new Error('RemoteXPC tunnel is not available for this session'), `Cannot create port forwarder via RemoteXPC tunnel for '${this.udid}'`); } this.log.debug(`Using RemoteXPC tunnel strategy for '${this.udid}'`); return new mod.DevicePortForwarder(localPort, devicePort, { primaryConnector: () => mod.connectViaTunnel(this.udid, devicePort), }); } /** * Runs a lockdown operation over the usbmux path for this device. */ async withUsbMuxLockdown(operation) { try { const mod = await this.requireModule(); const { lockdownService } = await mod.createLockdownServiceByUDID(this.udid); try { return await operation(lockdownService); } finally { lockdownService.close(); } } catch (err) { throw new Error(`Failed to read lockdown via appium-ios-remotexpc USBMUX path for '${this.udid}': ` + `${err.message}`, { cause: err }); } } /** * Runs a lockdown operation over the RSD tunnel for this device. */ async withTunnelLockdown(operation) { try { const mod = await this.requireModule(); const lockdown = await mod.createLockdownServiceForTunnel(this.udid); try { return await operation(lockdown); } finally { lockdown.close(); } } catch (err) { throw (0, utils_2.wrapRemoteXPCConnectionError)(err, `Tunnel lockdown failed for '${this.udid}'`); } } /** * Runs a RemoteXPC service operation when the session allows it. * * Tunnel availability failures during an operation are logged and return `null` without * disabling remotexpc for the session (unlike the one-time init probe). * Other failures are logged per call and also return `null` so callers can fall back once. */ async attemptService(feature, operation) { if (!(await this.determineAvailability())) { return null; } try { return await operation(await this.getServices()); } catch (err) { this.handleServiceError(feature, err, 'log'); return null; } } /** * Runs a RemoteXPC service operation when the session allows it. * * @throws When remotexpc is disabled for the session or the operation fails. */ async requireService(feature, operation) { if (!(await this.determineAvailability())) { throw (0, utils_2.wrapRemoteXPCConnectionError)(this.lastImportError ?? new Error('RemoteXPC is not available for this session'), `Failed ${feature} via RemoteXPC for '${this.udid}'`); } try { return await operation(await this.getServices()); } catch (err) { this.handleServiceError(feature, err, 'throw'); throw (0, utils_2.wrapRemoteXPCConnectionError)(err, `Failed ${feature} via RemoteXPC for '${this.udid}'`); } } /** * Disable remotexpc for the remainder of this session after the initial tunnel registry probe fails. */ noteTunnelUnavailable(feature, err) { if (!(0, utils_2.isTunnelAvailabilityError)(err)) { return; } this.enabled = false; this.services = null; if (!this.sessionFallbackLogged) { this.log.warn((0, utils_2.formatRemoteXPCFallbackLog)(feature, err)); this.sessionFallbackLogged = true; } else { this.log.debug(`RemoteXPC ${feature} skipped: tunnel unavailable for this session`); } } async ensureInitialized() { if (this.initialized) { return; } if (!this.initPromise) { this.initPromise = this.initialize(); } await this.initPromise; this.initialized = true; } async initialize() { if (!this.eligible) { return; } const loadedModule = await (0, module_loader_1.tryLoadRemoteXPCModule)(); if (!loadedModule) { const err = (0, module_loader_1.getLastRemoteXPCImportError)(); this.lastImportError = err; this.log.warn(`appium-ios-remotexpc unavailable for '${this.udid}': ${err?.message ?? 'unknown'}`); return; } this.module = loadedModule; this.useUsbMuxPath = await (0, usbmux_utils_1.isDeviceListedInUsbmux)(loadedModule, this.udid, this.log); try { await loadedModule.Services.getTunnelForDevice(this.udid, { waitMs: TUNNEL_REGISTRY_PORT_PROBE_TIMEOUT_MS, }); this.services = loadedModule.Services; this.enabled = true; this.log.debug(`RemoteXPC enabled for '${this.udid}' (tunnel registry reachable)`); } catch (err) { this.noteTunnelUnavailable('session initialization', err); } } async requireModule() { await this.ensureInitialized(); if (!this.module) { throw (0, utils_2.wrapRemoteXPCConnectionError)(this.lastImportError ?? new Error('appium-ios-remotexpc is not available'), `RemoteXPC module is not available for '${this.udid}'`); } return this.module; } handleServiceError(feature, err, onNonTunnelFailure) { const message = (0, utils_2.formatRemoteXPCFallbackLog)(feature, err); if ((0, utils_2.isTunnelAvailabilityError)(err)) { if (onNonTunnelFailure === 'log') { this.log.warn(message); } return; } if (onNonTunnelFailure === 'log') { this.log.error(message); } } } exports.RemoteXPCFacade = RemoteXPCFacade; //# sourceMappingURL=facade.js.map