appium-remote-debugger
Version:
Appium proxy for Remote Debugger protocol
268 lines (252 loc) • 8.77 kB
text/typescript
import {EventEmitter} from 'node:events';
import {log} from '../logger';
import _ from 'lodash';
import {util} from '@appium/support';
import type {StringRecord} from '@appium/types';
/**
* Represents a data message from the Web Inspector.
*/
interface DataMessage {
id?: string;
method: string;
params: StringRecord;
result: any;
error?: string | DataErrorMessage;
}
/**
* Represents an error message structure in a data message.
*/
interface DataErrorMessage {
message: string;
code: number;
data: any;
}
/**
* Handles messages from the Web Inspector and dispatches them as events.
* Extends EventEmitter to provide event-based message handling.
*/
export default class RpcMessageHandler extends EventEmitter {
/**
* Handles a message from the Web Inspector by parsing the selector
* and emitting appropriate events.
*
* @param plist - The plist message from the Web Inspector containing
* __selector and __argument properties.
*/
async handleMessage(plist: StringRecord): Promise<void> {
const selector = plist.__selector;
if (!selector) {
log.debug('Got an invalid plist');
return;
}
const argument = plist.__argument;
switch (selector) {
case '_rpc_reportSetup:':
this.emit(
'_rpc_reportSetup:',
null,
argument.WIRSimulatorNameKey,
argument.WIRSimulatorBuildKey,
argument.WIRSimulatorProductVersionKey,
);
break;
case '_rpc_reportConnectedApplicationList:':
this.emit(
'_rpc_reportConnectedApplicationList:',
null,
argument.WIRApplicationDictionaryKey,
);
break;
case '_rpc_applicationSentListing:':
this.emit(
'_rpc_forwardGetListing:',
null,
argument.WIRApplicationIdentifierKey,
argument.WIRListingKey,
);
break;
case '_rpc_applicationConnected:':
this.emit('_rpc_applicationConnected:', null, argument);
break;
case '_rpc_applicationDisconnected:':
this.emit('_rpc_applicationDisconnected:', null, argument);
break;
case '_rpc_applicationUpdated:':
this.emit('_rpc_applicationUpdated:', null, argument);
break;
case '_rpc_reportConnectedDriverList:':
this.emit('_rpc_reportConnectedDriverList:', null, argument);
break;
case '_rpc_reportCurrentState:':
this.emit('_rpc_reportCurrentState:', null, argument);
break;
case '_rpc_applicationSentData:':
await this.handleDataMessage(plist);
break;
default:
log.debug(
`Debugger got a message for '${selector}' and have no ` + `handler, doing nothing.`,
);
}
}
/**
* Parses the data key from a plist message.
* The data key is a JSON string that needs to be parsed.
*
* @param plist - The plist message containing the data key.
* @returns The parsed DataMessage object.
* @throws Error if the data key cannot be parsed.
*/
private parseDataKey(plist: StringRecord): DataMessage {
try {
return JSON.parse(plist.__argument.WIRMessageDataKey.toString('utf8'));
} catch (err: any) {
log.error(`Unparseable message data: ${_.truncate(JSON.stringify(plist), {length: 100})}`);
throw new Error(`Unable to parse message data: ${err.message}`);
}
}
/**
* Dispatches a data message by emitting events.
* If msgId is provided, emits a message-specific event.
* Otherwise, emits method-based events with appropriate argument mapping.
*
* @param msgId - If not empty, emits an event with this ID: <msgId, error, result>.
* If empty, emits method-based events: <name, error, ...args>.
* @param method - The method name from the data message.
* @param params - The parameters from the data message.
* @param result - The result from the data message.
* @param error - Any error that occurred during message processing.
*/
private async dispatchDataMessage(
msgId: string,
method: string | undefined,
params: StringRecord | undefined,
result: any,
error: Error | undefined,
): Promise<void> {
if (msgId) {
if (this.listenerCount(msgId)) {
if (_.has(result?.result, 'value')) {
result = result.result.value;
}
this.emit(msgId, error, result);
} else {
log.error(
`Web Inspector returned data for message '${msgId}' ` +
`but we were not waiting for that message! ` +
`result: '${JSON.stringify(result)}'; ` +
`error: '${JSON.stringify(error)}'`,
);
}
return;
}
const eventNames: string[] = method ? [method] : [];
let args: any[] = [params];
// some events have different names, or the arguments are mapped from the
// parameters received
switch (method) {
case 'Page.frameStoppedLoading':
eventNames.push('Page.frameNavigated');
// eslint-disable-next-line no-fallthrough
case 'Page.frameNavigated':
args = [`'${method}' event`];
break;
case 'Timeline.eventRecorded':
args = [params || (params as any)?.record];
break;
case 'Console.messageAdded':
args = [params?.message];
break;
case 'Runtime.executionContextCreated':
args = [params?.context];
break;
default:
// pass
break;
}
if (method && _.startsWith(method, 'Network.')) {
// aggregate Network events, and add original method name to the arguments
eventNames.push('NetworkEvent');
args.push(method);
}
if (method && _.startsWith(method, 'Console.')) {
// aggregate Console events, and add original method name to the arguments
eventNames.push('ConsoleEvent');
args.push(method);
}
for (const name of eventNames) {
this.emit(name, error, ...args);
}
}
/**
* Handles a data message from the Web Inspector by parsing it and
* dispatching appropriate events based on the message type.
*
* @param plist - The plist message from the Web Inspector.
*/
private async handleDataMessage(plist: StringRecord): Promise<void> {
const dataKey = this.parseDataKey(plist);
let msgId = (dataKey.id || '').toString();
let result = dataKey.result;
let method = dataKey.method;
let params = dataKey.params;
const parseError = (): Error | undefined => {
const defaultMessage = 'Error occurred in handling data message';
if (result?.wasThrown) {
const message =
result?.result?.value || result?.result?.description
? result?.result?.value || result?.result?.description
: (dataKey.error ?? defaultMessage);
return new Error(message);
}
if (dataKey.error) {
if (_.isPlainObject(dataKey.error)) {
const dataKeyError = dataKey.error as DataErrorMessage;
const error = new Error(defaultMessage);
for (const key of Object.keys(dataKeyError)) {
(error as any)[key] = dataKeyError[key as keyof DataErrorMessage];
}
return error;
}
return new Error(String(dataKey.error || defaultMessage));
}
return undefined;
};
switch (method) {
case 'Target.targetCreated':
case 'Target.targetDestroyed':
case 'Target.didCommitProvisionalTarget': {
const app = plist.__argument.WIRApplicationIdentifierKey;
const args =
method === 'Target.didCommitProvisionalTarget'
? params
: (params.targetInfo ?? {targetId: params.targetId});
this.emit(method, null, app, args);
return;
}
case 'Target.dispatchMessageFromTarget': {
if (!dataKey.error) {
try {
const message = JSON.parse(dataKey.params.message);
msgId = _.isUndefined(message.id) ? '' : String(message.id);
method = message.method;
result = message.result || message;
params = result.params;
} catch (err: any) {
// if this happens then some aspect of the protocol is missing to us
// so print the entire message to get visibility into what is going on
log.error(
`Unexpected message format from Web Inspector: ${util.jsonStringify(plist, null)}`,
);
throw err;
}
}
await this.dispatchDataMessage(msgId, method, params, result, parseError());
return;
}
default: {
await this.dispatchDataMessage(msgId, method, params, result, parseError());
}
}
}
}