appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
207 lines • 8.95 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.sessionClaimHandler = exports.SessionClaimHandler = void 0;
exports.setSharedIpcForTesting = setSharedIpcForTesting;
exports.resetDriverInstanceIpcForTesting = resetDriverInstanceIpcForTesting;
const promises_1 = require("node:timers/promises");
const support_1 = require("appium/support");
const asyncbox_1 = require("asyncbox");
const utils_1 = require("./utils");
class SessionClaimHandler {
getIpc;
static CLAIMED_TOPIC = 'xcuitest:sessionUdidClaimed';
static CONTENDED_TOPIC = 'xcuitest:sessionUdidContended';
static RELEASED_TOPIC = 'xcuitest:sessionUdidReleased';
static CONTENTION_PROBE_MS = 10;
static RELEASE_WAIT_MS = 15000;
subscriptionsBySessionId = new Map();
constructor(getIpc) {
this.getIpc = getIpc;
}
/** Subscribe the current session to udid claim messages from other sessions. */
async registerActiveSession(driver) {
const ipc = await this.getIpc();
const udid = driver.opts.udid;
const sessionId = driver.sessionId;
if (!ipc || !udid || !sessionId) {
return;
}
this.unregisterActiveSession(driver);
const subscription = ipc.subscribe(SessionClaimHandler.CLAIMED_TOPIC, this.getPublisherId(driver));
subscription.on('message', (message) => {
void this.dispatchSessionUdidMessage(driver, udid, sessionId, message);
});
this.subscriptionsBySessionId.set(sessionId, subscription);
}
/** Unsubscribe the current session from udid claim messages. */
unregisterActiveSession(driver) {
const sessionId = driver.sessionId;
if (!sessionId) {
return;
}
this.subscriptionsBySessionId.get(sessionId)?.unsubscribe();
this.subscriptionsBySessionId.delete(sessionId);
}
/** Publish this session's udid so any existing session on the same device can terminate. */
async claimSessionUdid(driver) {
const ipc = await this.getIpc();
if (!ipc) {
driver.log.debug('Driver-instance IPC is unavailable. Skipping publication of the session udid.');
return;
}
const udid = driver.opts.udid;
const sessionId = driver.sessionId;
if (!udid || !sessionId) {
driver.log.debug('The session udid is not known yet. Skipping udid publication.');
return;
}
const contendingSessionIds = new Set();
const releasedSessionIds = new Set();
const contendedSubscription = ipc.subscribe(SessionClaimHandler.CONTENDED_TOPIC, this.getPublisherId(driver));
const releasedSubscription = ipc.subscribe(SessionClaimHandler.RELEASED_TOPIC, this.getPublisherId(driver));
contendedSubscription.on('message', (message) => {
if (this.isMatchingSessionUdidMessage(message, udid, sessionId)) {
contendingSessionIds.add(message.data.sessionId);
}
});
releasedSubscription.on('message', (message) => {
if (this.isMatchingSessionUdidMessage(message, udid, sessionId)) {
releasedSessionIds.add(message.data.sessionId);
}
});
try {
await ipc.publish(SessionClaimHandler.CLAIMED_TOPIC, this.getPublisherId(driver), {
udid,
sessionId,
});
await (0, promises_1.setTimeout)(SessionClaimHandler.CONTENTION_PROBE_MS);
if (contendingSessionIds.size === 0) {
return;
}
try {
await (0, asyncbox_1.waitForCondition)(() => [...contendingSessionIds].every((id) => releasedSessionIds.has(id)), {
waitMs: SessionClaimHandler.RELEASE_WAIT_MS,
intervalMs: 50,
});
driver.log.debug(`Received release confirmation from ` +
`${support_1.util.pluralize('session', contendingSessionIds.size, true)} for udid '${udid}'`);
}
catch {
const pendingSessionIds = [...contendingSessionIds].filter((id) => !releasedSessionIds.has(id));
driver.log.warn(`Timed out after ${SessionClaimHandler.RELEASE_WAIT_MS}ms waiting for ` +
`${support_1.util.pluralize('session', pendingSessionIds.length, true)} ` +
`[${pendingSessionIds.join(', ')}] to release udid '${udid}'. ` +
`Proceeding with session startup.`);
}
}
finally {
contendedSubscription.unsubscribe();
releasedSubscription.unsubscribe();
}
}
/** @internal Exposed for unit tests. */
resetForTesting() {
for (const subscription of this.subscriptionsBySessionId.values()) {
subscription.unsubscribe();
}
this.subscriptionsBySessionId.clear();
}
async dispatchSessionUdidMessage(driver, udid, sessionId, message) {
try {
await this.handleSessionUdidMessage(driver, udid, message);
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
driver.log.warn(`Could not handle udid claim IPC message for session '${sessionId}': ${msg}`);
}
}
async handleSessionUdidMessage(driver, udid, message) {
if (!this.isMatchingSessionUdidMessage(message, udid, driver.sessionId ?? undefined)) {
return;
}
try {
await this.publishSessionUdidContended(driver, udid);
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
driver.log.warn(`Could not publish udid contention message for session '${driver.sessionId}': ${msg}`);
}
driver.log.warn(`Session '${message.data.sessionId}' is starting on udid '${udid}', which is already in use ` +
`by another session identified by ${driver.sessionId}. Running multiple parallel sessions on the same ` +
`device is highly discouraged. Consider enabling the Appium server's '--session-override' flag ` +
`and make sure to properly quit the previous session before starting a new one. ` +
`Terminating the obsolete session.`);
await this.terminateSessionOnRequest(driver, udid);
}
async publishSessionUdidContended(driver, udid) {
const ipc = await this.getIpc();
const sessionId = driver.sessionId ?? undefined;
if (!ipc || !udid || !sessionId) {
return;
}
await ipc.publish(SessionClaimHandler.CONTENDED_TOPIC, this.getPublisherId(driver), {
udid,
sessionId,
});
}
async terminateSessionOnRequest(driver, udid) {
const sessionId = driver.sessionId ?? undefined;
const publisherId = sessionId ? this.getPublisherId(driver) : undefined;
const { log } = driver;
try {
await driver.deleteSession();
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
log.warn(`Could not terminate session '${sessionId}' on IPC request: ${msg}`);
}
await this.publishSessionUdidReleased(log, udid, sessionId, publisherId);
}
async publishSessionUdidReleased(log, udid, sessionId, publisherId) {
try {
const ipc = await this.getIpc();
if (!ipc || !udid || !sessionId || !publisherId) {
return;
}
await ipc.publish(SessionClaimHandler.RELEASED_TOPIC, publisherId, {
udid,
sessionId,
});
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
log.warn(`Could not publish udid release message for session '${sessionId}': ${msg}`);
}
}
getPublisherId(driver) {
return support_1.node.getObjectId(driver);
}
isMatchingSessionUdidMessage(message, udid, sessionId) {
return message.data.udid === udid && message.data.sessionId !== sessionId;
}
}
exports.SessionClaimHandler = SessionClaimHandler;
const loadSharedIpc = (0, utils_1.memoize)(async function loadSharedIpc() {
try {
const { AppiumIpc } = (await import('appium/driver.js'));
return AppiumIpc ? new AppiumIpc() : undefined;
}
catch {
return undefined;
}
});
exports.sessionClaimHandler = new SessionClaimHandler(loadSharedIpc);
/**
* @internal Exposed for unit tests.
*/
function setSharedIpcForTesting(ipc) {
loadSharedIpc.cache.set(undefined, Promise.resolve(ipc));
}
/**
* @internal Exposed for unit tests.
*/
function resetDriverInstanceIpcForTesting() {
exports.sessionClaimHandler.resetForTesting();
loadSharedIpc.cache.clear();
}
//# sourceMappingURL=session-claim-handler.js.map