diff --git a/.travis.yml b/.travis.yml index c3a08f49..d15d8721 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,17 +5,6 @@ env: - _FORCE_LOGS=1 matrix: include: - - osx_image: xcode8.3 - node_js: "8" - env: COVERALLS=1 - - osx_image: xcode8.3 - node_js: "10" - - - osx_image: xcode9.4 - node_js: "8" - - osx_image: xcode9.4 - node_js: "10" - - osx_image: xcode10 node_js: "8" - osx_image: xcode10 diff --git a/lib/atoms.js b/lib/atoms.js index dfdc9d37..574b2c83 100644 --- a/lib/atoms.js +++ b/lib/atoms.js @@ -5,7 +5,7 @@ import path from 'path'; const atomsCache = {}; async function getAtoms (atomName) { - let atomFileName = __filename.indexOf('build/lib/atoms') !== -1 ? + const atomFileName = __filename.includes('build/lib/atoms') ? path.resolve(__dirname, '..', '..', 'atoms', `${atomName}.js`) : path.resolve(__dirname, '..', 'atoms', `${atomName}.js`); diff --git a/lib/helpers.js b/lib/helpers.js index a9bb0a26..ed5bf183 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -2,7 +2,7 @@ import log from './logger'; import getAtom from './atoms'; import _ from 'lodash'; import assert from 'assert'; -import Promise from 'bluebird'; +import B from 'bluebird'; const WEB_CONTENT_BUNDLE_ID = 'com.apple.WebKit.WebContent'; @@ -12,11 +12,11 @@ const WEB_CONTENT_BUNDLE_ID = 'com.apple.WebKit.WebContent'; * dictionary whose keys are understandable */ function appInfoFromDict (dict) { - let id = dict.WIRApplicationIdentifierKey; - let isProxy = _.isString(dict.WIRIsApplicationProxyKey) + const id = dict.WIRApplicationIdentifierKey; + const isProxy = _.isString(dict.WIRIsApplicationProxyKey) ? dict.WIRIsApplicationProxyKey.toLowerCase() === 'true' : dict.WIRIsApplicationProxyKey; - let entry = { + const entry = { id, isProxy, name: dict.WIRApplicationNameKey, @@ -39,7 +39,7 @@ function pageArrayFromDict (pageDict) { return [pageDict]; } let newPageArray = []; - for (let dict of _.values(pageDict)) { + for (const dict of _.values(pageDict)) { // count only WIRTypeWeb pages and ignore all others (WIRTypeJavaScript etc) if (_.isUndefined(dict.WIRTypeKey) || dict.WIRTypeKey === 'WIRTypeWeb') { newPageArray.push({ @@ -60,7 +60,7 @@ function pageArrayFromDict (pageDict) { function getDebuggerAppKey (bundleId, platformVersion, appDict) { let appId; if (parseFloat(platformVersion) >= 8) { - for (let [key, data] of _.toPairs(appDict)) { + for (const [key, data] of _.toPairs(appDict)) { if (data.bundleId === bundleId) { appId = key; break; @@ -70,7 +70,7 @@ function getDebuggerAppKey (bundleId, platformVersion, appDict) { if (appId) { log.debug(`Found app id key '${appId}' for bundle '${bundleId}'`); let proxiedAppIds = []; - for (let [key, data] of _.toPairs(appDict)) { + for (const [key, data] of _.toPairs(appDict)) { if (data.isProxy && data.hostId === appId) { log.debug(`Found separate bundleId '${data.bundleId}' ` + `acting as proxy for '${bundleId}', with app id '${key}'`); @@ -94,7 +94,7 @@ function getDebuggerAppKey (bundleId, platformVersion, appDict) { function appIdForBundle (bundleId, appDict) { let appId; - for (let [key, data] of _.toPairs(appDict)) { + for (const [key, data] of _.toPairs(appDict)) { if (data.bundleId === bundleId) { appId = key; break; @@ -112,21 +112,19 @@ function appIdForBundle (bundleId, appDict) { function getPossibleDebuggerAppKeys (bundleId, platformVersion, appDict) { let proxiedAppIds = []; if (parseFloat(platformVersion) >= 8) { - let appId = appIdForBundle(bundleId, appDict); + const appId = appIdForBundle(bundleId, appDict); // now we need to determine if we should pick a proxy for this instead if (appId) { + proxiedAppIds.push(appId); log.debug(`Found app id key '${appId}' for bundle '${bundleId}'`); - for (let [key, data] of _.toPairs(appDict)) { + for (const [key, data] of _.toPairs(appDict)) { if (data.isProxy && data.hostId === appId) { log.debug(`Found separate bundleId '${data.bundleId}' ` + `acting as proxy for '${bundleId}', with app id '${key}'`); proxiedAppIds.push(key); } } - if (proxiedAppIds.length === 0) { - proxiedAppIds = [appId]; - } } } else { if (_.has(appDict, bundleId)) { @@ -139,7 +137,7 @@ function getPossibleDebuggerAppKeys (bundleId, platformVersion, appDict) { function checkParams (params) { let errors = []; - for (let [param, value] of _.toPairs(params)) { + for (const [param, value] of _.toPairs(params)) { try { assert.ok(value); } catch (err) { @@ -163,7 +161,7 @@ async function getScriptForAtom (atom, args, frames, asyncCallBack = null) { let script; if (frames.length > 0) { script = atomSrc; - for (let frame of frames) { + for (const frame of frames) { script = await wrapScriptForFrame(script, frame); } } else { @@ -190,7 +188,7 @@ function simpleStringify (value) { // we get back objects sometimes with string versions of functions // which muddy the logs let cleanValue = _.clone(value); - for (let property of ['ceil', 'clone', 'floor', 'round', 'scale', 'toString']) { + for (const property of ['ceil', 'clone', 'floor', 'round', 'scale', 'toString']) { delete cleanValue[property]; } return JSON.stringify(cleanValue); @@ -200,7 +198,7 @@ function deferredPromise () { // http://bluebirdjs.com/docs/api/deferred-migration.html let resolve; let reject; - let promise = new Promise((res, rej) => { // eslint-disable-line promise/param-names + const promise = new B((res, rej) => { // eslint-disable-line promise/param-names resolve = res; reject = rej; }); diff --git a/lib/message-handlers.js b/lib/message-handlers.js index 10cbb137..499b0dae 100644 --- a/lib/message-handlers.js +++ b/lib/message-handlers.js @@ -1,21 +1,43 @@ import log from './logger'; import { RemoteDebugger } from './remote-debugger'; import { pageArrayFromDict, getDebuggerAppKey, simpleStringify } from './helpers'; +import _ from 'lodash'; + /* * Generic callbacks used throughout the lifecycle of the Remote Debugger. * These will be added to the prototype. */ + +/** + * Remove the `isKey` property from the page array, since it does not affect + * equality + */ +function cleanPageArray (arr) { + return _.map(arr, (el) => _.pick(el, 'id', 'title', 'url')); +} + function onPageChange (appIdKey, pageDict) { + const pageArray = pageArrayFromDict(pageDict); + // save the page dict for this app if (this.appDict[appIdKey]) { - if (this.appDict[appIdKey].pageDict && this.appDict[appIdKey].pageDict.resolve) { - // pageDict is a promise, so resolve - this.appDict[appIdKey].pageDict.resolve(pageDict); + if (this.appDict[appIdKey].pageArray) { + if (this.appDict[appIdKey].pageArray.resolve) { + // pageDict is a pending promise, so resolve + this.appDict[appIdKey].pageArray.resolve(); + } else { + // we have a pre-existing pageDict + if (_.isEqual(cleanPageArray(this.appDict[appIdKey].pageArray), cleanPageArray(pageArray))) { + log.debug(`Received page change notice for app '${appIdKey}' ` + + `but the listing has not changed. Ignoring.`); + return; + } + } } // keep track of the page dictionary - this.appDict[appIdKey].pageDict = pageArrayFromDict(pageDict); + this.appDict[appIdKey].pageArray = pageArray; } // only act if this is the correct app @@ -25,10 +47,12 @@ function onPageChange (appIdKey, pageDict) { return; } + + log.debug(`Page changed: ${simpleStringify(pageDict)}`); this.emit(RemoteDebugger.EVENT_PAGE_CHANGE, { appIdKey: appIdKey.replace('PID:', ''), - pageArray: pageArrayFromDict(pageDict) + pageArray, }); } @@ -64,7 +88,7 @@ function onAppDisconnect (dict) { function onAppUpdate (dict) { let appIdKey = dict.WIRApplicationIdentifierKey; - log.debug(`Notified that application '${appIdKey}' has been updated.`); + log.debug(`Notified that application '${appIdKey}' has been updated`); this.updateAppsWithDict(dict); } @@ -73,12 +97,22 @@ function onReportDriverList (dict) { log.debug(`Notified of connected drivers: ${JSON.stringify(dict.WIRDriverDictionaryKey)}.`); } +function onTargetCreated (app, targetInfo) { + log.debug(`Target created: ${app} ${JSON.stringify(targetInfo)}`); +} + +function onTargetDestroyed (app, targetInfo) { + log.debug(`Target destroyed: ${app} ${JSON.stringify(targetInfo)}`); +} + const messageHandlers = { onPageChange, onAppConnect, onAppDisconnect, onAppUpdate, onReportDriverList, + onTargetCreated, + onTargetDestroyed, }; export default messageHandlers; diff --git a/lib/remote-debugger-message-handler.js b/lib/remote-debugger-message-handler.js index 00c64a77..79dc82a6 100644 --- a/lib/remote-debugger-message-handler.js +++ b/lib/remote-debugger-message-handler.js @@ -2,13 +2,26 @@ import log from './logger'; import _ from 'lodash'; +// we will receive events that we do not listen to. +// if we start to listen to one of these, remove it from the list +const IGNORED_EVENTS = [ + 'Page.domContentEventFired', + 'Page.frameStartedLoading', + 'Page.frameStoppedLoading', + 'Page.frameScheduledNavigation', + 'Page.frameClearedScheduledNavigation', + 'Console.messagesCleared', +]; + export default class RpcMessageHandler { - constructor (specialHandlers) { + constructor (specialHandlers, isTargetBased = false) { this.setHandlers(); this.errorHandlers = {}; this.specialHandlers = _.clone(specialHandlers); this.dataHandlers = {}; this.willNavigateWithoutReload = false; + + this.isTargetBased = isTargetBased; } setDataMessageHandler (key, errorHandler, handler) { @@ -49,22 +62,22 @@ export default class RpcMessageHandler { this.willNavigateWithoutReload = allow; } - handleMessage (plist) { - let handlerFor = plist.__selector; - if (!handlerFor) { + async handleMessage (plist) { + const selector = plist.__selector; + if (!selector) { log.debug('Got an invalid plist'); return; } - if (_.has(this.handlers, handlerFor)) { - this.handlers[handlerFor](plist); + if (_.has(this.handlers, selector)) { + await this.handlers[selector](plist); } else { - log.debug(`Debugger got a message for '${handlerFor}' and have no ` + + log.debug(`Debugger got a message for '${selector}' and have no ` + `handler, doing nothing.`); } } - handleSpecialMessage (handler, ...args) { + async handleSpecialMessage (handler, ...args) { const fn = this.specialHandlers[handler]; if (fn) { @@ -78,7 +91,7 @@ export default class RpcMessageHandler { handler !== '_rpc_reportConnectedDriverList:') { this.specialHandlers[handler] = null; } - fn(...args); + await fn(...args); } else { log.warn(`Tried to access special message handler '${handler}' ` + `but none was found`); @@ -94,40 +107,15 @@ export default class RpcMessageHandler { } } - async handleDataMessage (plist) { - const dataKey = this.parseDataKey(plist); - const msgId = (dataKey.id || '').toString(); - let result = dataKey.result; - let error = dataKey.error || null; - - // we can get an error, or we can get a response that is an error - if (result && result.wasThrown) { - let message = (result.result && (result.result.value || result.result.description)) - ? (result.result.value || result.result.description) - : 'Error occurred in handling data message'; - error = new Error(message); - } - - if (error) { - if (this.hasErrorHandler(msgId)) { - this.errorHandlers[msgId](error); - } else { - log.error(`Error occurred in handling data message: ${error}`); - log.error('No error handler present, ignoring'); - } - - // short circuit - return; - } - - if (dataKey.method === 'Profiler.resetProfiles') { + async dispatchDataMessage (msgId, method, params, result, error) { + if (method === 'Profiler.resetProfiles') { log.debug('Device is telling us to reset profiles. Should probably ' + 'do some kind of callback here'); - } else if (dataKey.method === 'Page.frameNavigated') { + } else if (method === 'Page.frameNavigated') { if (!this.willNavigateWithoutReload && !this.pageLoading) { log.debug('Frame navigated, unloading page'); if (_.isFunction(this.specialHandlers['Page.frameNavigated'])) { - this.specialHandlers['Page.frameNavigated']('remote-debugger'); + await this.specialHandlers['Page.frameNavigated']('remote-debugger'); this.specialHandlers['Page.frameNavigated'] = null; } } else { @@ -135,18 +123,21 @@ export default class RpcMessageHandler { 'considering page state unloaded'); this.willNavigateWithoutReload = false; } - } else if (dataKey.method === 'Page.loadEventFired' && _.isFunction(this.specialHandlers.pageLoad)) { + } else if (IGNORED_EVENTS.includes(method)) { + // pass + } else if (method === 'Page.loadEventFired' && _.isFunction(this.specialHandlers.pageLoad)) { await this.specialHandlers.pageLoad(); - } else if (dataKey.method === 'Page.frameDetached' && _.isFunction(this.specialHandlers.frameDetached)) { + } else if (method === 'Page.frameDetached' && _.isFunction(this.specialHandlers.frameDetached)) { await this.specialHandlers.frameDetached(); - } else if (dataKey.method === 'Timeline.eventRecorded' && _.isFunction(this.timelineEventHandler)) { - this.timelineEventHandler(dataKey.params.record); - } else if (dataKey.method === 'Console.messageAdded' && _.isFunction(this.consoleLogEventHandler)) { - this.consoleLogEventHandler(dataKey.params.message); - } else if (dataKey.method && dataKey.method.startsWith('Network.') && _.isFunction(this.networkLogEventHandler)) { - this.networkLogEventHandler(dataKey.method, dataKey.params); + } else if (method === 'Timeline.eventRecorded' && _.isFunction(this.timelineEventHandler)) { + this.timelineEventHandler(params || params.record); + } else if (method === 'Console.messageAdded' && _.isFunction(this.consoleLogEventHandler)) { + this.consoleLogEventHandler(params.message); + } else if (method && method.startsWith('Network.') && _.isFunction(this.networkLogEventHandler)) { + this.networkLogEventHandler(method, params); } else if (_.isFunction(this.dataHandlers[msgId])) { log.debug('Found data handler for response'); + // we will either get back a result object that has a result.value // in which case that is what we want, // or else we return the whole thing @@ -168,38 +159,126 @@ export default class RpcMessageHandler { } } + logFullMessage (plist) { + // Buffers cannot be serialized in a readable way + const bufferToJSON = Buffer.prototype.toJSON; + delete Buffer.prototype.toJSON; + try { + log(JSON.stringify(plist, (k, v) => Buffer.isBuffer(v) ? v.toString('utf8') : v, 2)); + } finally { + // restore the function, so as to not break further serialization + Buffer.prototype.toJSON = bufferToJSON; + } + } + + async handleDataMessage (plist) { + const dataKey = this.parseDataKey(plist); + let msgId = (dataKey.id || '').toString(); + let result = dataKey.result; + + // we can get an error, or we can get a response that is an error + let error = dataKey.error || null; + if (result && result.wasThrown) { + let message = (result.result && (result.result.value || result.result.description)) + ? (result.result.value || result.result.description) + : 'Error occurred in handling data message'; + error = new Error(message); + } + + if (error) { + if (this.hasErrorHandler(msgId)) { + this.errorHandlers[msgId](error); + } else { + log.error(`Error occurred in handling data message: ${error}`); + log.error('No error handler present, ignoring'); + } + + // short circuit + return; + } + + let method = dataKey.method; + let params; + if (this.isTargetBased) { + if (method === 'Target.targetCreated') { + // this is in response to a `_rpc_forwardSocketSetup:` call + // targetInfo: { targetId: 'page-1', type: 'page' } + const app = plist.__argument.WIRApplicationIdentifierKey; + const targetInfo = dataKey.params.targetInfo; + await this.specialHandlers.targetCreated(app, targetInfo); + return; + } if (method === 'Target.targetDestroyed') { + const app = plist.__argument.WIRApplicationIdentifierKey; + const targetInfo = dataKey.params.targetInfo; + await this.specialHandlers.targetDestroyed(app, targetInfo); + return; + } else if (dataKey.method !== 'Target.dispatchMessageFromTarget') { + // this sort of message, at this point, is just an acknowledgement + // that the original message was received + if (!_.isEmpty(msgId)) { + log.debug(`Received receipt for message '${msgId}'`); + } + return; + } + + // at this point, we have a Target-based message wrapping a protocol message + let message; + try { + message = JSON.parse(dataKey.params.message); + msgId = message.id; + method = message.method; + result = message.result || message; + params = result.params; + } catch (err) { + // if this happens then some aspect of the protocol is missing to us + // so print the entire message to get visibiity into what is going on + log.error(`Unexpected message format from Web Inspector:`); + this.logFullMessage(plist); + throw err; + } + } else { + params = dataKey.params; + } + + if (!_.isEmpty(msgId)) { + log.debug(`Received response for message '${msgId}'`); + } + + await this.dispatchDataMessage(msgId, method, params, result, error); + } + setHandlers () { this.handlers = { - '_rpc_reportSetup:': (plist) => { - this.handleSpecialMessage('_rpc_reportIdentifier:', - plist.__argument.WIRSimulatorNameKey, - plist.__argument.WIRSimulatorBuildKey, - plist.__argument.WIRSimulatorProductVersionKey); + '_rpc_reportSetup:': async (plist) => { + await this.handleSpecialMessage('_rpc_reportIdentifier:', + plist.__argument.WIRSimulatorNameKey, + plist.__argument.WIRSimulatorBuildKey, + plist.__argument.WIRSimulatorProductVersionKey); }, - '_rpc_reportConnectedApplicationList:': (plist) => { - this.handleSpecialMessage('_rpc_reportConnectedApplicationList:', - plist.__argument.WIRApplicationDictionaryKey); + '_rpc_reportConnectedApplicationList:': async (plist) => { + await this.handleSpecialMessage('_rpc_reportConnectedApplicationList:', + plist.__argument.WIRApplicationDictionaryKey); }, - '_rpc_applicationSentListing:': (plist) => { - this.handleSpecialMessage('_rpc_forwardGetListing:', - plist.__argument.WIRApplicationIdentifierKey, - plist.__argument.WIRListingKey); + '_rpc_applicationSentListing:': async (plist) => { + await this.handleSpecialMessage('_rpc_forwardGetListing:', + plist.__argument.WIRApplicationIdentifierKey, + plist.__argument.WIRListingKey); }, - '_rpc_applicationConnected:': (plist) => { - this.handleSpecialMessage('_rpc_applicationConnected:', - plist.__argument); + '_rpc_applicationConnected:': async (plist) => { + await this.handleSpecialMessage('_rpc_applicationConnected:', + plist.__argument); }, - '_rpc_applicationDisconnected:': (plist) => { - this.handleSpecialMessage('_rpc_applicationDisconnected:', - plist.__argument); + '_rpc_applicationDisconnected:': async (plist) => { + await this.handleSpecialMessage('_rpc_applicationDisconnected:', + plist.__argument); }, - '_rpc_applicationUpdated:': (plist) => { - this.handleSpecialMessage('_rpc_applicationUpdated:', - plist.__argument); + '_rpc_applicationUpdated:': async (plist) => { + await this.handleSpecialMessage('_rpc_applicationUpdated:', + plist.__argument); }, - '_rpc_reportConnectedDriverList:': (plist) => { - this.handleSpecialMessage('_rpc_reportConnectedDriverList:', - plist.__argument); + '_rpc_reportConnectedDriverList:': async (plist) => { + await this.handleSpecialMessage('_rpc_reportConnectedDriverList:', + plist.__argument); }, '_rpc_applicationSentData:': this.handleDataMessage.bind(this), }; diff --git a/lib/remote-debugger-rpc-client.js b/lib/remote-debugger-rpc-client.js index 9872e05a..8c882337 100644 --- a/lib/remote-debugger-rpc-client.js +++ b/lib/remote-debugger-rpc-client.js @@ -3,17 +3,20 @@ import _ from 'lodash'; import bplistCreate from 'bplist-creator'; import bplistParser from 'bplist-parser'; import bufferpack from 'bufferpack'; -import Promise from 'bluebird'; +import B from 'bluebird'; import { REMOTE_DEBUGGER_PORT } from './remote-debugger'; import UUID from 'uuid-js'; import net from 'net'; import RpcMessageHandler from './remote-debugger-message-handler'; -import getRemoteCommand from './remote-messages'; +import RemoteMessages from './remote-messages'; +const MIN_PLATFORM_FOR_TARGET_BASED = 12.2; + export default class RemoteDebuggerRpcClient { constructor (opts = {}) { - const { + let { + platformVersion = {}, host = '::1', port = REMOTE_DEBUGGER_PORT, socketPath, @@ -31,18 +34,24 @@ export default class RemoteDebuggerRpcClient { this.connected = false; this.connId = UUID.create().toString(); this.senderId = UUID.create().toString(); - this.curMsgId = 0; + this.msgId = 0; this.received = Buffer.alloc(0); this.readPos = 0; // message handlers this.specialMessageHandlers = specialMessageHandlers; - this.messageHandler = null; + + // on iOS 12.2 the messages get sent through the Target domain + if (_.isString(platformVersion)) { + platformVersion = parseFloat(platformVersion); + } + const isTargetBased = platformVersion >= MIN_PLATFORM_FOR_TARGET_BASED; + this.remoteMessages = new RemoteMessages(isTargetBased); + this.messageHandler = new RpcMessageHandler(this.specialMessageHandlers, isTargetBased); + log.debug(`Using '${isTargetBased ? 'Target-based' : 'full Web Inspector protocol'}' communication`); } async connect () { - this.messageHandler = new RpcMessageHandler(this.specialMessageHandlers); - // create socket and handle its messages if (this.socketPath) { if (this.messageProxy) { @@ -87,7 +96,7 @@ export default class RemoteDebuggerRpcClient { this.socket.on('data', this.receive.bind(this)); // connect the socket - return await new Promise((resolve, reject) => { + return await new B((resolve, reject) => { // only resolve this function when we are actually connected this.socket.on('connect', () => { log.debug(`Debugger socket connected`); @@ -136,7 +145,7 @@ export default class RemoteDebuggerRpcClient { } async selectApp (appIdKey, applicationConnectedHandler) { - return await new Promise((resolve, reject) => { + return await new B((resolve, reject) => { // local callback, temporarily added as callback to // `_rpc_applicationConnected:` remote debugger response // to handle the initial connection @@ -179,17 +188,20 @@ export default class RemoteDebuggerRpcClient { }); } - async send (command, opts = {}) { // eslint-disable-line require-await + async send (command, opts = {}) { // error listener, which needs to be removed after the promise is resolved let onSocketError; - return new Promise((resolve, reject) => { + return await new B((resolve, reject) => { // promise to be resolved whenever remote debugger // replies to our request + // keep track of the messages coming and going using a simple sequential id + const msgId = this.msgId++; + // retrieve the correct command to send opts = _.defaults({connId: this.connId, senderId: this.senderId}, opts); - let data = getRemoteCommand(command, opts); + const cmd = this.remoteMessages.getRemoteCommand(command, opts); // most of the time we don't care when socket.write does // so give it an empty function @@ -205,53 +217,62 @@ export default class RemoteDebuggerRpcClient { reject(exception); }; this.socket.on('error', onSocketError); - if (this.messageHandler.hasSpecialMessageHandler(data.__selector)) { + + if (this.messageHandler.hasSpecialMessageHandler(cmd.__selector)) { // special replies will return any number of arguments // temporarily wrap with promise handling - let specialMessageHandler = this.getSpecialMessageHandler(data.__selector); - this.setSpecialMessageHandler(data.__selector, reject, function (...args) { + const specialMessageHandler = this.getSpecialMessageHandler(cmd.__selector); + this.setSpecialMessageHandler(cmd.__selector, reject, function (...args) { log.debug(`Received response from socket send: '${_.truncate(JSON.stringify(args), {length: 50})}'`); // call the original listener, and put it back, if necessary specialMessageHandler(...args); - if (this.messageHandler.hasSpecialMessageHandler(data.__selector)) { + if (this.messageHandler.hasSpecialMessageHandler(cmd.__selector)) { // this means that the system has not removed this listener - this.setSpecialMessageHandler(data.__selector, null, specialMessageHandler); + this.setSpecialMessageHandler(cmd.__selector, null, specialMessageHandler); } resolve(args); }.bind(this)); - } else if (data.__argument && data.__argument.WIRSocketDataKey) { - // keep track of the messages coming and going using - // a simple sequential id - this.curMsgId++; - + } else if (cmd.__argument && cmd.__argument.WIRSocketDataKey) { const errorHandler = function (err) { const msg = `Remote debugger error with code '${err.code}': ${err.message}`; reject(new Error(msg)); }; - this.setDataMessageHandler(this.curMsgId.toString(), errorHandler, (value) => { + this.setDataMessageHandler(msgId.toString(), errorHandler, function (value) { const msg = _.truncate(_.isString(value) ? value : JSON.stringify(value), {length: 50}); log.debug(`Received data response from socket send: '${msg}'`); log.debug(`Original command: ${command}`); resolve(value); }); - data.__argument.WIRSocketDataKey.id = this.curMsgId; - data.__argument.WIRSocketDataKey = - Buffer.from(JSON.stringify(data.__argument.WIRSocketDataKey)); + + // make sure the message being sent has all the information that is needed + if (cmd.__argument.WIRSocketDataKey.params) { + cmd.__argument.WIRSocketDataKey.params.id = msgId; + if (!cmd.__argument.WIRSocketDataKey.params.targetId) { + cmd.__argument.WIRSocketDataKey.params.targetId = `page-${opts.pageIdKey}`; + } + if (cmd.__argument.WIRSocketDataKey.params.message) { + cmd.__argument.WIRSocketDataKey.params.message.id = msgId; + cmd.__argument.WIRSocketDataKey.params.message = JSON.stringify(cmd.__argument.WIRSocketDataKey.params.message); + } + } + cmd.__argument.WIRSocketDataKey.id = msgId; + cmd.__argument.WIRSocketDataKey = + Buffer.from(JSON.stringify(cmd.__argument.WIRSocketDataKey)); } else { // we want to immediately resolve this socket.write // any long term callbacks will do their business in the background socketCb = resolve; } - log.debug(`Sending '${data.__selector}' message to remote debugger`); + log.debug(`Sending '${cmd.__selector}' message to remote debugger (id: ${msgId})`); // remote debugger expects a binary plist as data let plist; try { - plist = bplistCreate(data); + plist = bplistCreate(cmd); } catch (e) { let msg = `Could not create binary plist from data: ${e.message}`; log.error(msg); @@ -277,11 +298,13 @@ export default class RemoteDebuggerRpcClient { }) .finally(() => { // remove this listener, so we don't exhaust the system - this.socket.removeListener('error', onSocketError); + if (_.isFunction(onSocketError)) { + this.socket.removeListener('error', onSocketError); + } }); } - receive (data) { + async receive (data) { // Append this new data to the existing Buffer this.received = Buffer.concat([this.received, data]); let dataLeftOver = true; @@ -289,17 +312,20 @@ export default class RemoteDebuggerRpcClient { // Parse multiple messages in the same packet while (dataLeftOver) { // Store a reference to where we were - let oldReadPos = this.readPos; + const oldReadPos = this.readPos; // Read the prefix (plist length) to see how far to read next // It's always 4 bytes long - let prefix = this.received.slice(this.readPos, this.readPos + 4); + const prefix = this.received.slice(this.readPos, this.readPos + 4); + if (_.isEmpty(prefix)) { + return; + } let msgLength; try { msgLength = bufferpack.unpack('L', prefix)[0]; - } catch (e) { - log.error(`Buffer could not unpack: ${e}`); + } catch (err) { + log.error(`Buffer could not unpack: ${err}`); return; } @@ -314,7 +340,7 @@ export default class RemoteDebuggerRpcClient { } // Extract the main body of the message (where the plist should be) - let body = this.received.slice(this.readPos, msgLength + this.readPos); + const body = this.received.slice(this.readPos, msgLength + this.readPos); // Extract the plist let plist; @@ -330,7 +356,7 @@ export default class RemoteDebuggerRpcClient { plist = plist[0]; } - for (let key of ['WIRMessageDataKey', 'WIRDestinationKey', 'WIRSocketDataKey']) { + for (const key of ['WIRMessageDataKey', 'WIRDestinationKey', 'WIRSocketDataKey']) { if (!_.isUndefined(plist[key])) { plist[key] = plist[key].toString('utf8'); } @@ -359,7 +385,7 @@ export default class RemoteDebuggerRpcClient { // Now do something with the plist if (plist) { - this.messageHandler.handleMessage(plist); + await this.messageHandler.handleMessage(plist); } } } diff --git a/lib/remote-debugger.js b/lib/remote-debugger.js index 76a99285..b9b68ce8 100644 --- a/lib/remote-debugger.js +++ b/lib/remote-debugger.js @@ -4,11 +4,12 @@ import log from './logger'; import { errorFromCode } from 'appium-base-driver'; import RemoteDebuggerRpcClient from './remote-debugger-rpc-client'; import messageHandlers from './message-handlers'; -import { appInfoFromDict, pageArrayFromDict, getDebuggerAppKey, getPossibleDebuggerAppKeys, checkParams, - getScriptForAtom, simpleStringify, deferredPromise } from './helpers'; +import { appInfoFromDict, pageArrayFromDict, getDebuggerAppKey, + getPossibleDebuggerAppKeys, checkParams, getScriptForAtom, + simpleStringify, deferredPromise } from './helpers'; import { util } from 'appium-support'; import _ from 'lodash'; -import Promise from 'bluebird'; +import B from 'bluebird'; const DEBUGGER_TYPES = { @@ -53,7 +54,7 @@ class RemoteDebugger extends events.EventEmitter { socketPath, pageReadyTimeout = PAGE_READY_TIMEOUT, remoteDebugProxy, - garbageCollectOnExecute = true, + garbageCollectOnExecute = false, } = opts; this.bundleId = bundleId; @@ -89,8 +90,10 @@ class RemoteDebugger extends events.EventEmitter { '_rpc_applicationDisconnected:': this.onAppDisconnect.bind(this), '_rpc_applicationUpdated:': this.onAppUpdate.bind(this), '_rpc_reportConnectedDriverList:': this.onReportDriverList.bind(this), - 'pageLoad': this.pageLoad.bind(this), - 'frameDetached': this.frameDetached.bind(this), + pageLoad: this.pageLoad.bind(this), + frameDetached: this.frameDetached.bind(this), + targetCreated: this.onTargetCreated.bind(this), + targetDestroyed: this.onTargetDestroyed.bind(this), }; this.rpcClient = null; @@ -117,11 +120,13 @@ class RemoteDebugger extends events.EventEmitter { // initialize the rpc client this.rpcClient = new RemoteDebuggerRpcClient({ + platformVersion: this.platformVersion, host: this.host, port: this.port, socketPath: this.socketPath, specialMessageHandlers: this.specialCbs, messageProxy: this.remoteDebugProxy, + }); await this.rpcClient.connect(); @@ -146,29 +151,9 @@ class RemoteDebugger extends events.EventEmitter { return !!(this.rpcClient && this.rpcClient.isConnected()); } - logApplicationDictionary (apps) { - function getValueString (key, value) { - if (_.isFunction(value)) { - return '[Function]'; - } - if (key === 'pageDict' && !_.isArray(value)) { - return '"Waiting for data"'; - } - return JSON.stringify(value); - } - log.debug('Current applications available:'); - for (let [app, info] of _.toPairs(apps)) { - log.debug(` Application: '${app}'`); - for (let [key, value] of _.toPairs(info)) { - let valueString = getValueString(key, value); - log.debug(` ${key}: ${valueString}`); - } - } - } - async setConnectionKey () { // only resolve when the connection response is received - return await new Promise((resolve, reject) => { + return await new B((resolve, reject) => { // local callback, called when the remote debugger has established // a connection to the app under test // `app` will be an array of dictionaries of app information @@ -181,7 +166,7 @@ class RemoteDebugger extends events.EventEmitter { // translate the received information into an easier-to-manage // hash with app id as key, and app info as value - for (let dict of _.values(apps)) { + for (const dict of _.values(apps)) { let [id, entry] = appInfoFromDict(dict); newDict[id] = entry; } @@ -208,13 +193,13 @@ class RemoteDebugger extends events.EventEmitter { let [id, entry] = appInfoFromDict(dict); if (this.appDict[id]) { // preserve the page dictionary for this entry - entry.pageDict = this.appDict[id].pageDict; + entry.pageArray = this.appDict[id].pageArray; } this.appDict[id] = entry; // add a promise to get the page dictionary - if (_.isUndefined(entry.pageDict)) { - entry.pageDict = deferredPromise(); + if (_.isUndefined(entry.pageArray)) { + entry.pageArray = deferredPromise(); } // try to get the app id from our connected apps @@ -223,6 +208,26 @@ class RemoteDebugger extends events.EventEmitter { } } + logApplicationDictionary (apps) { + function getValueString (key, value) { + if (_.isFunction(value)) { + return '[Function]'; + } + if (key === 'pageDict' && !_.isArray(value)) { + return `'Waiting for data'`; + } + return JSON.stringify(value); + } + log.debug('Current applications available:'); + for (const [app, info] of _.toPairs(apps)) { + log.debug(` Application: '${app}'`); + for (const [key, value] of _.toPairs(info)) { + const valueString = getValueString(key, value); + log.debug(` ${key}: ${valueString}`); + } + } + } + async selectApp (currentUrl = null, maxTries = SELECT_APP_RETRIES, ignoreAboutBlankUrl = false) { log.debug('Selecting application'); if (!this.appDict || _.keys(this.appDict).length === 0) { @@ -236,7 +241,7 @@ class RemoteDebugger extends events.EventEmitter { this.logApplicationDictionary(this.appDict); let possibleAppIds = getPossibleDebuggerAppKeys(this.bundleId, this.platformVersion, this.appDict); log.debug(`Trying out the possible app ids: ${possibleAppIds.join(', ')}`); - for (let attemptedAppIdKey of possibleAppIds) { + for (const attemptedAppIdKey of possibleAppIds) { try { log.debug(`Selecting app ${attemptedAppIdKey} (try #${i + 1} of ${maxTries})`); [appIdKey, pageDict] = await this.rpcClient.selectApp(attemptedAppIdKey, this.onAppConnect.bind(this)); @@ -248,24 +253,24 @@ class RemoteDebugger extends events.EventEmitter { } // save the page array for this app - this.appDict[appIdKey].pageDict = pageArrayFromDict(pageDict); + this.appDict[appIdKey].pageArray = 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. let found = false; dictLoop: for (const appDict of _.values(this.appDict)) { if (found) break; // eslint-disable-line curly - if (!appDict || !appDict.pageDict) { + if (!appDict || !appDict.pageArray) { continue; } // if the page dictionary has not been loaded yet from the web // inspector, wait for it or time out after 10s - if (appDict.pageDict.promise) { + if (appDict.pageArray.promise) { try { - await Promise.resolve(appDict.pageDict.promise).timeout(10000); + await B.resolve(appDict.pageArray.promise).timeout(10000); } catch (err) { - if (!(err instanceof Promise.TimeoutError)) { + if (!(err instanceof B.TimeoutError)) { throw err; } // on timeout, just go on @@ -273,7 +278,7 @@ class RemoteDebugger extends events.EventEmitter { } } - for (const dict of (appDict.pageDict || [])) { + for (const dict of (appDict.pageArray || [])) { if ((!ignoreAboutBlankUrl || dict.url !== 'about:blank') && (!currentUrl || dict.url === currentUrl)) { // save where we found the right page appIdKey = appDict.id; @@ -313,22 +318,24 @@ class RemoteDebugger extends events.EventEmitter { // wait for all the promises are back, or 30s passes const pagePromises = Object.values(this.appDict) - .filter((app) => !!app.pageDict && !!app.pageDict.promise) - .map((app) => app.pageDict.promise); + .filter((app) => !!app.pageArray && !!app.pageArray.promise) + .map((app) => app.pageArray.promise); if (pagePromises.length) { log.debug(`Waiting for ${pagePromises.length} pages to be fulfilled`); - await Promise.any([Promise.delay(30000), Promise.all(pagePromises)]); + await B.any([B.delay(30000), B.all(pagePromises)]); } // translate the dictionary into a useful form, and return to sender - let pageArray = pageArrayFromDict(pageDict); + const pageArray = _.isEmpty(this.appDict[appIdKey].pageArray) + ? pageArrayFromDict(pageDict) + : this.appDict[appIdKey].pageArray; log.debug(`Finally selecting app ${this.appIdKey}: ${simpleStringify(pageArray)}`); let fullPageArray = []; - for (let [app, info] of _.toPairs(this.appDict)) { - if (!_.isArray(info.pageDict)) continue; // eslint-disable-line curly + for (const [app, info] of _.toPairs(this.appDict)) { + if (!_.isArray(info.pageArray)) continue; // eslint-disable-line curly let id = app.replace('PID:', ''); - for (let page of info.pageDict) { + for (const page of info.pageArray) { if (page.url && (!ignoreAboutBlankUrl || page.url !== 'about:blank') && (!currentUrl || page.url === currentUrl)) { let pageDict = _.clone(page); pageDict.id = `${id}.${pageDict.id}`; @@ -355,13 +362,13 @@ class RemoteDebugger extends events.EventEmitter { await this.rpcClient.send('enablePage', { appIdKey: this.appIdKey, pageIdKey: this.pageIdKey, - debuggerType: this.debuggerType + debuggerType: this.debuggerType, + targetId: 'page-1', }); log.debug('Enabled activity on page'); // make sure everything is ready to go - let ready = await this.checkPageIsReady(); - if (!skipReadyCheck && !ready) { + if (!skipReadyCheck && !await this.checkPageIsReady()) { await this.pageUnload(); } } @@ -369,9 +376,9 @@ class RemoteDebugger extends events.EventEmitter { async executeAtom (atom, args, frames) { if (!this.rpcClient.connected) throw new Error('Remote debugger is not connected'); // eslint-disable-line curly - let script = await getScriptForAtom(atom, args, frames); - - let value = await this.execute(script, true); + log.debug(`Executing atom '${atom}'`); + const script = await getScriptForAtom(atom, args, frames); + const value = await this.execute(script, true); log.debug(`Received result for atom '${atom}' execution: ${_.truncate(simpleStringify(value), {length: RESPONSE_LOG_LENGTH})}`); return value; } @@ -394,12 +401,12 @@ class RemoteDebugger extends events.EventEmitter { let start = startPageLoadMs || Date.now(); log.debug('Page loaded, verifying whether ready'); - let verify = async () => { + const verify = async () => { this.pageLoadDelay = util.cancellableDelay(timeoutMs); try { await this.pageLoadDelay; } catch (err) { - if (err instanceof Promise.CancellationError) { + if (err instanceof B.CancellationError) { // if the promise has been cancelled // we want to skip checking the readiness return; @@ -416,7 +423,7 @@ class RemoteDebugger extends events.EventEmitter { await pageLoadVerifyHook(); } - let ready = await this.checkPageIsReady(); + const ready = await this.checkPageIsReady(); // if we are ready, or we've spend too much time on this if (ready || (this.pageLoadMs > 0 && (start + this.pageLoadMs) < Date.now())) { @@ -450,22 +457,22 @@ class RemoteDebugger extends events.EventEmitter { } async checkPageIsReady () { - let errors = checkParams({appIdKey: this.appIdKey}); + const errors = checkParams({appIdKey: this.appIdKey}); if (errors) throw new Error(errors); // eslint-disable-line curly log.debug('Checking document readyState'); const readyCmd = '(function (){ return document.readyState; })()'; let readyState = 'loading'; try { - readyState = await Promise.resolve(this.execute(readyCmd, true)).timeout(this.pageReadyTimeout); + readyState = await B.resolve(this.execute(readyCmd, true)).timeout(this.pageReadyTimeout); } catch (err) { - if (!(err instanceof Promise.TimeoutError)) { + if (!(err instanceof B.TimeoutError)) { throw err; } log.debug(`Page readiness check timed out after ${this.pageReadyTimeout}ms`); return false; } - log.debug(`readyState was ${simpleStringify(readyState)}`); + log.debug(`Document readyState is '${readyState}'`); return readyState === 'complete'; } @@ -487,7 +494,7 @@ class RemoteDebugger extends events.EventEmitter { if (!this.useNewSafari) { // a small pause for the browser to catch up - await Promise.delay(1000); + await B.delay(1000); } if (this.debuggerType === DEBUGGER_TYPES.webinspector) { @@ -497,7 +504,7 @@ class RemoteDebugger extends events.EventEmitter { } async waitForFrameNavigated () { - return await new Promise(async (resolve, reject) => { + return await new B(async (resolve, reject) => { log.debug('Waiting for frame navigated message...'); let startMs = Date.now(); @@ -609,7 +616,7 @@ class RemoteDebugger extends events.EventEmitter { command, appIdKey: this.appIdKey, pageIdKey: this.pageIdKey, - debuggerType: this.debuggerType + debuggerType: this.debuggerType, }); return this.convertResult(res); @@ -624,7 +631,7 @@ class RemoteDebugger extends events.EventEmitter { } log.debug('Calling javascript function'); - let res = await this.rpcClient.send('callJSFunction', { + const res = await this.rpcClient.send('callJSFunction', { objId, fn, args, @@ -698,7 +705,7 @@ class RemoteDebugger extends events.EventEmitter { return; } - await Promise.resolve(this.rpcClient.send('garbageCollect', { + await B.resolve(this.rpcClient.send('garbageCollect', { appIdKey: this.appIdKey, pageIdKey: this.pageIdKey, debuggerType: this.debuggerType @@ -706,7 +713,7 @@ class RemoteDebugger extends events.EventEmitter { .then(function () { // eslint-disable-line promise/prefer-await-to-then log.debug(`Garbage collection successful`); }).catch(function (err) { // eslint-disable-line promise/prefer-await-to-callbacks - if (err instanceof Promise.TimeoutError) { + if (err instanceof B.TimeoutError) { log.debug(`Garbage collection timed out after ${timeoutMs}ms`); } else { log.debug(`Unable to collect garbage: ${err.message}`); @@ -721,7 +728,7 @@ RemoteDebugger.EVENT_FRAMES_DETACHED = 'remote_debugger_frames_detached'; RemoteDebugger.EVENT_DISCONNECT = 'remote_debugger_disconnect'; // add generic callbacks -for (let [name, handler] of _.toPairs(messageHandlers)) { +for (const [name, handler] of _.toPairs(messageHandlers)) { RemoteDebugger.prototype[name] = handler; } diff --git a/lib/remote-messages.js b/lib/remote-messages.js index aedf5656..d74e5973 100644 --- a/lib/remote-messages.js +++ b/lib/remote-messages.js @@ -3,253 +3,294 @@ import { DEBUGGER_TYPES } from './remote-debugger'; import _ from 'lodash'; -/* - * Connection functions - */ - -function setConnectionKey (connId) { - return { - __argument: { - WIRConnectionIdentifierKey: connId - }, - __selector: '_rpc_reportIdentifier:' - }; -} -function connectToApp (connId, appIdKey) { - return { - __argument: { - WIRConnectionIdentifierKey: connId, - WIRApplicationIdentifierKey: appIdKey - }, - __selector: '_rpc_forwardGetListing:' - }; -} +class RemoteMessages { + constructor (targetBased = false) { + this.targetBased = targetBased; + } -function setSenderKey (connId, senderId, appIdKey, pageIdKey) { - return { - __argument: { - WIRApplicationIdentifierKey: appIdKey, - WIRConnectionIdentifierKey: connId, - WIRSenderKey: senderId, - WIRPageIdentifierKey: pageIdKey, - WIRAutomaticallyPause: false - }, - __selector: '_rpc_forwardSocketSetup:' - }; -} + /* + * Connection functions + */ -/* - * Action functions - */ - -function indicateWebView (connId, appIdKey, pageIdKey, enabled) { - return { - __argument: { - WIRApplicationIdentifierKey: appIdKey, - WIRIndicateEnabledKey: _.isUndefined(enabled) ? true : enabled, - WIRConnectionIdentifierKey: connId, - WIRPageIdentifierKey: pageIdKey - }, - __selector: '_rpc_forwardIndicateWebView:' - }; -} + setConnectionKey (connId) { + return { + __argument: { + WIRConnectionIdentifierKey: connId + }, + __selector: '_rpc_reportIdentifier:' + }; + } -function sendJSCommand (connId, senderId, appIdKey, pageIdKey, debuggerType, js) { - return command('Runtime.evaluate', - {expression: js, returnByValue: true}, appIdKey, connId, senderId, pageIdKey, debuggerType); -} + connectToApp (connId, appIdKey) { + return { + __argument: { + WIRConnectionIdentifierKey: connId, + WIRApplicationIdentifierKey: appIdKey + }, + __selector: '_rpc_forwardGetListing:' + }; + } -function callJSFunction (connId, senderId, appIdKey, pageIdKey, debuggerType, objId, fn, args) { - return command('Runtime.callFunctionOn', - {objectId: objId, functionDeclaration: fn, arguments: args, returnByValue: true}, - appIdKey, connId, senderId, pageIdKey, debuggerType); -} + setSenderKey (connId, senderId, appIdKey, pageIdKey) { + return { + __argument: { + WIRApplicationIdentifierKey: appIdKey, + WIRConnectionIdentifierKey: connId, + WIRSenderKey: senderId, + WIRPageIdentifierKey: pageIdKey, + WIRAutomaticallyPause: false + }, + __selector: '_rpc_forwardSocketSetup:' + }; + } -function setUrl (connId, senderId, appIdKey, pageIdKey, debuggerType, url) { - return command('Page.navigate', {url}, appIdKey, connId, - senderId, pageIdKey, debuggerType); -} + indicateWebView (connId, appIdKey, pageIdKey, opts) { + const {enabled} = opts; + return { + __argument: { + WIRApplicationIdentifierKey: appIdKey, + WIRIndicateEnabledKey: _.isUndefined(enabled) ? true : enabled, + WIRConnectionIdentifierKey: connId, + WIRPageIdentifierKey: pageIdKey + }, + __selector: '_rpc_forwardIndicateWebView:' + }; + } -function enablePage (connId, senderId, appIdKey, pageIdKey, debuggerType) { - return command('Page.enable', {}, appIdKey, connId, senderId, - pageIdKey, debuggerType); -} -function startTimeline (connId, senderId, appIdKey, pageIdKey, debuggerType) { - return command('Timeline.start', {}, appIdKey, connId, senderId, - pageIdKey, debuggerType); -} -function stopTimeline (connId, senderId, appIdKey, pageIdKey, debuggerType) { - return command('Timeline.stop', {}, appIdKey, connId, senderId, - pageIdKey, debuggerType); -} + /* + * Action functions + */ -function startConsole (connId, senderId, appIdKey, pageIdKey, debuggerType) { - return command('Console.enable', {}, appIdKey, connId, senderId, - pageIdKey, debuggerType); -} + getFullCommand (connId, senderId, appIdKey, pageIdKey, debuggerType, method, params) { + const [realMethod, realParams] = this.targetBased + ? ['Target.sendMessageToTarget', {message: {method, params}}] + : [method, params]; + return this.command(realMethod, realParams, appIdKey, connId, senderId, pageIdKey, debuggerType); + } -function stopConsole (connId, senderId, appIdKey, pageIdKey, debuggerType) { - return command('Console.disable', {}, appIdKey, connId, senderId, - pageIdKey, debuggerType); -} + sendJSCommand (connId, senderId, appIdKey, pageIdKey, debuggerType, opts = {}) { + const method = 'Runtime.evaluate'; + const params = { + expression: opts.command, + returnByValue: true, + }; + return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, debuggerType, method, params); + } -function startNetwork (connId, senderId, appIdKey, pageIdKey, debuggerType) { - return command('Network.enable', {}, appIdKey, connId, senderId, - pageIdKey, debuggerType); -} + callJSFunction (connId, senderId, appIdKey, pageIdKey, debuggerType, opts = {}) { + const method = 'Runtime.callFunctionOn'; + const params = { + objectId: opts.objId, + functionDeclaration: opts.fn, + arguments: opts.args, + returnByValue: true, + }; -function stopNetwork (connId, senderId, appIdKey, pageIdKey, debuggerType) { - return command('Network.disable', {}, appIdKey, connId, senderId, - pageIdKey, debuggerType); -} + return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, debuggerType, method, params); + } -function getCookies (connId, senderId, appIdKey, pageIdKey, debuggerType, urls) { - return command('Page.getCookies', {urls}, appIdKey, connId, senderId, - pageIdKey, debuggerType); -} + setUrl (connId, senderId, appIdKey, pageIdKey, debuggerType, opts = {}) { + const method = 'Page.navigate'; + const params = { + url: opts.url, + }; + return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, debuggerType, method, params); + } -function deleteCookie (connId, senderId, appIdKey, pageIdKey, debuggerType, cookieName, url) { - return command('Page.deleteCookie', {cookieName, url}, appIdKey, connId, senderId, - pageIdKey, debuggerType); -} + enablePage (connId, senderId, appIdKey, pageIdKey, debuggerType) { + const method = 'Page.enable'; + const params = {}; + return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, debuggerType, method, params); + } -function garbageCollect (connId, senderId, appIdKey, pageIdKey, debuggerType) { - return command('Heap.gc', {}, appIdKey, connId, senderId, - pageIdKey, debuggerType); -} + startTimeline (connId, senderId, appIdKey, pageIdKey, debuggerType) { + const method = 'Timeline.start'; + const params = {}; + return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, debuggerType, method, params); + } + + stopTimeline (connId, senderId, appIdKey, pageIdKey, debuggerType) { + const method = 'Timeline.stop'; + const params = {}; + return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, debuggerType, method, params); + } + startConsole (connId, senderId, appIdKey, pageIdKey, debuggerType) { + const method = 'Console.enable'; + const params = {}; + return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, debuggerType, method, params); + } -/* - * Internal functions - */ + stopConsole (connId, senderId, appIdKey, pageIdKey, debuggerType) { + const method = 'Console.disable'; + const params = {}; + return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, debuggerType, method, params); + } -function command (method, params, appIdKey, connId, senderId, pageIdKey, debuggerType) { - if (debuggerType !== null && debuggerType === DEBUGGER_TYPES.webkit) { - return commandWebKit(method, params); - } else { - return commandWebInspector(method, params, appIdKey, connId, senderId, pageIdKey); + startNetwork (connId, senderId, appIdKey, pageIdKey, debuggerType) { + const method = 'Network.enable'; + const params = {}; + return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, debuggerType, method, params); + } + + stopNetwork (connId, senderId, appIdKey, pageIdKey, debuggerType) { + const method = 'Network.disable'; + const params = {}; + return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, debuggerType, method, params); + } + + getCookies (connId, senderId, appIdKey, pageIdKey, debuggerType, opts = {}) { + const method = 'Page.getCookies'; + const params = { + urls: opts.url, + }; + return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, debuggerType, method, params); + } + + deleteCookie (connId, senderId, appIdKey, pageIdKey, debuggerType, opts = {}) { + const method = 'Page.deleteCookie'; + const params = { + cookieName: opts.cookieName, + url: opts.url, + }; + return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, debuggerType, method, params); + } + + garbageCollect (connId, senderId, appIdKey, pageIdKey, debuggerType) { + const method = 'Heap.gc'; + const params = {}; + return this.getFullCommand(connId, senderId, appIdKey, pageIdKey, debuggerType, method, params); } -} -function commandWebInspector (method, params, appIdKey, connId, senderId, pageIdKey) { - let plist = { - __argument: { - WIRApplicationIdentifierKey: appIdKey, - WIRSocketDataKey: { - method, - params: { - objectGroup: 'console', - includeCommandLineAPI: true, - doNotPauseOnExceptionsAndMuteConsole: true, - } + + /* + * Internal functions + */ + + command (method, params, appIdKey, connId, senderId, pageIdKey, debuggerType) { + if (debuggerType !== null && debuggerType === DEBUGGER_TYPES.webkit) { + return this.commandWebKit(method, params); + } else { + return this.commandWebInspector(method, params, appIdKey, connId, senderId, pageIdKey); + } + } + + commandWebInspector (method, params, appIdKey, connId, senderId, pageIdKey) { + let plist = { + __argument: { + WIRApplicationIdentifierKey: appIdKey, + WIRSocketDataKey: { + method, + params: { + objectGroup: 'console', + includeCommandLineAPI: true, + doNotPauseOnExceptionsAndMuteConsole: true, + } + }, + WIRConnectionIdentifierKey: connId, + WIRSenderKey: senderId, + WIRPageIdentifierKey: pageIdKey, + WIRAutomaticallyPause: true, }, - WIRConnectionIdentifierKey: connId, - WIRSenderKey: senderId, - WIRPageIdentifierKey: pageIdKey - }, - __selector: '_rpc_forwardSocketData:' - }; - if (params) { - plist.__argument.WIRSocketDataKey.params = - _.extend(plist.__argument.WIRSocketDataKey.params, params); - } - return plist; -} + __selector: '_rpc_forwardSocketData:' + }; + if (params) { + plist.__argument.WIRSocketDataKey.params = + _.extend(plist.__argument.WIRSocketDataKey.params, params); + } + return plist; + } -//generate a json request using the webkit protocol -function commandWebKit (method, params) { - let jsonRequest = { - method, - params: { - objectGroup: 'console', - includeCommandLineAPI: true, - doNotPauseOnExceptionsAndMuteConsole: true + //generate a json request using the webkit protocol + commandWebKit (method, params) { + let jsonRequest = { + method, + params: { + objectGroup: 'console', + includeCommandLineAPI: true, + doNotPauseOnExceptionsAndMuteConsole: true + } + }; + if (params) { + //if there any parameters add them + jsonRequest.params = _.extend(jsonRequest.params, params); } - }; - if (params) { - //if there any parameters add them - jsonRequest.params = _.extend(jsonRequest.params, params); + return jsonRequest; } - return jsonRequest; -} -export default function getRemoteCommand (command, opts) { - let cmd; - - switch (command) { - case 'setConnectionKey': - cmd = setConnectionKey(opts.connId); - break; - case 'connectToApp': - cmd = connectToApp(opts.connId, opts.appIdKey); - break; - case 'setSenderKey': - cmd = setSenderKey(opts.connId, opts.senderId, opts.appIdKey, - opts.pageIdKey); - break; - case 'indicateWebView': - cmd = indicateWebView(opts.connId, opts.appIdKey, opts.pageIdKey, - opts.enabled); - break; - case 'sendJSCommand': - cmd = sendJSCommand(opts.connId, opts.senderId, opts.appIdKey, - opts.pageIdKey, opts.debuggerType, opts.command); - break; - case 'callJSFunction': - cmd = callJSFunction(opts.connId, opts.senderId, opts.appIdKey, - opts.pageIdKey, opts.debuggerType, opts.objId, opts.fn, - opts.args); - break; - case 'setUrl': - cmd = setUrl(opts.connId, opts.senderId, opts.appIdKey, opts.pageIdKey, - opts.debuggerType, opts.url); - break; - case 'enablePage': - cmd = enablePage(opts.connId, opts.senderId, opts.appIdKey, - opts.pageIdKey, opts.debuggerType); - break; - case 'startTimeline': - cmd = startTimeline(opts.connId, opts.senderId, opts.appIdKey, - opts.pageIdKey, opts.debuggerType); - break; - case 'stopTimeline': - cmd = stopTimeline(opts.connId, opts.senderId, opts.appIdKey, - opts.pageIdKey, opts.debuggerType); - break; - case 'startConsole': - cmd = startConsole(opts.connId, opts.senderId, opts.appIdKey, - opts.pageIdKey, opts.debuggerType); - break; - case 'stopConsole': - cmd = stopConsole(opts.connId, opts.senderId, opts.appIdKey, - opts.pageIdKey, opts.debuggerType); - break; - case 'startNetwork': - cmd = startNetwork(opts.connId, opts.senderId, opts.appIdKey, - opts.pageIdKey, opts.debuggerType); - break; - case 'stopNetwork': - cmd = stopNetwork(opts.connId, opts.senderId, opts.appIdKey, - opts.pageIdKey, opts.debuggerType); - break; - case 'getCookies': - cmd = getCookies(opts.connId, opts.senderId, opts.appIdKey, - opts.pageIdKey, opts.debuggerType, opts.urls); - break; - case 'deleteCookie': - cmd = deleteCookie(opts.connId, opts.senderId, opts.appIdKey, - opts.pageIdKey, opts.debuggerType, opts.cookieName, opts.url); - break; - case 'garbageCollect': - cmd = garbageCollect(opts.connId, opts.senderId, opts.appIdKey, - opts.pageIdKey, opts.debuggerType); - break; - default: - throw new Error(`Unknown command: ${command}`); - } - - return cmd; + getRemoteCommand (command, opts) { + let cmd; + + const { + connId, + appIdKey, + senderId, + pageIdKey, + debuggerType, + } = opts; + + switch (command) { + case 'setConnectionKey': + cmd = this.setConnectionKey(connId); + break; + case 'connectToApp': + cmd = this.connectToApp(connId, appIdKey); + break; + case 'setSenderKey': + cmd = this.setSenderKey(connId, senderId, appIdKey, pageIdKey); + break; + case 'indicateWebView': + cmd = this.indicateWebView(connId, appIdKey, pageIdKey, opts); + break; + case 'sendJSCommand': + cmd = this.sendJSCommand(connId, senderId, appIdKey, pageIdKey, debuggerType, opts); + break; + case 'callJSFunction': + cmd = this.callJSFunction(connId, senderId, appIdKey, pageIdKey, debuggerType, opts); + break; + case 'setUrl': + cmd = this.setUrl(connId, senderId, appIdKey, pageIdKey, debuggerType, opts); + break; + case 'enablePage': + cmd = this.enablePage(connId, senderId, appIdKey, pageIdKey, debuggerType); + break; + case 'startTimeline': + cmd = this.startTimeline(connId, senderId, appIdKey, pageIdKey, debuggerType); + break; + case 'stopTimeline': + cmd = this.stopTimeline(connId, senderId, appIdKey, pageIdKey, debuggerType); + break; + case 'startConsole': + cmd = this.startConsole(connId, senderId, appIdKey, pageIdKey, debuggerType); + break; + case 'stopConsole': + cmd = this.stopConsole(connId, senderId, appIdKey, pageIdKey, debuggerType); + break; + case 'startNetwork': + cmd = this.startNetwork(connId, senderId, appIdKey, pageIdKey, debuggerType); + break; + case 'stopNetwork': + cmd = this.stopNetwork(connId, senderId, appIdKey, pageIdKey, debuggerType); + break; + case 'getCookies': + cmd = this.getCookies(connId, senderId, appIdKey, pageIdKey, debuggerType, opts); + break; + case 'deleteCookie': + cmd = this.deleteCookie(connId, senderId, appIdKey, pageIdKey, debuggerType, opts); + break; + case 'garbageCollect': + cmd = this.garbageCollect(connId, senderId, appIdKey, pageIdKey, debuggerType); + break; + default: + throw new Error(`Unknown command: ${command}`); + } + + return cmd; + } } + +export { RemoteMessages }; +export default RemoteMessages; diff --git a/lib/webkit-rpc-client.js b/lib/webkit-rpc-client.js index 563bbe2d..ff4a3988 100644 --- a/lib/webkit-rpc-client.js +++ b/lib/webkit-rpc-client.js @@ -1,8 +1,8 @@ import log from './logger'; import { REMOTE_DEBUGGER_PORT, RPC_RESPONSE_TIMEOUT_MS } from './remote-debugger'; -import getRemoteCommand from './remote-messages'; +import RemoteMessages from './remote-messages'; import WebSocket from 'ws'; -import Promise from 'bluebird'; +import B from 'bluebird'; import _ from 'lodash'; import events from 'events'; import { simpleStringify } from './helpers'; @@ -26,10 +26,12 @@ export default class WebKitRpcClient extends events.EventEmitter { this.dataHandlers = {}; this.dataMethods = {}; this.errorHandlers = {}; + + this.remoteMessages = new RemoteMessages(); } async connect (pageId) { - return await new Promise((resolve, reject) => { + return await new B((resolve, reject) => { // we will only resolve this call when the socket is open // WebKit url let url = `ws://${this.host}:${this.port}/devtools/page/${pageId}`; @@ -71,7 +73,7 @@ export default class WebKitRpcClient extends events.EventEmitter { } async send (command, opts = {}) { - let data = getRemoteCommand(command, _.defaults({connId: this.connId, senderId: this.senderId}, opts)); + let data = this.remoteMessages.getRemoteCommand(command, _.defaults({connId: this.connId, senderId: this.senderId}, opts)); log.debug(`Sending WebKit data: ${_.truncate(JSON.stringify(data), DATA_LOG_LENGTH)}`); log.debug(`Webkit response timeout: ${this.responseTimeout}`); @@ -80,7 +82,7 @@ export default class WebKitRpcClient extends events.EventEmitter { data.id = this.curMsgId; const id = this.curMsgId.toString(); - return await new Promise((resolve, reject) => { + return await new B((resolve, reject) => { // only resolve the send command when WebKit returns a response // store the handler and the data sent this.dataHandlers[id] = resolve; @@ -99,7 +101,7 @@ export default class WebKitRpcClient extends events.EventEmitter { throw e; } log.warn(e.message); - return Promise.resolve(null); + return B.resolve(null); }).finally((res) => { // no need to hold onto anything delete this.dataHandlers[id]; diff --git a/test/helpers/remote-debugger-server.js b/test/helpers/remote-debugger-server.js index a8244995..ea10a9dd 100644 --- a/test/helpers/remote-debugger-server.js +++ b/test/helpers/remote-debugger-server.js @@ -4,7 +4,7 @@ import net from 'net'; import bplistCreate from 'bplist-creator'; import bplistParser from 'bplist-parser'; import bufferpack from 'bufferpack'; -import Promise from 'bluebird'; +import B from 'bluebird'; import { logger } from 'appium-support'; @@ -273,7 +273,7 @@ class RemoteDebuggerServer { async start () { let leftOverData; - return await new Promise((resolve, reject) => { + return await new B((resolve, reject) => { this.server = net.createServer((c) => { this.client = c; c.on('end', () => { @@ -332,7 +332,7 @@ class RemoteDebuggerServer { } async stop () { - return await new Promise((resolve) => { + return await new B((resolve) => { if (this.server) { if (this.client) { this.client.end(); diff --git a/test/helpers/webkit-remote-debugger-server.js b/test/helpers/webkit-remote-debugger-server.js index 6d5b1ac0..20da1226 100644 --- a/test/helpers/webkit-remote-debugger-server.js +++ b/test/helpers/webkit-remote-debugger-server.js @@ -1,7 +1,7 @@ // transpile:main import http from 'http'; -import Promise from 'bluebird'; +import B from 'bluebird'; import ws from 'ws'; import { logger } from 'appium-support'; @@ -24,7 +24,7 @@ class WebKitRemoteDebuggerServer { async start (ws = false) { if (!ws) { // just need a simple http server for non-websocket calls - return await new Promise((resolve) => { + return await new B((resolve) => { this.server = http.createServer((req, res) => { res.writeHead(200, {'Content-Type': 'application/json'}); if (this.nextResponse) { @@ -41,7 +41,7 @@ class WebKitRemoteDebuggerServer { // need a fake websocket server // but it doesn't need to do anything but connect and disconnect this.ws = true; - return new Promise((resolve) => { + return new B((resolve) => { this.server = new WebSocketServer({host: 'localhost', port: 1337}, resolve); }); } @@ -50,7 +50,7 @@ class WebKitRemoteDebuggerServer { // stop one or both of the servers. async stop () { if (!this.ws) { - return await new Promise((resolve) => { + return await new B((resolve) => { if (this.server) { this.server.close((err) => { // eslint-disable-line promise/prefer-await-to-callbacks resolve(`Stopped listening: ${err}`); @@ -62,7 +62,7 @@ class WebKitRemoteDebuggerServer { } else { // websocket server isn't asynchronous this.server.close(); - return Promise.resolve(); + return B.resolve(); } } diff --git a/test/unit/remote-debugger-specs.js b/test/unit/remote-debugger-specs.js index 6c93ebf2..8ed59fcd 100644 --- a/test/unit/remote-debugger-specs.js +++ b/test/unit/remote-debugger-specs.js @@ -6,7 +6,7 @@ import { withConnectedServer } from '../helpers/server-setup'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import sinon from 'sinon'; -import Promise from 'bluebird'; +import B from 'bluebird'; import { MOCHA_TIMEOUT } from '../helpers/helpers'; @@ -27,6 +27,7 @@ describe('RemoteDebugger', function () { pageLoadMs: 5000, port: 27754, debuggerType: DEBUGGER_TYPES.webinspector, + garbageCollectOnExecute: true, }; rd = new RemoteDebugger(opts); rds[0] = rd; @@ -122,7 +123,7 @@ describe('RemoteDebugger', function () { if (rd.appIdKey !== initialIdKey) { break; } - await Promise.delay(100); + await B.delay(100); } let spy = sinon.spy(rd.rpcClient, 'selectApp'); @@ -143,7 +144,7 @@ describe('RemoteDebugger', function () { let spy = sinon.spy(rd.rpcClient, 'selectApp'); let selectPromise = rd.selectApp(); - await Promise.delay(1000); + await B.delay(1000); server.sendPageInfoMessage('PID:44'); server.sendPageInfoMessage('PID:42'); server.sendPageInfoMessage('PID:46'); @@ -164,7 +165,7 @@ describe('RemoteDebugger', function () { })); describe('#selectPage', withConnectedServer(rds, (server) => { - confirmRpcSend('selectPage', [1, 2, true], 4); + confirmRpcSend('selectPage', [1, 2, true], 2); confirmRpcSend('selectPage', [1, 2, false], 6); confirmRemoteDebuggerErrorHandling(server, 'selectPage', [1, 2]); })); diff --git a/test/unit/remote-messages-specs.js b/test/unit/remote-messages-specs.js index 2618ce13..71f99c02 100644 --- a/test/unit/remote-messages-specs.js +++ b/test/unit/remote-messages-specs.js @@ -1,15 +1,17 @@ // transpile:mocha -import getRemoteCommand from '../../lib/remote-messages'; +import RemoteMessages from '../../lib/remote-messages'; import chai from 'chai'; import { MOCHA_TIMEOUT } from '../helpers/helpers'; chai.should(); -describe('getRemoteCommand', function () { +describe('RemoteMessages#getRemoteCommand', function () { this.timeout(MOCHA_TIMEOUT); + const remoteMessages = new RemoteMessages(); + const commands = [ 'setConnectionKey', 'connectToApp', 'setSenderKey', 'indicateWebView', 'sendJSCommand', 'callJSFunction', 'setUrl', 'enablePage', 'startTimeline', @@ -17,7 +19,7 @@ describe('getRemoteCommand', function () { ]; for (const command of commands) { it(`should be able to retrieve ${command} command`, function () { - const remoteCommand = getRemoteCommand(command, {}); + const remoteCommand = remoteMessages.getRemoteCommand(command, {}); remoteCommand.should.be.an.instanceof(Object); remoteCommand.__argument.should.exist; remoteCommand.__selector.should.exist;