appium-remote-debugger
Version:
Appium proxy for Remote Debugger protocol
274 lines (254 loc) • 9.94 kB
text/typescript
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));
}
}