diff --git a/lib/remote-debugger-real-device.js b/lib/remote-debugger-real-device.js index 0fb018e..0914ca9 100644 --- a/lib/remote-debugger-real-device.js +++ b/lib/remote-debugger-real-device.js @@ -29,15 +29,11 @@ export default class RemoteDebuggerRealDevice extends RemoteDebugger { bundleId: this.bundleId, platformVersion: this.platformVersion, isSafari: this.isSafari, - host: this.host, - port: this.port, - socketPath: this.socketPath, - messageProxy: this.remoteDebugProxy, logAllCommunication: this.logAllCommunication, logAllCommunicationHexDump: this.logAllCommunicationHexDump, socketChunkSize: this.socketChunkSize, webInspectorMaxFrameLength: this.webInspectorMaxFrameLength, - udid: this.udid + udid: this.udid, }); } } diff --git a/lib/rpc/remote-messages.js b/lib/rpc/remote-messages.js index 37d9417..ef3355a 100644 --- a/lib/rpc/remote-messages.js +++ b/lib/rpc/remote-messages.js @@ -24,7 +24,7 @@ const COMMANDS = { 'Timeline.stop': FULL_COMMAND, }; -class RemoteMessages { +export class RemoteMessages { constructor (isTargetBased = false) { this.isTargetBased = isTargetBased; } @@ -249,5 +249,4 @@ class RemoteMessages { } } -export { RemoteMessages }; export default RemoteMessages; diff --git a/lib/rpc/rpc-client-real-device.js b/lib/rpc/rpc-client-real-device.js index 694dbe3..235feaf 100644 --- a/lib/rpc/rpc-client-real-device.js +++ b/lib/rpc/rpc-client-real-device.js @@ -3,13 +3,19 @@ import RpcClient from './rpc-client'; import { services } from 'appium-ios-device'; -export default class RpcClientRealDevice extends RpcClient { +export class RpcClientRealDevice extends RpcClient { + /** + * @param {import('./rpc-client').RpcClientOptions} [opts={}] + */ constructor (opts = {}) { super(Object.assign({ shouldCheckForTarget: false, }, opts)); } + /** + * @override + */ async connect () { this.service = await services.startWebInspectorService(this.udid, { osVersion: this.platformVersion, @@ -24,6 +30,9 @@ export default class RpcClientRealDevice extends RpcClient { this.isConnected = true; } + /** + * @override + */ async disconnect () { if (!this.isConnected) { return; @@ -35,10 +44,16 @@ export default class RpcClientRealDevice extends RpcClient { this.isConnected = false; } + /** + * @override + */ async sendMessage (cmd) { // eslint-disable-line require-await this.service.sendMessage(cmd); } + /** + * @override + */ async receive (data) { if (!this.isConnected) { return; @@ -47,3 +62,5 @@ export default class RpcClientRealDevice extends RpcClient { await this.messageHandler.handleMessage(data); } } + +export default RpcClientRealDevice; diff --git a/lib/rpc/rpc-client-simulator.js b/lib/rpc/rpc-client-simulator.js index 2689367..3ae8b23 100644 --- a/lib/rpc/rpc-client-simulator.js +++ b/lib/rpc/rpc-client-simulator.js @@ -5,8 +5,25 @@ import net from 'net'; import RpcClient from './rpc-client'; import { services } from 'appium-ios-device'; +export class RpcClientSimulator extends RpcClient { + /** @type {string|undefined} */ + host; -export default class RpcClientSimulator extends RpcClient { + /** @type {number|undefined} */ + port; + + /** @type {any} */ + messageProxy; + + /** @type {import('node:net').Socket|null} */ + socket; + + /** @type {string|undefined} */ + socketPath; + + /** + * @param {import('./rpc-client').RpcClientOptions & RpcClientSimulatorOptions} [opts={}] + */ constructor (opts = {}) { super(Object.assign({ shouldCheckForTarget: false, @@ -28,6 +45,9 @@ export default class RpcClientSimulator extends RpcClient { this.socketPath = socketPath; } + /** + * @override + */ async connect () { // create socket and handle its messages if (this.socketPath) { @@ -59,7 +79,7 @@ export default class RpcClientSimulator extends RpcClient { // tcp socket log.debug(`Connecting to remote debugger ${this.messageProxy ? 'via proxy ' : ''}through TCP: ${this.host}:${this.port}`); this.socket = new net.Socket(); - this.socket.connect(this.port, this.host); + this.socket.connect(/** @type {number} */ (this.port), /** @type {String} */ (this.host)); } this.socket.setNoDelay(true); @@ -107,6 +127,9 @@ export default class RpcClientSimulator extends RpcClient { }); } + /** + * @override + */ async disconnect () { if (!this.isConnected) { return; @@ -118,6 +141,9 @@ export default class RpcClientSimulator extends RpcClient { this.isConnected = false; } + /** + * @override + */ async sendMessage (cmd) { let onSocketError; @@ -148,6 +174,9 @@ export default class RpcClientSimulator extends RpcClient { }); } + /** + * @override + */ async receive (data) { if (!this.isConnected) { return; @@ -166,3 +195,13 @@ export default class RpcClientSimulator extends RpcClient { await this.messageHandler.handleMessage(data); } } + +export default RpcClientSimulator; + +/** + * @typedef {Object} RpcClientSimulatorOptions + * @property {string} [socketPath] + * @property {string} [host='::1'] + * @property {number} [port] + * @property {any} [messageProxy] + */ diff --git a/lib/rpc/rpc-client.js b/lib/rpc/rpc-client.js index 6893683..1306ffb 100644 --- a/lib/rpc/rpc-client.js +++ b/lib/rpc/rpc-client.js @@ -8,17 +8,18 @@ import { util, timing } from '@appium/support'; import { EventEmitter } from 'node:events'; import { ON_TARGET_PROVISIONED_EVENT } from './constants'; - const DATA_LOG_LENGTH = {length: 200}; - -const WAIT_FOR_TARGET_TIMEOUT = 10000; -const WAIT_FOR_TARGET_INTERVAL = 1000; - +const WAIT_FOR_TARGET_TIMEOUT_MS = 10000; +const WAIT_FOR_TARGET_INTERVAL_MS = 1000; const MIN_PLATFORM_FOR_TARGET_BASED = '12.2'; - // `Target.exists` protocol method was removed from WebKit in 13.4 const MIN_PLATFORM_NO_TARGET_EXISTS = '13.4'; +/** + * @param {boolean} isSafari + * @param {string} platformVersion + * @returns {boolean} + */ function isTargetBased (isSafari, platformVersion) { // On iOS 12.2 the messages get sent through the Target domain // On iOS 13.0+, WKWebView also needs to follow the Target domain, @@ -39,13 +40,62 @@ export class RpcClient { /** @type {boolean} */ connected; - constructor (opts = {}) { - this._targets = []; - this._shouldCheckForTarget = !!opts.shouldCheckForTarget; + /** @type {boolean} */ + isSafari; + + /** @type {string} */ + connId; + + /** @type {string} */ + senderId; + + /** @type {number} */ + msgId; + + /** @type {string|undefined} */ + udid; + + /** @type {boolean|undefined} */ + logAllCommunication; + + /** @type {boolean|undefined} */ + logAllCommunicationHexDump; + + /** @type {number|undefined} */ + socketChunkSize; + + /** @type {number|undefined} */ + webInspectorMaxFrameLength; + + /** @type {boolean|undefined} */ + fullPageInitialization; + + /** @type {string|undefined} */ + bundleId; + /** @type {string} */ + platformVersion; + + /** @type {string[]} */ + _contexts; + + /** @type {import('@appium/types').StringRecord} */ + _targets; + + /** @type {EventEmitter} */ + _targetSubscriptions; + + /** @type {boolean} */ + _shouldCheckForTarget; + + /** + * + * @param {RpcClientOptions} [opts={}] + */ + constructor (opts = {}) { const { bundleId, - platformVersion = {}, + platformVersion = '', isSafari = true, logAllCommunication = false, logAllCommunicationHexDump = false, @@ -79,21 +129,34 @@ export class RpcClient { this._targetSubscriptions = new EventEmitter(); // start with a best guess for the protocol - this.isTargetBased = isTargetBased(isSafari, this.platformVersion); + this._shouldCheckForTarget = !!opts.shouldCheckForTarget; + this.isTargetBased = platformVersion ? isTargetBased(isSafari, platformVersion) : true; } + /** + * @returns {string[]} + */ get contexts () { return this._contexts; } + /** + * @returns {boolean} + */ get needsTarget () { return this.shouldCheckForTarget && this.isTargetBased; } + /** + * @returns {import('@appium/types').StringRecord} + */ get targets () { return this._targets; } + /** + * @returns {boolean} + */ get shouldCheckForTarget () { return this._shouldCheckForTarget; } @@ -102,36 +165,65 @@ export class RpcClient { this._shouldCheckForTarget = !!shouldCheckForTarget; } + /** + * @returns {boolean} + */ get isConnected () { return this.connected; } + /** + * @param {boolean} connected + */ set isConnected (connected) { this.connected = !!connected; } + /** + * @returns {EventEmitter} + */ get targetSubscriptions() { return this._targetSubscriptions; } + /** + * + * @param {string} event + * @param {Function} listener + * @returns {this} + */ on (event, listener) { // @ts-ignore messageHandler must be defined here this.messageHandler.on(event, listener); return this; } + /** + * + * @param {string} event + * @param {Function} listener + * @returns {this} + */ once (event, listener) { // @ts-ignore messageHandler must be defined here this.messageHandler.once(event, listener); return this; } + /** + * @param {string} event + * @param {Function} listener + * @returns {this} + */ off (event, listener) { // @ts-ignore messageHandler must be defined here this.messageHandler.off(event, listener); return this; } + /** + * @param {boolean} isTargetBased + */ set isTargetBased (isTargetBased) { log.warn(`Setting communication protocol: using ${isTargetBased ? 'Target-based' : 'full Web Inspector protocol'} communication`); this._isTargetBased = isTargetBased; @@ -156,8 +248,11 @@ export class RpcClient { } } + /** + * @returns {boolean} + */ get isTargetBased () { - return this._isTargetBased; + return !!this._isTargetBased; } /** @@ -179,8 +274,8 @@ export class RpcClient { // otherwise waiting is necessary to see what the target is try { await waitForCondition(() => !_.isEmpty(this.getTarget(appIdKey, pageIdKey)), { - waitMs: WAIT_FOR_TARGET_TIMEOUT, - intervalMs: WAIT_FOR_TARGET_INTERVAL, + waitMs: WAIT_FOR_TARGET_TIMEOUT_MS, + intervalMs: WAIT_FOR_TARGET_INTERVAL_MS, error: 'No targets found, unable to communicate with device', }); } catch (err) { @@ -348,12 +443,21 @@ export class RpcClient { this.messageHandler?.removeAllListeners(); } + /** + * @param {string} command + * @returns {Promise} + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars async sendMessage (command) { // eslint-disable-line require-await throw new Error(`Sub-classes need to implement a 'sendMessage' function`); } - async receive (/* data */) { // eslint-disable-line require-await + /** + * @param {any} data + * @returns {Promise} + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async receive (data) { // eslint-disable-line require-await throw new Error(`Sub-classes need to implement a 'receive' function`); } @@ -681,6 +785,9 @@ export class RpcClient { this.contexts.push(context.id); } + /** + * @returns {void} + */ onGarbageCollected () { // just want to log that this is happening, as it can affect opertion log.debug(`Web Inspector garbage collected`); @@ -698,3 +805,17 @@ export class RpcClient { } export default RpcClient; + +/** + * @typedef {Object} RpcClientOptions + * @property {string} [bundleId] + * @property {string} [platformVersion=''] + * @property {boolean} [isSafari=true] + * @property {boolean} [logAllCommunication=false] + * @property {boolean} [logAllCommunicationHexDump=false] + * @property {number} [webInspectorMaxFrameLength] + * @property {number} [socketChunkSize] + * @property {boolean} [fullPageInitialization=false] + * @property {string} [udid] + * @property {boolean} [shouldCheckForTarget] + */