appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
244 lines • 10.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DeviceConnectionsFactory = exports.DEVICE_CONNECTIONS_FACTORY = void 0;
const lodash_1 = __importDefault(require("lodash"));
const net_1 = __importDefault(require("net"));
const bluebird_1 = __importDefault(require("bluebird"));
const support_1 = require("appium/support");
const appium_ios_device_1 = require("appium-ios-device");
const portscanner_1 = require("portscanner");
const asyncbox_1 = require("asyncbox");
const LOCALHOST = '127.0.0.1';
class iProxy {
constructor(udid, localport, deviceport) {
this.localport = parseInt(localport, 10);
this.deviceport = parseInt(deviceport, 10);
this.udid = udid;
this.localServer = null;
this.log = support_1.logger.getLogger(`iProxy@${udid.substring(0, 8)}:${this.localport}`);
}
async start() {
if (this.localServer) {
return;
}
this.localServer = net_1.default.createServer(async (localSocket) => {
let remoteSocket;
try {
// We can only connect to the remote socket after the local socket connection succeeds
remoteSocket = await appium_ios_device_1.utilities.connectPort(this.udid, this.deviceport);
}
catch (e) {
this.log.debug(e.message);
localSocket.destroy();
return;
}
const destroyCommChannel = () => {
remoteSocket.unpipe(localSocket);
localSocket.unpipe(remoteSocket);
};
remoteSocket.once('close', () => {
destroyCommChannel();
localSocket.destroy();
});
// not all remote socket errors are critical for the user
remoteSocket.on('error', (e) => this.log.debug(e));
localSocket.once('end', destroyCommChannel);
localSocket.once('close', () => {
destroyCommChannel();
remoteSocket.destroy();
});
localSocket.on('error', (e) => this.log.warn(e.message));
localSocket.pipe(remoteSocket);
remoteSocket.pipe(localSocket);
});
const listeningPromise = new bluebird_1.default((resolve, reject) => {
/** @type {net.Server} */ (this.localServer).once('listening', resolve);
/** @type {net.Server} */ (this.localServer).once('error', reject);
});
this.localServer.listen(this.localport);
try {
await listeningPromise;
}
catch (e) {
this.localServer = null;
throw e;
}
this.localServer.on('error', (e) => this.log.warn(e.message));
this.localServer.once('close', (e) => {
if (e) {
this.log.info(`The connection has been closed with error ${e.message}`);
}
else {
this.log.info(`The connection has been closed`);
}
this.localServer = null;
});
this.onBeforeProcessExit = this._closeLocalServer.bind(this);
// Make sure we free up the socket on process exit
process.on('beforeExit', this.onBeforeProcessExit);
}
_closeLocalServer() {
if (!this.localServer) {
return;
}
this.log.debug(`Closing the connection`);
this.localServer.close();
this.localServer = null;
}
stop() {
if (this.onBeforeProcessExit) {
process.off('beforeExit', this.onBeforeProcessExit);
this.onBeforeProcessExit = null;
}
this._closeLocalServer();
}
}
const log = support_1.logger.getLogger('DevCon Factory');
const PORT_CLOSE_TIMEOUT = 15 * 1000; // 15 seconds
const SPLITTER = ':';
class DeviceConnectionsFactory {
constructor() {
this._connectionsMapping = {};
}
_udidAsToken(udid) {
return `${support_1.util.hasValue(udid) ? udid : ''}${SPLITTER}`;
}
_portAsToken(port) {
return `${SPLITTER}${support_1.util.hasValue(port) ? port : ''}`;
}
_toKey(udid = null, port = null) {
return `${support_1.util.hasValue(udid) ? udid : ''}${SPLITTER}${support_1.util.hasValue(port) ? port : ''}`;
}
_releaseProxiedConnections(connectionKeys) {
const keys = connectionKeys.filter((k) => lodash_1.default.has(this._connectionsMapping[k], 'iproxy'));
for (const key of keys) {
log.info(`Releasing the listener for '${key}'`);
try {
this._connectionsMapping[key].iproxy.stop();
}
catch (e) {
log.debug(e);
}
}
return keys;
}
listConnections(udid = null, port = null, strict = false) {
if (!udid && !port) {
return [];
}
// `this._connectionMapping` keys have format `udid:port`
// the `strict` argument enforces to match keys having both `udid` and `port`
// if they are defined
// while in non-strict mode keys having any of these are going to be matched
return lodash_1.default.keys(this._connectionsMapping).filter((key) => strict && udid && port
? key === this._toKey(udid, port)
: (udid && key.startsWith(this._udidAsToken(udid))) ||
(port && key.endsWith(this._portAsToken(port))));
}
async requestConnection(udid, port, options = {}) {
if (!udid || !port) {
log.warn('Did not know how to request the connection:');
if (!udid) {
log.warn('- Device UDID is unset');
}
if (!port) {
log.warn('- The local port number is unset');
}
return;
}
const { usePortForwarding, devicePort } = options;
log.info(`Requesting connection for device ${udid} on local port ${port}` +
(devicePort ? `, device port ${devicePort}` : ''));
log.debug(`Cached connections count: ${lodash_1.default.size(this._connectionsMapping)}`);
const connectionsOnPort = this.listConnections(null, port);
if (!lodash_1.default.isEmpty(connectionsOnPort)) {
log.info(`Found cached connections on port #${port}: ${JSON.stringify(connectionsOnPort)}`);
}
if (usePortForwarding) {
let isPortBusy = (await (0, portscanner_1.checkPortStatus)(port, LOCALHOST)) === 'open';
if (isPortBusy) {
log.warn(`Port #${port} is busy. Did you quit the previous driver session(s) properly?`);
if (!lodash_1.default.isEmpty(connectionsOnPort)) {
log.info('Trying to release the port');
for (const key of this._releaseProxiedConnections(connectionsOnPort)) {
delete this._connectionsMapping[key];
}
const timer = new support_1.timing.Timer().start();
try {
await (0, asyncbox_1.waitForCondition)(async () => {
try {
if ((await (0, portscanner_1.checkPortStatus)(port, LOCALHOST)) !== 'open') {
log.info(`Port #${port} has been successfully released after ` +
`${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
isPortBusy = false;
return true;
}
}
catch { }
return false;
}, {
waitMs: PORT_CLOSE_TIMEOUT,
intervalMs: 300,
});
}
catch {
log.warn(`Did not know how to release port #${port} in ` +
`${timer.getDuration().asMilliSeconds.toFixed(0)}ms`);
}
}
}
if (isPortBusy) {
throw new Error(`The port #${port} is occupied by an other process. ` +
`You can either quit that process or select another free port.`);
}
}
const currentKey = this._toKey(udid, port);
if (usePortForwarding) {
const iproxy = new iProxy(udid, port, devicePort);
try {
await iproxy.start();
this._connectionsMapping[currentKey] = { iproxy };
}
catch (e) {
try {
iproxy.stop();
}
catch (e1) {
log.debug(e1);
}
throw e;
}
}
else {
this._connectionsMapping[currentKey] = {};
}
log.info(`Successfully requested the connection for ${currentKey}`);
}
releaseConnection(udid = null, port = null) {
if (!udid && !port) {
log.warn('Neither device UDID nor local port is set. ' +
'Did not know how to release the connection');
return;
}
log.info(`Releasing connections for ${udid || 'any'} device on ${port || 'any'} port number`);
const keys = this.listConnections(udid, port, true);
if (lodash_1.default.isEmpty(keys)) {
log.info('No cached connections have been found');
return;
}
log.info(`Found cached connections to release: ${JSON.stringify(keys)}`);
this._releaseProxiedConnections(keys);
for (const key of keys) {
delete this._connectionsMapping[key];
}
log.debug(`Cached connections count: ${lodash_1.default.size(this._connectionsMapping)}`);
}
}
exports.DeviceConnectionsFactory = DeviceConnectionsFactory;
const DEVICE_CONNECTIONS_FACTORY = new DeviceConnectionsFactory();
exports.DEVICE_CONNECTIONS_FACTORY = DEVICE_CONNECTIONS_FACTORY;
exports.default = DEVICE_CONNECTIONS_FACTORY;
//# sourceMappingURL=device-connections-factory.js.map