mirror of
https://github.com/appium/appium.git
synced 2026-10-25 02:00:57 +08:00
600 lines
24 KiB
JavaScript
600 lines
24 KiB
JavaScript
import _ from 'lodash';
|
|
import log from './logger';
|
|
import { getBuildInfo, updateBuildInfo, APPIUM_VER } from './config';
|
|
import { findMatchingDriver } from './drivers';
|
|
import { BaseDriver, errors, isSessionCommand } from 'appium-base-driver';
|
|
import B from 'bluebird';
|
|
import AsyncLock from 'async-lock';
|
|
import { parseCapsForInnerDriver, pullSettings } from './utils';
|
|
import { util } from 'appium-support';
|
|
|
|
const desiredCapabilityConstraints = {
|
|
automationName: {
|
|
presence: true,
|
|
isString: true,
|
|
},
|
|
platformName: {
|
|
presence: true,
|
|
isString: true,
|
|
},
|
|
};
|
|
|
|
const sessionsListGuard = new AsyncLock();
|
|
const pendingDriversGuard = new AsyncLock();
|
|
|
|
class AppiumDriver extends BaseDriver {
|
|
constructor (args) {
|
|
// It is necessary to set `--tmp` here since it should be set to
|
|
// process.env.APPIUM_TMP_DIR once at an initial point in the Appium lifecycle.
|
|
// The process argument will be referenced by BaseDriver.
|
|
// Please call appium-support.tempDir module to apply this benefit.
|
|
if (args.tmpDir) {
|
|
process.env.APPIUM_TMP_DIR = args.tmpDir;
|
|
}
|
|
|
|
super(args);
|
|
|
|
this.desiredCapConstraints = desiredCapabilityConstraints;
|
|
|
|
// the main Appium Driver has no new command timeout
|
|
this.newCommandTimeoutMs = 0;
|
|
|
|
this.args = Object.assign({}, args);
|
|
|
|
// Access to sessions list must be guarded with a Semaphore, because
|
|
// it might be changed by other async calls at any time
|
|
// It is not recommended to access this property directly from the outside
|
|
this.sessions = {};
|
|
|
|
// Access to pending drivers list must be guarded with a Semaphore, because
|
|
// it might be changed by other async calls at any time
|
|
// It is not recommended to access this property directly from the outside
|
|
this.pendingDrivers = {};
|
|
|
|
this.pluginClasses = []; // list of which plugins are active
|
|
this.sessionPlugins = {}; // map of sessions to actual plugin instances per session
|
|
this.sessionlessPlugins = []; // some commands are sessionless, so we need a set of plugins for them
|
|
|
|
// allow this to happen in the background, so no `await`
|
|
updateBuildInfo();
|
|
}
|
|
|
|
/**
|
|
* Cancel commands queueing for the umbrella Appium driver
|
|
*/
|
|
get isCommandsQueueEnabled () {
|
|
return false;
|
|
}
|
|
|
|
sessionExists (sessionId) {
|
|
const dstSession = this.sessions[sessionId];
|
|
return dstSession && dstSession.sessionId !== null;
|
|
}
|
|
|
|
driverForSession (sessionId) {
|
|
return this.sessions[sessionId];
|
|
}
|
|
|
|
async getStatus () { // eslint-disable-line require-await
|
|
return {
|
|
build: _.clone(getBuildInfo()),
|
|
};
|
|
}
|
|
|
|
async getSessions () {
|
|
const sessions = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions);
|
|
return _.toPairs(sessions)
|
|
.map(([id, driver]) => ({id, capabilities: driver.caps}));
|
|
}
|
|
|
|
printNewSessionAnnouncement (driverName, driverVersion, driverBaseVersion) {
|
|
log.info(driverVersion
|
|
? `Appium v${APPIUM_VER} creating new ${driverName} (v${driverVersion}) session`
|
|
: `Appium v${APPIUM_VER} creating new ${driverName} session`
|
|
);
|
|
log.info(`Checking BaseDriver versions for Appium and ${driverName}`);
|
|
log.info(AppiumDriver.baseVersion
|
|
? `Appium's BaseDriver version is ${AppiumDriver.baseVersion}`
|
|
: `Could not determine Appium's BaseDriver version`
|
|
);
|
|
log.info(driverBaseVersion
|
|
? `${driverName}'s BaseDriver version is ${driverBaseVersion}`
|
|
: `Could not determine ${driverName}'s BaseDriver version`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This is just an alias for driver.js's method, which is necessary for
|
|
* mocking in the test suite
|
|
*/
|
|
_findMatchingDriver (...args) {
|
|
return findMatchingDriver(...args);
|
|
}
|
|
|
|
/**
|
|
* Create a new session
|
|
* @param {Object} jsonwpCaps JSONWP formatted desired capabilities
|
|
* @param {Object} reqCaps Required capabilities (JSONWP standard)
|
|
* @param {Object} w3cCapabilities W3C capabilities
|
|
* @return {Array} Unique session ID and capabilities
|
|
*/
|
|
async createSession (jsonwpCaps, reqCaps, w3cCapabilities) {
|
|
const defaultCapabilities = _.cloneDeep(this.args.defaultCapabilities);
|
|
const defaultSettings = pullSettings(defaultCapabilities);
|
|
jsonwpCaps = _.cloneDeep(jsonwpCaps);
|
|
const jwpSettings = Object.assign({}, defaultSettings, pullSettings(jsonwpCaps));
|
|
w3cCapabilities = _.cloneDeep(w3cCapabilities);
|
|
// It is possible that the client only provides caps using JSONWP standard,
|
|
// although firstMatch/alwaysMatch properties are still present.
|
|
// In such case we assume the client understands W3C protocol and merge the given
|
|
// JSONWP caps to W3C caps
|
|
const w3cSettings = Object.assign({}, jwpSettings);
|
|
Object.assign(w3cSettings, pullSettings((w3cCapabilities || {}).alwaysMatch || {}));
|
|
for (const firstMatchEntry of ((w3cCapabilities || {}).firstMatch || [])) {
|
|
Object.assign(w3cSettings, pullSettings(firstMatchEntry));
|
|
}
|
|
|
|
let protocol;
|
|
let innerSessionId, dCaps;
|
|
try {
|
|
// Parse the caps into a format that the InnerDriver will accept
|
|
const parsedCaps = parseCapsForInnerDriver(
|
|
jsonwpCaps,
|
|
w3cCapabilities,
|
|
this.desiredCapConstraints,
|
|
defaultCapabilities
|
|
);
|
|
|
|
const {desiredCaps, processedJsonwpCapabilities, processedW3CCapabilities, error} = parsedCaps;
|
|
protocol = parsedCaps.protocol;
|
|
|
|
// If the parsing of the caps produced an error, throw it in here
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
|
|
const {
|
|
driver: InnerDriver,
|
|
version: driverVersion
|
|
} = this._findMatchingDriver(this.driverConfig, desiredCaps);
|
|
this.printNewSessionAnnouncement(InnerDriver.name, driverVersion, InnerDriver.baseVersion);
|
|
|
|
if (this.args.sessionOverride) {
|
|
await this.deleteAllSessions();
|
|
}
|
|
|
|
let runningDriversData, otherPendingDriversData;
|
|
const d = new InnerDriver(this.args);
|
|
|
|
// We want to assign security values directly on the driver. The driver
|
|
// should not read security values from `this.opts` because those values
|
|
// could have been set by a malicious user via capabilities, whereas we
|
|
// want a guarantee the values were set by the appium server admin
|
|
if (this.args.relaxedSecurityEnabled) {
|
|
log.info(`Applying relaxed security to '${InnerDriver.name}' as per ` +
|
|
`server command line argument. All insecure features will be ` +
|
|
`enabled unless explicitly disabled by --deny-insecure`);
|
|
d.relaxedSecurityEnabled = true;
|
|
}
|
|
|
|
if (!_.isEmpty(this.args.denyInsecure)) {
|
|
log.info('Explicitly preventing use of insecure features:');
|
|
this.args.denyInsecure.map((a) => log.info(` ${a}`));
|
|
d.denyInsecure = this.args.denyInsecure;
|
|
}
|
|
|
|
if (!_.isEmpty(this.args.allowInsecure)) {
|
|
log.info('Explicitly enabling use of insecure features:');
|
|
this.args.allowInsecure.map((a) => log.info(` ${a}`));
|
|
d.allowInsecure = this.args.allowInsecure;
|
|
}
|
|
|
|
// This assignment is required for correct web sockets functionality inside the driver
|
|
d.server = this.server;
|
|
try {
|
|
runningDriversData = await this.curSessionDataForDriver(InnerDriver);
|
|
} catch (e) {
|
|
throw new errors.SessionNotCreatedError(e.message);
|
|
}
|
|
await pendingDriversGuard.acquire(AppiumDriver.name, () => {
|
|
this.pendingDrivers[InnerDriver.name] = this.pendingDrivers[InnerDriver.name] || [];
|
|
otherPendingDriversData = this.pendingDrivers[InnerDriver.name].map((drv) => drv.driverData);
|
|
this.pendingDrivers[InnerDriver.name].push(d);
|
|
});
|
|
|
|
try {
|
|
[innerSessionId, dCaps] = await d.createSession(
|
|
processedJsonwpCapabilities,
|
|
reqCaps,
|
|
processedW3CCapabilities,
|
|
[...runningDriversData, ...otherPendingDriversData]
|
|
);
|
|
protocol = d.protocol;
|
|
await sessionsListGuard.acquire(AppiumDriver.name, () => {
|
|
this.sessions[innerSessionId] = d;
|
|
});
|
|
} finally {
|
|
await pendingDriversGuard.acquire(AppiumDriver.name, () => {
|
|
_.pull(this.pendingDrivers[InnerDriver.name], d);
|
|
});
|
|
}
|
|
|
|
this.attachUnexpectedShutdownHandler(d, innerSessionId);
|
|
|
|
log.info(`New ${InnerDriver.name} session created successfully, session ` +
|
|
`${innerSessionId} added to master session list`);
|
|
|
|
// set the New Command Timeout for the inner driver
|
|
d.startNewCommandTimeout();
|
|
|
|
// apply initial values to Appium settings (if provided)
|
|
if (d.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
|
|
log.info(`Applying the initial values to Appium settings parsed from W3C caps: ` +
|
|
JSON.stringify(w3cSettings));
|
|
await d.updateSettings(w3cSettings);
|
|
} else if (d.isMjsonwpProtocol() && !_.isEmpty(jwpSettings)) {
|
|
log.info(`Applying the initial values to Appium settings parsed from MJSONWP caps: ` +
|
|
JSON.stringify(jwpSettings));
|
|
await d.updateSettings(jwpSettings);
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
protocol,
|
|
error,
|
|
};
|
|
}
|
|
|
|
return {
|
|
protocol,
|
|
value: [innerSessionId, dCaps, protocol]
|
|
};
|
|
}
|
|
|
|
attachUnexpectedShutdownHandler (driver, innerSessionId) {
|
|
const removeSessionFromMasterList = (cause = new Error('Unknown error')) => {
|
|
log.warn(`Closing session, cause was '${cause.message}'`);
|
|
log.info(`Removing session '${innerSessionId}' from our master session list`);
|
|
delete this.sessions[innerSessionId];
|
|
delete this.sessionPlugins[innerSessionId];
|
|
};
|
|
|
|
// eslint-disable-next-line promise/prefer-await-to-then
|
|
if (_.isFunction((driver.onUnexpectedShutdown || {}).then)) {
|
|
// TODO: Remove this block after all the drivers use base driver above v 5.0.0
|
|
// Remove the session on unexpected shutdown, so that we are in a position
|
|
// to open another session later on.
|
|
driver.onUnexpectedShutdown
|
|
// eslint-disable-next-line promise/prefer-await-to-then
|
|
.then(() => {
|
|
// if we get here, we've had an unexpected shutdown, so error
|
|
throw new Error('Unexpected shutdown');
|
|
})
|
|
.catch((e) => {
|
|
// if we cancelled the unexpected shutdown promise, that means we
|
|
// no longer care about it, and can safely ignore it
|
|
if (!(e instanceof B.CancellationError)) {
|
|
removeSessionFromMasterList(e);
|
|
}
|
|
}); // this is a cancellable promise
|
|
} else if (_.isFunction(driver.onUnexpectedShutdown)) {
|
|
// since base driver v 5.0.0
|
|
driver.onUnexpectedShutdown(removeSessionFromMasterList);
|
|
} else {
|
|
log.warn(`Failed to attach the unexpected shutdown listener. ` +
|
|
`Is 'onUnexpectedShutdown' method available for '${driver.constructor.name}'?`);
|
|
}
|
|
}
|
|
|
|
async curSessionDataForDriver (InnerDriver) {
|
|
const sessions = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions);
|
|
const data = _.values(sessions)
|
|
.filter((s) => s.constructor.name === InnerDriver.name)
|
|
.map((s) => s.driverData);
|
|
for (let datum of data) {
|
|
if (!datum) {
|
|
throw new Error(`Problem getting session data for driver type ` +
|
|
`${InnerDriver.name}; does it implement 'get ` +
|
|
`driverData'?`);
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
|
|
async deleteSession (sessionId) {
|
|
let protocol;
|
|
try {
|
|
let otherSessionsData = null;
|
|
let dstSession = null;
|
|
await sessionsListGuard.acquire(AppiumDriver.name, () => {
|
|
if (!this.sessions[sessionId]) {
|
|
return;
|
|
}
|
|
const curConstructorName = this.sessions[sessionId].constructor.name;
|
|
otherSessionsData = _.toPairs(this.sessions)
|
|
.filter(([key, value]) => value.constructor.name === curConstructorName && key !== sessionId)
|
|
.map(([, value]) => value.driverData);
|
|
dstSession = this.sessions[sessionId];
|
|
protocol = dstSession.protocol;
|
|
log.info(`Removing session ${sessionId} from our master session list`);
|
|
// regardless of whether the deleteSession completes successfully or not
|
|
// make the session unavailable, because who knows what state it might
|
|
// be in otherwise
|
|
delete this.sessions[sessionId];
|
|
delete this.sessionPlugins[sessionId];
|
|
});
|
|
return {
|
|
protocol,
|
|
value: await dstSession.deleteSession(sessionId, otherSessionsData),
|
|
};
|
|
} catch (e) {
|
|
log.error(`Had trouble ending session ${sessionId}: ${e.message}`);
|
|
return {
|
|
protocol,
|
|
error: e,
|
|
};
|
|
}
|
|
}
|
|
|
|
async deleteAllSessions (opts = {}) {
|
|
const sessionsCount = _.size(this.sessions);
|
|
if (0 === sessionsCount) {
|
|
log.debug('There are no active sessions for cleanup');
|
|
return;
|
|
}
|
|
|
|
const {
|
|
force = false,
|
|
reason,
|
|
} = opts;
|
|
log.debug(`Cleaning up ${util.pluralize('active session', sessionsCount, true)}`);
|
|
const cleanupPromises = force
|
|
? _.values(this.sessions).map((drv) => drv.startUnexpectedShutdown(reason && new Error(reason)))
|
|
: _.keys(this.sessions).map((id) => this.deleteSession(id));
|
|
for (const cleanupPromise of cleanupPromises) {
|
|
try {
|
|
await cleanupPromise;
|
|
} catch (e) {
|
|
log.debug(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the appropriate plugins for a session (or sessionless plugins)
|
|
*
|
|
* @param {?string} sessionId - the sessionId (or null) to use to find plugins
|
|
* @returns {Array} - array of plugin instances
|
|
*/
|
|
pluginsForSession (sessionId = null) {
|
|
let plugins = [];
|
|
if (sessionId) {
|
|
if (!this.sessionPlugins[sessionId]) {
|
|
this.sessionPlugins[sessionId] = this.createPluginInstances();
|
|
}
|
|
plugins = this.sessionPlugins[sessionId];
|
|
} else {
|
|
if (_.isEmpty(this.sessionlessPlugins)) {
|
|
this.sessionlessPlugins = this.createPluginInstances();
|
|
}
|
|
plugins = this.sessionlessPlugins;
|
|
}
|
|
return plugins;
|
|
}
|
|
|
|
/**
|
|
* To get plugins for a command, we either get the plugin instances associated with the
|
|
* particular command's session, or in the case of sessionless plugins, pull from the set of
|
|
* plugin instances reserved for sessionless commands (and we lazily create plugin instances on
|
|
* first use)
|
|
*
|
|
* @param {string} cmd - the name of the command to find a plugin to handle
|
|
* @param {?string} sessionId - the particular session for which to find a plugin, or null if
|
|
* sessionless
|
|
*/
|
|
pluginsToHandleCmd (cmd, sessionId = null) {
|
|
// to handle a given command, a plugin should either implement that command as a plugin
|
|
// instance method or it should implement a generic 'handle' method
|
|
return this.pluginsForSession(sessionId)
|
|
.filter((p) => _.isFunction(p[cmd]) || _.isFunction(p.handle));
|
|
}
|
|
|
|
createPluginInstances (sessionId) {
|
|
if (!sessionId) {
|
|
sessionId = 'sessionless';
|
|
}
|
|
sessionId = _.truncate(sessionId, {length: 11});
|
|
return this.pluginClasses.map((PluginClass) => {
|
|
const name = `${PluginClass.pluginName} (${sessionId})`;
|
|
return new PluginClass(name);
|
|
});
|
|
}
|
|
|
|
async executeCommand (cmd, ...args) {
|
|
// We have basically three cases for how to handle commands:
|
|
// 1. handle getStatus (we do this as a special out of band case so it doesn't get added to an
|
|
// execution queue, and can be called while e.g. createSession is in progress)
|
|
// 2. handle commands that this umbrella driver should handle, rather than the actual session
|
|
// driver (for example, deleteSession, or other non-session commands)
|
|
// 3. handle session driver commands.
|
|
// The tricky part is that because we support command plugins, we need to wrap any of these
|
|
// cases with plugin handling.
|
|
|
|
const isGetStatus = cmd === 'getStatus';
|
|
const isUmbrellaCmd = !isGetStatus && isAppiumDriverCommand(cmd);
|
|
const isSessionCmd = !isGetStatus && !isUmbrellaCmd;
|
|
|
|
// first do some error checking. If we're requesting a session command execution, then make
|
|
// sure that session actually exists on the session driver, and set the session driver itself
|
|
let sessionId = null;
|
|
let dstSession = null;
|
|
let protocol = null;
|
|
let driver = this;
|
|
if (isSessionCmd) {
|
|
sessionId = _.last(args);
|
|
dstSession = await sessionsListGuard.acquire(AppiumDriver.name, () => this.sessions[sessionId]);
|
|
if (!dstSession) {
|
|
throw new Error(`The session with id '${sessionId}' does not exist`);
|
|
}
|
|
// now save the response protocol given that the session driver's protocol might differ
|
|
protocol = dstSession.protocol;
|
|
driver = dstSession;
|
|
}
|
|
|
|
// get any plugins which are registered as handling this command
|
|
const plugins = this.pluginsToHandleCmd(cmd, sessionId);
|
|
|
|
// now we define a 'cmdHandledBy' object which will keep track of which plugins have handled this
|
|
// command. we care about this because (a) multiple plugins can handle the same command, and
|
|
// (b) there's no guarantee that a plugin will actually call the next() method which runs the
|
|
// original command execution. This results in a situation where the command might be handled
|
|
// by some but not all plugins, or by plugin(s) but not by the default behavior. So start out
|
|
// this object declaring that the default handler has not been executed.
|
|
const cmdHandledBy = {default: false};
|
|
|
|
// now we define an async function which will be passed to plugins, and successively wrapped
|
|
// if there is more than one plugin that can handle the command. To start off with, the async
|
|
// function is defined as calling the default behavior, i.e., whichever of the 3 cases above is
|
|
// the appropriate one
|
|
const defaultBehavior = async () => {
|
|
// if we're running with plugins, make sure we log that the default behavior is actually
|
|
// happening so we can tell when the plugin call chain is unwrapping to the default behavior
|
|
// if that's what happens
|
|
plugins.length && log.info(`Executing default handling behavior for command '${cmd}'`);
|
|
|
|
// if we make it here, we know that the default behavior is handled
|
|
cmdHandledBy.default = true;
|
|
|
|
if (isGetStatus) {
|
|
return await this.getStatus();
|
|
}
|
|
|
|
if (isUmbrellaCmd) {
|
|
// some commands, like deleteSession, we want to make sure to handle on *this* driver,
|
|
// not the platform driver
|
|
return await super.executeCommand(cmd, ...args);
|
|
}
|
|
|
|
// here we know that we are executing a session command, and have a valid session driver
|
|
return await dstSession.executeCommand(cmd, ...args);
|
|
};
|
|
|
|
// now take our default behavior, wrap it with any number of plugin behaviors, and run it
|
|
const wrappedCmd = this.wrapCommandWithPlugins({
|
|
driver, cmd, args, plugins, cmdHandledBy, next: defaultBehavior
|
|
});
|
|
const res = await this.executeWrappedCommand({wrappedCmd, protocol});
|
|
|
|
// if we had plugins, make sure to log out the helpful report about which plugins ended up
|
|
// handling the command and which didn't
|
|
plugins.length && this.logPluginHandlerReport({cmd, cmdHandledBy});
|
|
|
|
return res;
|
|
}
|
|
|
|
wrapCommandWithPlugins ({driver, cmd, args, next, cmdHandledBy, plugins}) {
|
|
plugins.length && log.info(`Plugins which can handle cmd '${cmd}': ${plugins.map((p) => p.name)}`);
|
|
|
|
// now we can go through each plugin and wrap `next` around its own handler, passing the *old*
|
|
// next in so that it can call it if it wants to
|
|
for (const plugin of plugins) {
|
|
// need an IIFE here because we want the value of next that's passed to plugin.handle to be
|
|
// exactly the value of next here before reassignment; we don't want it to be lazily
|
|
// evaluated, otherwise we end up with infinite recursion of the last `next` to be defined.
|
|
cmdHandledBy[plugin.name] = false; // we see a new plugin, so add it to the 'cmdHandledBy' object
|
|
next = ((_next) => async () => {
|
|
log.info(`Plugin ${plugin.name} is now handling cmd '${cmd}'`);
|
|
cmdHandledBy[plugin.name] = true; // if we make it here, this plugin has attempted to handle cmd
|
|
// first attempt to handle the command via a command-specific handler on the plugin
|
|
if (plugin[cmd]) {
|
|
return await plugin[cmd](_next, driver, ...args);
|
|
}
|
|
// otherwise, call the generic 'handle' method
|
|
return await plugin.handle(_next, driver, cmd, ...args);
|
|
})(next);
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
logPluginHandlerReport ({cmd, cmdHandledBy}) {
|
|
// at the end of the day, we have an object representing which plugins ended up getting
|
|
// their code run as part of handling this command. Because plugins can choose *not* to
|
|
// pass control to other plugins or to the default driver behavior, this is information
|
|
// which is probably useful to the user (especially in situations where plugins might not
|
|
// interact well together, and it would be hard to debug otherwise without this kind of
|
|
// message).
|
|
const didHandle = Object.keys(cmdHandledBy).filter((k) => cmdHandledBy[k]);
|
|
const didntHandle = Object.keys(cmdHandledBy).filter((k) => !cmdHandledBy[k]);
|
|
if (didntHandle.length > 0) {
|
|
log.info(`Command '${cmd}' was not handled by the following beahviors or plugins, even ` +
|
|
`though they were registered to handle it: ${JSON.stringify(didntHandle)}. The ` +
|
|
`command *was* handled by these: ${JSON.stringify(didHandle)}.`);
|
|
}
|
|
}
|
|
|
|
async executeWrappedCommand ({wrappedCmd, protocol}) {
|
|
let cmdRes, cmdErr, res = {};
|
|
try {
|
|
// At this point, `wrappedCmd` defines a whole sequence of plugin handlers, culminating in
|
|
// our default handler. Whatever it returns is what we're going to want to send back to the
|
|
// user.
|
|
cmdRes = await wrappedCmd();
|
|
} catch (e) {
|
|
cmdErr = e;
|
|
}
|
|
|
|
// Sadly, we don't know exactly what kind of object will be returned. It will either be a bare
|
|
// object, or a protocol-aware object with protocol and error/value keys. So we need to sniff
|
|
// it and make sure we don't double-wrap it if it's the latter kind.
|
|
if (_.isPlainObject(cmdRes) && _.has(cmdRes, 'protocol')) {
|
|
res = cmdRes;
|
|
} else {
|
|
res.value = cmdRes;
|
|
res.error = cmdErr;
|
|
res.protocol = protocol;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
// We want to override basedriver's proxy avoidance detection since plugins might want to avoid
|
|
// proxying in order to handle a command, so we use this function to make an extra check whether
|
|
// plugins might want to do this.
|
|
proxyRouteIsAvoided (sessionId, method, url, body) {
|
|
if (super.proxyRouteIsAvoided(sessionId, method, url, body)) {
|
|
return true;
|
|
}
|
|
// if even just one plugin needs to avoid proxying, do so
|
|
for (const plugin of this.pluginsForSession(sessionId)) {
|
|
if (plugin.shouldAvoidProxy?.(method, url, body)) {
|
|
log.info(`Request to ${url} would normally be proxied, but plugin ${plugin.name} wants ` +
|
|
`to handle it internally. We'll avoid proxying in this case.`);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
proxyActive (sessionId) {
|
|
const dstSession = this.sessions[sessionId];
|
|
return dstSession && _.isFunction(dstSession.proxyActive) && dstSession.proxyActive(sessionId);
|
|
}
|
|
|
|
getProxyAvoidList (sessionId) {
|
|
const dstSession = this.sessions[sessionId];
|
|
return dstSession ? dstSession.getProxyAvoidList() : [];
|
|
}
|
|
|
|
canProxy (sessionId) {
|
|
const dstSession = this.sessions[sessionId];
|
|
return dstSession && dstSession.canProxy(sessionId);
|
|
}
|
|
}
|
|
|
|
// help decide which commands should be proxied to sub-drivers and which
|
|
// should be handled by this, our umbrella driver
|
|
function isAppiumDriverCommand (cmd) {
|
|
return !isSessionCommand(cmd) || cmd === 'deleteSession';
|
|
}
|
|
|
|
export { AppiumDriver };
|