appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
280 lines • 10.1 kB
JavaScript
"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