UNPKG

appium-remote-debugger

Version:
329 lines 13.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const logger_1 = __importDefault(require("../logger")); const utils_1 = require("../utils"); const events_1 = __importDefault(require("./events")); const support_1 = require("@appium/support"); const asyncbox_1 = require("asyncbox"); const lodash_1 = __importDefault(require("lodash")); const APP_CONNECT_TIMEOUT_MS = 0; const APP_CONNECT_INTERVAL_MS = 100; const SELECT_APP_RETRIES = 20; const SELECT_APP_RETRY_SLEEP_MS = 500; const SAFARI_BUNDLE_ID = 'com.apple.mobilesafari'; const BLANK_PAGE_URL = 'about:blank'; /** * @typedef {Object} AppPages * @property {string} appIdKey * @property {Record<string, any>} pageDict */ /** * @typedef {Object} App * @property {string} id * @property {string} bundleId */ /** * * @this {import('../remote-debugger').RemoteDebugger} */ async function setConnectionKey() { logger_1.default.debug('Sending connection key request'); if (!this.rpcClient) { throw new Error('rpcClient is undefined. Is the debugger connected?'); } // send but only wait to make sure the socket worked // as response from Web Inspector can take a long time await this.rpcClient.send('setConnectionKey', {}, false); } /** * * @this {import('../remote-debugger').RemoteDebugger} */ async function connect(timeout = APP_CONNECT_TIMEOUT_MS) { this.setup(); // initialize the rpc client this.initRpcClient(); if (!this.rpcClient) { throw new Error('rpcClient is undefined. Is the debugger connected?'); } // listen for basic debugger-level events this.rpcClient.on('_rpc_reportSetup:', lodash_1.default.noop); this.rpcClient.on('_rpc_forwardGetListing:', this.onPageChange.bind(this)); this.rpcClient.on('_rpc_reportConnectedApplicationList:', this.onConnectedApplicationList.bind(this)); this.rpcClient.on('_rpc_applicationConnected:', this.onAppConnect.bind(this)); this.rpcClient.on('_rpc_applicationDisconnected:', this.onAppDisconnect.bind(this)); this.rpcClient.on('_rpc_applicationUpdated:', this.onAppUpdate.bind(this)); this.rpcClient.on('_rpc_reportConnectedDriverList:', this.onConnectedDriverList.bind(this)); this.rpcClient.on('_rpc_reportCurrentState:', this.onCurrentState.bind(this)); this.rpcClient.on('Page.frameDetached', this.frameDetached.bind(this)); await this.rpcClient.connect(); // get the connection information about the app try { this.setConnectionKey(); if (timeout) { logger_1.default.debug(`Waiting up to ${timeout}ms for applications to be reported`); try { await (0, asyncbox_1.waitForCondition)(() => !lodash_1.default.isEmpty(this.appDict), { waitMs: timeout, intervalMs: APP_CONNECT_INTERVAL_MS, }); } catch (err) { logger_1.default.debug(`Timed out waiting for applications to be reported`); } } return this.appDict || {}; } catch (err) { logger_1.default.error(`Error setting connection key: ${err.message}`); await this.disconnect(); throw err; } } /** * * @this {import('../remote-debugger').RemoteDebugger} * @returns {Promise<void>} */ async function disconnect() { if (this.rpcClient) { await this.rpcClient.disconnect(); } this.emit(events_1.default.EVENT_DISCONNECT, true); this.teardown(); } /** * * @this {import('../remote-debugger').RemoteDebugger} * @param {string?} currentUrl * @param {number} [maxTries] * @param {boolean} [ignoreAboutBlankUrl] * @returns {Promise<AppPages[]>} */ async function selectApp(currentUrl = null, maxTries = SELECT_APP_RETRIES, ignoreAboutBlankUrl = false) { logger_1.default.debug('Selecting application'); if (!this.rpcClient) { throw new Error('rpcClient is undefined. Is the debugger connected?'); } const shouldCheckForTarget = this.rpcClient.shouldCheckForTarget; this.rpcClient.shouldCheckForTarget = false; try { const timer = new support_1.timing.Timer().start(); if (!this.appDict || lodash_1.default.isEmpty(this.appDict)) { logger_1.default.debug('No applications currently connected.'); return []; } const { appIdKey, pageDict } = await this.searchForApp(currentUrl, maxTries, ignoreAboutBlankUrl); // if, after all this, we have no dictionary, we have failed if (!appIdKey || !pageDict) { logger_1.default.errorAndThrow(`Could not connect to a valid app after ${maxTries} tries.`); } if (this.appIdKey !== appIdKey) { logger_1.default.debug(`Received altered app id, updating from '${this.appIdKey}' to '${appIdKey}'`); this.appIdKey = appIdKey; } logApplicationDictionary(this.appDict); // translate the dictionary into a useful form, and return to sender const pageArray = lodash_1.default.isEmpty(this.appDict[appIdKey].pageArray) ? (0, utils_1.pageArrayFromDict)(pageDict) : this.appDict[appIdKey].pageArray; logger_1.default.debug(`Finally selecting app ${this.appIdKey}: ${(0, utils_1.simpleStringify)(pageArray)}`); let fullPageArray = []; for (const [app, info] of lodash_1.default.toPairs(this.appDict)) { if (!lodash_1.default.isArray(info.pageArray) || !info.isActive) { continue; } const id = app.replace('PID:', ''); for (const page of info.pageArray) { if (!(ignoreAboutBlankUrl && page.url === BLANK_PAGE_URL)) { let pageDict = lodash_1.default.clone(page); pageDict.id = `${id}.${pageDict.id}`; pageDict.bundleId = info.bundleId; fullPageArray.push(pageDict); } } } logger_1.default.debug(`Selected app after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); return fullPageArray; } finally { this.rpcClient.shouldCheckForTarget = shouldCheckForTarget; } } /** * * @this {import('../remote-debugger').RemoteDebugger} * @param {string?} currentUrl * @param {number} maxTries * @param {boolean} ignoreAboutBlankUrl * @returns {Promise<AppPages?>} */ async function searchForApp(currentUrl, maxTries, ignoreAboutBlankUrl) { const bundleIds = this.includeSafari && !this.isSafari ? [this.bundleId, ...this.additionalBundleIds, SAFARI_BUNDLE_ID] : [this.bundleId, ...this.additionalBundleIds]; try { return await (0, asyncbox_1.retryInterval)(maxTries, SELECT_APP_RETRY_SLEEP_MS, async (retryCount) => { if (!this.rpcClient) { throw new Error('rpcClient is undefined. Is the debugger connected?'); } logApplicationDictionary(this.appDict); const possibleAppIds = (0, utils_1.getPossibleDebuggerAppKeys)(bundleIds, this.appDict); logger_1.default.debug(`Trying out the possible app ids: ${possibleAppIds.join(', ')} (try #${retryCount + 1} of ${maxTries})`); for (const attemptedAppIdKey of possibleAppIds) { try { if (!this.appDict[attemptedAppIdKey].isActive) { logger_1.default.debug(`Skipping app '${attemptedAppIdKey}' because it is not active`); continue; } logger_1.default.debug(`Attempting app '${attemptedAppIdKey}'`); const [appIdKey, pageDict] = await this.rpcClient.selectApp(attemptedAppIdKey); // in iOS 8.2 the connect logic happens, but with an empty dictionary // which leads to the remote debugger getting disconnected, and into a loop if (lodash_1.default.isEmpty(pageDict)) { logger_1.default.debug('Empty page dictionary received. Trying again.'); continue; } // save the page array for this app this.appDict[appIdKey].pageArray = (0, utils_1.pageArrayFromDict)(pageDict); // if we are looking for a particular url, make sure we // have the right page. Ignore empty or undefined urls. // Ignore about:blank if requested. const result = this.searchForPage(this.appDict, currentUrl, ignoreAboutBlankUrl); if (result) { return result; } if (currentUrl) { logger_1.default.debug(`Received app, but expected url ('${currentUrl}') was not found. Trying again.`); } else { logger_1.default.debug('Received app, but no match was found. Trying again.'); } } catch (err) { logger_1.default.debug(`Error checking application: '${err.message}'. Retrying connection`); } } retryCount++; throw new Error('Failed to find an app to select'); }, 0); } catch (ign) { logger_1.default.errorAndThrow(`Could not connect to a valid app after ${maxTries} tries.`); } return null; } /** * * @this {import('../remote-debugger').RemoteDebugger} * @param {Record<string, any>} appsDict * @param {string?} currentUrl * @param {boolean} [ignoreAboutBlankUrl] * @returns {AppPages?} */ function searchForPage(appsDict, currentUrl = null, ignoreAboutBlankUrl = false) { for (const appDict of lodash_1.default.values(appsDict)) { if (!appDict || !appDict.isActive || !appDict.pageArray || appDict.pageArray.promise) { continue; } for (const dict of appDict.pageArray) { if ((!ignoreAboutBlankUrl || dict.url !== BLANK_PAGE_URL) && (!currentUrl || dict.url === currentUrl || dict.url === `${currentUrl}/`)) { return { appIdKey: appDict.id, pageDict: dict }; } } } return null; } /** * * @this {import('../remote-debugger').RemoteDebugger} * @param {string} appIdKey * @param {string} pageIdKey * @param {boolean} [skipReadyCheck] * @returns {Promise<void>} */ async function selectPage(appIdKey, pageIdKey, skipReadyCheck = false) { this.appIdKey = `PID:${appIdKey}`; this.pageIdKey = pageIdKey; logger_1.default.debug(`Selecting page '${pageIdKey}' on app '${this.appIdKey}' and forwarding socket setup`); if (!this.rpcClient) { throw new Error('rpcClient is undefined. Is the debugger connected?'); } const timer = new support_1.timing.Timer().start(); await this.rpcClient.selectPage(this.appIdKey, pageIdKey); // make sure everything is ready to go if (!skipReadyCheck && !await this.checkPageIsReady()) { await this.pageUnload(); } logger_1.default.debug(`Selected page after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); } /** * * @param {Record<string, any>} apps * @returns {void} */ function logApplicationDictionary(apps) { function getValueString(key, value) { if (lodash_1.default.isFunction(value)) { return '[Function]'; } if (key === 'pageArray' && !lodash_1.default.isArray(value)) { return `"Waiting for data"`; } return JSON.stringify(value); } logger_1.default.debug('Current applications available:'); for (const [app, info] of lodash_1.default.toPairs(apps)) { logger_1.default.debug(` Application: "${app}"`); for (const [key, value] of lodash_1.default.toPairs(info)) { if (key === 'pageArray' && Array.isArray(value) && value.length) { logger_1.default.debug(` ${key}:`); for (const page of value) { let prefix = '- '; for (const [k, v] of lodash_1.default.toPairs(page)) { logger_1.default.debug(` ${prefix}${k}: ${JSON.stringify(v)}`); prefix = ' '; } } } else { const valueString = getValueString(key, value); logger_1.default.debug(` ${key}: ${valueString}`); } } } } /** * * @this {import('../remote-debugger').RemoteDebugger} * @param {Record<string, any>} dict * @returns {void} */ function updateAppsWithDict(dict) { // get the dictionary entry into a nice form, and add it to the // application dictionary this.appDict = this.appDict || {}; let [id, entry] = (0, utils_1.appInfoFromDict)(dict); if (this.appDict[id]) { // preserve the page dictionary for this entry entry.pageArray = this.appDict[id].pageArray; } this.appDict[id] = entry; // add a promise to get the page dictionary if (lodash_1.default.isUndefined(entry.pageArray)) { entry.pageArray = (0, utils_1.deferredPromise)(); } // try to get the app id from our connected apps if (!this.appIdKey) { this.appIdKey = (0, utils_1.getDebuggerAppKey)(this.bundleId, this.appDict); } } exports.default = { setConnectionKey, connect, disconnect, selectApp, searchForApp, searchForPage, selectPage, updateAppsWithDict }; //# sourceMappingURL=connect.js.map