appium-remote-debugger
Version:
Appium proxy for Remote Debugger protocol
329 lines • 13.7 kB
JavaScript
"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