UNPKG

appium-remote-debugger

Version:
274 lines (254 loc) 9.94 kB
import {events} from './events'; import {pageArrayFromDict, appInfoFromDict} from '../utils'; import _ from 'lodash'; import { setAppIdKey, getAppDict, getAppIdKey, getBundleId, getNavigatingToPage, setCurrentState, setConnectedDrivers, getSkippedApps, } from './property-accessors'; import type {RemoteDebugger} from '../remote-debugger'; import type {StringRecord} from '@appium/types'; import type {AppDict} from '../types'; /* * Generic callbacks used throughout the lifecycle of the Remote Debugger. * These will be added to the prototype. */ /** * Handles page change notifications from the remote debugger. * Updates the page array for the specified application and emits a page change * event if the pages have actually changed and navigation is not in progress. * * @param err - Error object if an error occurred, null or undefined otherwise. * @param appIdKey - The application identifier key for which pages have changed. * @param pageDict - Dictionary containing the new page information. */ export async function onPageChange( this: RemoteDebugger, err: Error | null | undefined, appIdKey: string, pageDict: StringRecord, ): Promise<void> { if (_.isEmpty(pageDict)) { return; } const currentPages = pageArrayFromDict(pageDict); // save the page dict for this app if (getAppDict(this)[appIdKey]) { const previousPages = getAppDict(this)[appIdKey].pageArray; // we have a pre-existing pageDict if (previousPages && _.isEqual(previousPages, currentPages)) { this.log.debug( `Received page change notice for app '${appIdKey}' ` + `but the listing has not changed. Ignoring.`, ); return; } // keep track of the page dictionary getAppDict(this)[appIdKey].pageArray = currentPages; this.log.debug( `Pages changed for ${appIdKey}: ${JSON.stringify(previousPages)} -> ${JSON.stringify(currentPages)}`, ); } if (getNavigatingToPage(this)) { // in the middle of navigating, so reporting a page change will cause problems return; } this.emit(events.EVENT_PAGE_CHANGE, { appIdKey: appIdKey.replace('PID:', ''), pageArray: currentPages, }); } /** * Handles notifications when a new application connects to the remote debugger. * Updates the application dictionary with the new application information. * * @param err - Error object if an error occurred, null or undefined otherwise. * @param dict - Dictionary containing the new application information including * the WIRApplicationIdentifierKey. */ export async function onAppConnect( this: RemoteDebugger, err: Error | null | undefined, dict: StringRecord, ): Promise<void> { const appIdKey = dict.WIRApplicationIdentifierKey; this.log.debug(`Notified that new application '${appIdKey}' has connected`); updateAppsWithDict.bind(this)(dict); } /** * Handles notifications when an application disconnects from the remote debugger. * Removes the application from the dictionary and attempts to find a replacement * if the disconnected app was the currently selected one. Emits a disconnect event * if no applications remain. * * @param err - Error object if an error occurred, null or undefined otherwise. * @param dict - Dictionary containing the disconnected application information * including the WIRApplicationIdentifierKey. */ export function onAppDisconnect( this: RemoteDebugger, err: Error | null | undefined, dict: StringRecord, ): void { const appIdKey = dict.WIRApplicationIdentifierKey; this.log.debug(`Application '${appIdKey}' disconnected. Removing from app dictionary.`); this.log.debug(`Current app is '${getAppIdKey(this)}'`); // get rid of the entry in our app dictionary, // since it is no longer available delete getAppDict(this)[appIdKey]; // if the disconnected app is the one we are connected to, try to find another if (getAppIdKey(this) === appIdKey) { this.log.debug(`No longer have app id. Attempting to find new one.`); setAppIdKey(this, getDebuggerAppKey.bind(this)(getBundleId(this) as string)); } if (_.isEmpty(getAppDict(this))) { // this means we no longer have any apps. what the what? this.log.debug('Main app disconnected. Disconnecting altogether.'); this.emit(events.EVENT_DISCONNECT, true); } } /** * Handles notifications when an application's information is updated. * Updates the application dictionary with the new information while preserving * any existing page array data. * * @param err - Error object if an error occurred, null or undefined otherwise. * @param dict - Dictionary containing the updated application information. */ export async function onAppUpdate( this: RemoteDebugger, err: Error | null | undefined, dict: StringRecord, ): Promise<void> { this.log.debug(`Notified that an application has been updated`); updateAppsWithDict.bind(this)(dict); } /** * Handles notifications containing the list of connected drivers. * Updates the internal connected drivers list with the received information. * * @param err - Error object if an error occurred, null or undefined otherwise. * @param drivers - Dictionary containing the connected driver list with * WIRDriverDictionaryKey. */ export function onConnectedDriverList( this: RemoteDebugger, err: Error | null | undefined, drivers: StringRecord, ): void { setConnectedDrivers(this, drivers.WIRDriverDictionaryKey); this.log.debug(`Received connected driver list: ${JSON.stringify(this.connectedDrivers)}`); } /** * Handles notifications about the current automation availability state. * This state changes when 'Remote Automation' setting in Safari's advanced settings * is toggled. The state can be either WIRAutomationAvailabilityAvailable or * WIRAutomationAvailabilityNotAvailable. * * @param err - Error object if an error occurred, null or undefined otherwise. * @param state - Dictionary containing the automation availability state with * WIRAutomationAvailabilityKey. */ export function onCurrentState( this: RemoteDebugger, err: Error | null | undefined, state: StringRecord, ): void { setCurrentState(this, state.WIRAutomationAvailabilityKey); // This state changes when 'Remote Automation' in 'Settings app' > 'Safari' > 'Advanced' > 'Remote Automation' changes // WIRAutomationAvailabilityAvailable or WIRAutomationAvailabilityNotAvailable this.log.debug( `Received connected automation availability state: ${JSON.stringify(this.currentState)}`, ); } /** * Handles notifications containing the list of connected applications. * Translates the received information into the application dictionary format, * filtering out any applications that are in the skipped apps list. * * @param err - Error object if an error occurred, null or undefined otherwise. * @param apps - Dictionary containing the connected applications list. */ export async function onConnectedApplicationList( this: RemoteDebugger, err: Error | null | undefined, apps: StringRecord, ): Promise<void> { this.log.debug(`Received connected applications list: ${_.keys(apps).join(', ')}`); // translate the received information into an easier-to-manage // hash with app id as key, and app info as value const newDict: AppDict = {}; for (const dict of _.values(apps)) { const [id, entry] = appInfoFromDict(dict); if (getSkippedApps(this).includes(entry.name)) { continue; } newDict[id] = entry; } // update the object's list of apps _.defaults(getAppDict(this), newDict); } /** * Given a bundle ID, finds the correct remote debugger app identifier key * that is currently connected. Also handles proxy applications that may act * on behalf of the requested bundle ID. * * @param bundleId - The bundle identifier to search for. * @returns The application identifier key if found, undefined otherwise. * If a proxy application is found, returns the proxy's app ID instead. */ export function getDebuggerAppKey(this: RemoteDebugger, bundleId: string): string | undefined { let appId: string | undefined; for (const [key, data] of _.toPairs(getAppDict(this))) { if (data.bundleId === bundleId) { appId = key; break; } } // now we need to determine if we should pick a proxy for this instead if (appId) { this.log.debug(`Found app id key '${appId}' for bundle '${bundleId}'`); let proxyAppId: string | undefined; for (const [key, data] of _.toPairs(getAppDict(this))) { if (data.isProxy && data.hostId === appId) { this.log.debug( `Found separate bundleId '${data.bundleId}' ` + `acting as proxy for '${bundleId}', with app id '${key}'`, ); // set the app id... the last one will be used, so just keep re-assigning proxyAppId = key; } } if (proxyAppId) { appId = proxyAppId; this.log.debug(`Using proxied app id '${appId}'`); } } return appId; } /** * Updates the application dictionary with information from the provided dictionary. * Preserves existing page array data if the application already exists in the dictionary. * Attempts to set the app ID key if one is not currently set. * * @param dict - Dictionary containing application information to add or update. */ function updateAppsWithDict(this: RemoteDebugger, dict: StringRecord): void { // get the dictionary entry into a nice form, and add it to the // application dictionary const [id, entry] = appInfoFromDict(dict); if (getAppDict(this)[id]?.pageArray) { // preserve the page dictionary for this entry entry.pageArray = getAppDict(this)[id].pageArray; } getAppDict(this)[id] = entry; // try to get the app id from our connected apps if (!getAppIdKey(this)) { setAppIdKey(this, getDebuggerAppKey.bind(this)(getBundleId(this) as string)); } }