mirror of
https://github.com/appium/appium.git
synced 2026-10-29 12:41:58 +08:00
231 lines
8.1 KiB
JavaScript
231 lines
8.1 KiB
JavaScript
import { MobileJsonWireProtocol,
|
|
errors } from 'mobile-json-wire-protocol';
|
|
import os from 'os';
|
|
import commands from './commands';
|
|
import helpers from './helpers';
|
|
import log from './logger';
|
|
import { desiredCapabilityConstraints, validator } from './desired-caps';
|
|
import B from 'bluebird';
|
|
import _ from 'lodash';
|
|
|
|
validator.promise = B;
|
|
validator.prettify = (val) => { return val; };
|
|
|
|
const NEW_COMMAND_TIMEOUT_MS = 60 * 1000;
|
|
|
|
class BaseDriver extends MobileJsonWireProtocol {
|
|
|
|
constructor (opts = {}, shouldValidateCaps = true) {
|
|
super();
|
|
this.sessionId = null;
|
|
this.opts = opts;
|
|
this.caps = null;
|
|
this.helpers = helpers;
|
|
this.newCommandTimeoutMs = NEW_COMMAND_TIMEOUT_MS;
|
|
this.implicitWaitMs = 0;
|
|
this._constraints = _.cloneDeep(desiredCapabilityConstraints);
|
|
this.locatorStrategies = [];
|
|
this.webLocatorStrategies = [];
|
|
|
|
// use a custom tmp dir to avoid losing data and app when computer is
|
|
// restarted
|
|
this.opts.tmpDir = this.opts.tmpDir ||
|
|
process.env.APPIUM_TMP_DIR ||
|
|
os.tmpDir();
|
|
|
|
// base-driver internals
|
|
this.curCommand = new B((r) => { r(); }); // see note in execute
|
|
this.curCommandCancellable = new B((r) => { r(); }); // see note in execute
|
|
this.shutdownUnexpectedly = false;
|
|
this.noCommandTimer = null;
|
|
this.shouldValidateCaps = shouldValidateCaps;
|
|
// settings should be instantiated by implementing drivers
|
|
this.settings = null;
|
|
this.resetOnUnexpectedShutdown();
|
|
|
|
// keeping track of initial opts
|
|
this.initialOpts = _.cloneDeep(this.opts);
|
|
}
|
|
|
|
/*
|
|
* Initialize a new onUnexpectedShutdown promise, cancelling existing one.
|
|
*/
|
|
resetOnUnexpectedShutdown() {
|
|
// TODO: use positive logic (i.e. onShutdown)
|
|
if (this.onUnexpectedShutdown && !this.onUnexpectedShutdown.isFulfilled()) {
|
|
this.onUnexpectedShutdown.cancel();
|
|
}
|
|
this.onUnexpectedShutdown = new B((resolve, reject) => {
|
|
this.unexpectedShutdownDeferred = {resolve, reject};
|
|
}).cancellable();
|
|
// noop handler to avoid warning.
|
|
this.onUnexpectedShutdown.catch(() => {});
|
|
}
|
|
|
|
// we only want subclasses to ever extend the contraints
|
|
set desiredCapConstraints (constraints) {
|
|
this._constraints = Object.assign(this._constraints, constraints);
|
|
}
|
|
|
|
get desiredCapConstraints () {
|
|
return this._constraints;
|
|
}
|
|
|
|
// method required by MJSONWP in order to determine whether it should
|
|
// respond with an invalid session response
|
|
sessionExists (sessionId) {
|
|
if (!sessionId) return false;
|
|
return sessionId === this.sessionId;
|
|
}
|
|
|
|
logExtraCaps (caps) {
|
|
let extraCaps = _.difference(_.keys(caps),
|
|
_.keys(this._constraints));
|
|
if (extraCaps.length) {
|
|
log.warn(`The following capabilities were provided, but are not ` +
|
|
`recognized by appium: ${extraCaps.join(', ')}.`);
|
|
}
|
|
}
|
|
|
|
validateDesiredCaps (caps) {
|
|
if (!this.shouldValidateCaps) {
|
|
return true;
|
|
}
|
|
|
|
let validationOptions = {fullMessages: false};
|
|
let validationErrors = validator.validate(caps,
|
|
this._constraints,
|
|
validationOptions);
|
|
|
|
if (validationErrors) {
|
|
let message = `The desiredCapabilities object was not valid for the ` +
|
|
`following reason(s):`;
|
|
for (let [attribute, reasons] of _.pairs(validationErrors)) {
|
|
for (let reason of reasons) {
|
|
message += ` ${attribute} ${reason},`;
|
|
}
|
|
}
|
|
message = message.slice(0, -1) + '.';
|
|
log.error(message);
|
|
throw new errors.SessionNotCreatedError(message);
|
|
}
|
|
|
|
this.logExtraCaps(caps);
|
|
|
|
return true;
|
|
}
|
|
|
|
// This is the main command handler for the driver. It wraps command
|
|
// execution with timeout logic, checking that we have a valid session,
|
|
// and ensuring that we execute commands one at a time. This method is called
|
|
// by MJSONWP's express router.
|
|
async executeCommand (cmd, ...args) {
|
|
// if we had a command timer running, clear it now that we're starting
|
|
// a new command and so don't want to time out
|
|
this.clearNewCommandTimeout();
|
|
|
|
// if we don't have this command, it must not be implemented
|
|
if (!this[cmd]) {
|
|
throw new errors.NotYetImplementedError();
|
|
}
|
|
|
|
// What we're doing here is pretty clever. this.curCommand is always
|
|
// a promise representing the command currently being executed by the
|
|
// driver, or the last command executed by the driver (it starts off as
|
|
// essentially a pre-resolved promise). When a command comes in, we tack it
|
|
// to the end of this.curCommand, essentially saying we want to execute it
|
|
// whenever this.curCommand is done. We call this new promise nextCommand,
|
|
// and its resolution is what we ultimately will return to whomever called
|
|
// us. Meanwhile, we reset this.curCommand to _be_ nextCommand (but
|
|
// ignoring any rejections), so that if another command comes into the
|
|
// server, it gets tacked on to the end of nextCommand. Thus we create
|
|
// a chain of promises that acts as a queue with single concurrency.
|
|
let nextCommand = this.curCommand.then(() => {
|
|
// if we unexpectedly shut down, we need to reject every command in
|
|
// the queue before we actually try to run it
|
|
if (this.shutdownUnexpectedly) {
|
|
return B.reject(new errors.NoSuchDriverError("The driver was unexpectedly shut down!"));
|
|
}
|
|
// We also need to turn the command into a cancellable promise so if we
|
|
// have an unexpected shutdown event, for example, we can cancel it from
|
|
// outside, rejecting the current command immediately
|
|
this.curCommandCancellable = new B((r) => { r(); }).then(() => {
|
|
return this[cmd](...args);
|
|
}).cancellable();
|
|
return this.curCommandCancellable;
|
|
});
|
|
this.curCommand = nextCommand.catch(() => {});
|
|
let res = await nextCommand;
|
|
|
|
// if we have set a new command timeout (which is the default), start a
|
|
// timer once we've finished executing this command. If we don't clear
|
|
// the timer (which is done when a new command comes in), we will trigger
|
|
// automatic session deletion in this.onCommandTimeout. Of course we don't
|
|
// want to trigger the timer when the user is shutting down the session
|
|
// intentionally
|
|
if (this.newCommandTimeoutMs && cmd !== 'deleteSession') {
|
|
// reseting existing timeout
|
|
this.clearNewCommandTimeout();
|
|
this.startNewCommandTimeout(async () => {
|
|
log.warn(`Shutting down because we waited ` +
|
|
`${this.newCommandTimeoutMs / 1000} seconds for a command`);
|
|
await this.startUnexpectedShutdown(new Error(`Command ${cmd} timed out!`));
|
|
});
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
async startUnexpectedShutdown (err = null) {
|
|
if (err === null) {
|
|
err = new errors.NoSuchDriverError("The driver was unexpectedly shut down!");
|
|
}
|
|
this.unexpectedShutdownDeferred.reject(err); // allow others to listen for this
|
|
this.curCommandCancellable.cancel(err);
|
|
this.shutdownUnexpectedly = true;
|
|
await this.deleteSession();
|
|
this.shutdownUnexpectedly = false;
|
|
}
|
|
|
|
validateLocatorStrategy (strategy, webContext = false) {
|
|
|
|
let validStrats = this.locatorStrategies;
|
|
|
|
if (webContext) {
|
|
validStrats = validStrats.concat(this.webLocatorStrategies);
|
|
}
|
|
|
|
if (!_.contains(validStrats, strategy)) {
|
|
throw new errors.InvalidSelectorError(`Locator Strategy "${strategy}" is not supported for this session`);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Restart the session with the original caps,
|
|
* preserving the timeout config.
|
|
*/
|
|
async reset () {
|
|
log.debug("Resetting app mid-session");
|
|
log.debug("Running generic full reset");
|
|
let currentConfig = {};
|
|
for (let k of ['implicitWaitMs', 'newCommandTimeoutMs', 'sessionId']) {
|
|
currentConfig[k] = this[k];
|
|
}
|
|
await this.deleteSession();
|
|
log.debug("Restarting app");
|
|
await this.createSession(this.caps);
|
|
|
|
for (let [k,v] of _.pairs(currentConfig)) {
|
|
this[k] = v;
|
|
}
|
|
this.clearNewCommandTimeout();
|
|
}
|
|
|
|
}
|
|
|
|
for (let [cmd, fn] of _.pairs(commands)) {
|
|
BaseDriver.prototype[cmd] = fn;
|
|
}
|
|
|
|
export { BaseDriver };
|