appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
170 lines • 6.89 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.LockdownClient = void 0;
const appium_ios_device_1 = require("appium-ios-device");
const logger_1 = require("../logger");
const remote_xpc_1 = require("./remote-xpc");
/**
* Unified lockdown access for real devices.
*
* On iOS/tvOS 18+ attempts a tunnel registry RemoteXPC connection and lockdown over RSD
* (`createLockdownServiceForTunnel`). When that path is unavailable, uses
* {@linkcode utilities} from `appium-ios-device` (USB/local usbmux).
*/
class LockdownClient {
udid;
log;
strategy;
remoteXPCFacade;
constructor(udid, log, strategy, remoteXPCFacade = null) {
this.udid = udid;
this.log = log;
this.strategy = strategy;
this.remoteXPCFacade = remoteXPCFacade;
}
/**
* @param udid - Device UDID
* @param opts - Creation options
*/
static async createForDevice(udid, opts = {}) {
const { allowLegacyFallback = true, facade = null, logger = logger_1.log } = opts;
if (!facade?.eligible) {
if (!allowLegacyFallback) {
throw new Error(`RemoteXPC lockdown is required for '${udid}', but this session is not eligible ` + `for RemoteXPC.`);
}
return new LockdownClient(udid, logger, 'ios-device');
}
try {
const strategy = await facade.resolveLockdownStrategy();
return new LockdownClient(udid, logger, strategy, facade);
}
catch (err) {
if (!(0, remote_xpc_1.isRemoteXPCUnavailableError)(err)) {
throw err;
}
if (!allowLegacyFallback) {
throw err;
}
logger.warn('RemoteXPC lockdown is not available. Using appium-ios-device lockdown (legacy fallback).');
return new LockdownClient(udid, logger, 'ios-device');
}
}
static coerceFiniteNumber(value) {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'bigint') {
const converted = Number(value);
return Number.isFinite(converted) ? converted : undefined;
}
if (typeof value === 'string') {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return undefined;
}
async close() {
// Tunnel strategy opens lockdown per operation; nothing to close here.
}
/**
* Full lockdown `GetValue` payload (`GetValue` with no key/domain).
*/
async getDeviceInfo() {
if (this.strategy === 'ios-device') {
return await appium_ios_device_1.utilities.getDeviceInfo(this.udid);
}
return (await this.runWithRemotexpcLockdownRequiringValue((lockdown) => lockdown.getDeviceInfo(), 'device info payload'));
}
/**
* Device ProductVersion from lockdown.
*
* Uses the same lockdown selection strategy as {@linkcode getDeviceInfo}.
* If a RemoteXPC lockdown payload does not include ProductVersion, throws.
*/
async getOSVersion() {
if (this.strategy === 'ios-device') {
return await appium_ios_device_1.utilities.getOSVersion(this.udid);
}
return await this.runWithRemotexpcLockdownRequiringValue((lockdown) => lockdown.getProductVersion(), 'ProductVersion');
}
/**
* Fields needed to format device local time (same contract as {@linkcode utilities.getDeviceTime}).
*/
async getDeviceTimeFields() {
const readTimeFromLockdown = async (lockdown) => {
const info = await lockdown.getDeviceInfo();
const timestamp = LockdownClient.coerceFiniteNumber(info.TimeIntervalSince1970);
const tzOffsetSeconds = LockdownClient.coerceFiniteNumber(info.TimeZoneOffsetFromUTC);
if (timestamp === undefined || tzOffsetSeconds === undefined) {
return undefined;
}
return { timestamp, utcOffset: tzOffsetSeconds / 60 };
};
switch (this.strategy) {
case 'ios-device': {
const { timestamp, utcOffset, timeZone } = await appium_ios_device_1.utilities.getDeviceTime(this.udid);
return {
timestamp,
utcOffset: this.normalizeUtcOffsetMinutes(utcOffset, timeZone),
};
}
case 'remotexpc-usbmux':
case 'remotexpc-tunnel':
return await this.runWithRemotexpcLockdownRequiringValue(readTimeFromLockdown, 'device time fields');
}
}
requireRemoteXPCFacade() {
if (!this.remoteXPCFacade) {
throw new Error(`RemoteXPC lockdown is not configured for '${this.udid}'.`);
}
return this.remoteXPCFacade;
}
/**
* Legacy ios-device can provide inconsistent offset payloads. Normalize to a final offset in
* minutes for consumers.
*/
normalizeUtcOffsetMinutes(utcOffset, timeZone) {
// Normal/expected: offset already in minutes.
if (Math.abs(utcOffset) <= 12 * 60) {
return utcOffset;
}
// Sometimes `timeZone` is a numeric offset in seconds.
const offsetSeconds = typeof timeZone === 'number' ? timeZone : Number(timeZone);
if (Number.isFinite(offsetSeconds) && Math.abs(offsetSeconds) <= 12 * 60 * 60) {
return offsetSeconds / 60;
}
this.log.warn(`Did not know how to apply UTC offset from lockdown (utcOffset=${utcOffset}, timeZone=${timeZone}). ` +
`Using UTC.`);
return 0;
}
async runWithRemotexpcUsbmuxLockdown(fn) {
return await this.requireRemoteXPCFacade().withUsbMuxLockdown(fn);
}
async runWithRemotexpcLockdown(fn) {
switch (this.strategy) {
case 'remotexpc-usbmux':
return await this.runWithRemotexpcUsbmuxLockdown(fn);
case 'remotexpc-tunnel':
return await this.runWithTunnelLockdown(fn);
default:
throw new Error(`RemoteXPC lockdown is not active for '${this.udid}'.`);
}
}
async runWithRemotexpcLockdownRequiringValue(fn, valueName) {
const value = await this.runWithRemotexpcLockdown(fn);
if (!value) {
throw new Error(`RemoteXPC ${this.getRemotexpcLockdownLabel()} lockdown did not return ${valueName} for '${this.udid}'.`);
}
return value;
}
getRemotexpcLockdownLabel() {
return this.strategy === 'remotexpc-usbmux' ? 'USB' : 'tunnel';
}
async runWithTunnelLockdown(fn) {
return await this.requireRemoteXPCFacade().withTunnelLockdown(fn);
}
}
exports.LockdownClient = LockdownClient;
//# sourceMappingURL=lockdown-client.js.map