@appium/base-driver
Version:
Base driver class for Appium drivers
389 lines • 17.5 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseDriver = void 0;
const support_1 = require("@appium/support");
const types_1 = require("@appium/types");
const bluebird_1 = __importDefault(require("bluebird"));
const lodash_1 = __importDefault(require("lodash"));
const capabilities_1 = require("../helpers/capabilities");
const levenshtein_match_1 = require("../helpers/levenshtein-match");
const session_1 = require("../helpers/session");
const protocol_1 = require("../protocol");
const capabilities_2 = require("./capabilities");
const core_1 = require("./core");
const helpers = __importStar(require("./helpers"));
const extension_command_name_1 = require("../helpers/extension-command-name");
const EVENT_SESSION_INIT = 'newSessionRequested';
const EVENT_SESSION_START = 'newSessionStarted';
const EVENT_SESSION_QUIT_START = 'quitSessionRequested';
const EVENT_SESSION_QUIT_DONE = 'quitSessionFinished';
const ON_UNEXPECTED_SHUTDOWN_EVENT = 'onUnexpectedShutdown';
class BaseDriver extends core_1.DriverCore {
cliArgs;
caps;
originalCaps;
desiredCapConstraints;
server;
serverHost;
serverPort;
serverPath;
constructor(opts, shouldValidateCaps = true) {
super(opts, shouldValidateCaps);
this.caps = {};
this.cliArgs = {};
}
/**
* Contains the base constraints plus whatever the subclass wants to add.
*
* Subclasses _shouldn't_ need to use this. If you need to use this, please create
* an issue:
* @see {@link https://github.com/appium/appium/issues/new}
*/
get _desiredCapConstraints() {
return Object.freeze(lodash_1.default.merge({}, types_1.BASE_DESIRED_CAP_CONSTRAINTS, this.desiredCapConstraints));
}
/**
* 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) {
// get start time for this command, and log in special cases
const startTime = Date.now();
if (cmd === 'createSession') {
// If creating a session determine if W3C or MJSONWP protocol was requested and remember the choice
this.protocol = (0, protocol_1.determineProtocol)(args);
this.logEvent(EVENT_SESSION_INIT);
}
else if (cmd === protocol_1.DELETE_SESSION_COMMAND) {
this.logEvent(EVENT_SESSION_QUIT_START);
}
// 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
await this.clearNewCommandTimeout();
if (this.shutdownUnexpectedly) {
throw new protocol_1.errors.NoSuchDriverError('The driver was unexpectedly shut down!');
}
// If we don't have this command, it must not be implemented
if (!this[cmd]) {
await this.startNewCommandTimeout();
throw new protocol_1.errors.NotYetImplementedError();
}
const runCommandPromise = async () => {
let unexpectedShutdownRejecter = null;
let unexpectedShutdownResolver = null;
let wasSessionShutdownUnexpectedly = false;
const onUnexpectedShutdown = (e) => {
wasSessionShutdownUnexpectedly = true;
unexpectedShutdownRejecter?.(e);
};
try {
return await bluebird_1.default.race([
this[cmd](...args),
// This promise is needed to monitor if the session has been
// shut down unexpectedly while the command was running
new bluebird_1.default((resolve, reject) => {
unexpectedShutdownResolver = resolve;
unexpectedShutdownRejecter = reject;
this.eventEmitter.once(ON_UNEXPECTED_SHUTDOWN_EVENT, onUnexpectedShutdown);
})
]);
}
finally {
if (unexpectedShutdownRejecter && unexpectedShutdownResolver) {
// This is needed to prevent memory leaks
this.eventEmitter.removeListener(ON_UNEXPECTED_SHUTDOWN_EVENT, onUnexpectedShutdown);
unexpectedShutdownRejecter = null;
// @ts-ignore typescript cannot understand this
unexpectedShutdownResolver?.();
}
// 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 (!wasSessionShutdownUnexpectedly && this.isCommandsQueueEnabled && cmd !== protocol_1.DELETE_SESSION_COMMAND) {
// resetting existing timeout
await this.startNewCommandTimeout();
}
}
};
const synchronizationKey = BaseDriver.name;
// eslint-disable-next-line dot-notation
const commandsQueueLen = this.commandsQueueGuard['queues']?.[synchronizationKey]?.length ?? 0;
if (this.isCommandsQueueEnabled && commandsQueueLen > 0) {
this.log.debug(`Scheduling the '${cmd}' command to the ${this.constructor.name} commands queue. ` +
`${support_1.util.pluralize('queue item', commandsQueueLen, true)} ${commandsQueueLen === 1 ? 'is' : 'are'} ` +
`already waiting for execution.`);
}
const res = this.isCommandsQueueEnabled
? await this.commandsQueueGuard.acquire(synchronizationKey, runCommandPromise)
: await runCommandPromise();
// log timing information about this command
const endTime = Date.now();
if (this.clarifyCommandName) {
cmd = this.clarifyCommandName(cmd, args);
}
this._eventHistory.commands.push({ cmd, startTime, endTime });
if (cmd === 'createSession') {
this.logEvent(EVENT_SESSION_START);
}
else if (cmd === protocol_1.DELETE_SESSION_COMMAND) {
this.logEvent(EVENT_SESSION_QUIT_DONE);
}
return res;
}
clarifyCommandName(cmd, args) {
if (cmd === 'execute') {
const firstArg = args?.[0];
if (lodash_1.default.isString(firstArg) && firstArg.trim().length > 0) {
return extension_command_name_1.resolveExecuteExtensionName.call(this, firstArg);
}
}
return cmd;
}
async startUnexpectedShutdown(err = new protocol_1.errors.NoSuchDriverError('The driver was unexpectedly shut down!')) {
this.eventEmitter.emit(ON_UNEXPECTED_SHUTDOWN_EVENT, err); // allow others to listen for this
this.shutdownUnexpectedly = true;
try {
if (this.sessionId !== null) {
await this.deleteSession(this.sessionId);
}
}
finally {
this.shutdownUnexpectedly = false;
}
}
async startNewCommandTimeout() {
// make sure there are no rogue timeouts
await this.clearNewCommandTimeout();
// if command timeout is 0, it is disabled
if (!this.newCommandTimeoutMs)
return; // eslint-disable-line curly
this.noCommandTimer = setTimeout(async () => {
this.log.warn(`Shutting down because we waited ` +
`${this.newCommandTimeoutMs / 1000.0} seconds for a command`);
const errorMessage = `New Command Timeout of ` +
`${this.newCommandTimeoutMs / 1000.0} seconds ` +
`expired. Try customizing the timeout using the ` +
`'newCommandTimeout' desired capability`;
await this.startUnexpectedShutdown(new Error(errorMessage));
}, this.newCommandTimeoutMs);
}
assignServer(server, host, port, path) {
this.server = server;
this.serverHost = host;
this.serverPort = port;
this.serverPath = path;
}
/*
* Restart the session with the original caps,
* preserving the timeout config.
*/
async reset() {
this.log.debug('Resetting app mid-session');
this.log.debug('Running generic full reset');
// preserving state
const currentConfig = {};
for (const property of [
'implicitWaitMs',
'newCommandTimeoutMs',
'sessionId',
'resetOnUnexpectedShutdown',
]) {
currentConfig[property] = this[property];
}
try {
if (this.sessionId !== null) {
await this.deleteSession(this.sessionId);
}
this.log.debug('Restarting app');
await this.createSession(this.originalCaps);
}
finally {
// always restore state.
for (const [key, value] of lodash_1.default.toPairs(currentConfig)) {
this[key] = value;
}
}
await this.clearNewCommandTimeout();
}
/**
*
* Historically the first two arguments were reserved for JSONWP capabilities.
* Appium 2 has dropped the support of these, so now we only accept capability
* objects in W3C format and thus allow any of the three arguments to represent
* the latter.
*/
async createSession(w3cCapabilities1, w3cCapabilities2, w3cCapabilities,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
driverData) {
if (this.sessionId !== null) {
throw new protocol_1.errors.SessionNotCreatedError('Cannot create a new session while one is in progress');
}
this.log.debug();
const originalCaps = lodash_1.default.cloneDeep([w3cCapabilities, w3cCapabilities1, w3cCapabilities2].find(capabilities_1.isW3cCaps));
if (!originalCaps) {
throw new protocol_1.errors.SessionNotCreatedError('Appium only supports W3C-style capability objects. ' +
'Your client is sending an older capabilities format. Please update your client library.');
}
this.setProtocolW3C();
this.originalCaps = originalCaps;
this.log.debug(`Creating session with W3C capabilities: ${JSON.stringify(originalCaps, null, 2)}`);
let caps;
try {
caps = (0, capabilities_2.processCapabilities)(originalCaps, this._desiredCapConstraints, this.shouldValidateCaps);
caps = (0, capabilities_1.fixCaps)(caps, this._desiredCapConstraints, this.log);
}
catch (e) {
throw new protocol_1.errors.SessionNotCreatedError(e.message);
}
this.validateDesiredCaps(caps);
this.sessionId = support_1.util.uuidV4();
this.sessionCreationTimestampMs = Date.now();
this.caps = caps;
// merge caps onto opts so we don't need to worry about what's where
this.opts = { ...lodash_1.default.cloneDeep(this.initialOpts), ...this.caps };
// deal with resets
// some people like to do weird things by setting noReset and fullReset
// both to true, but this is misguided and strange, so error here instead
if (this.opts.noReset && this.opts.fullReset) {
throw new Error("The 'noReset' and 'fullReset' capabilities are mutually " +
'exclusive and should not both be set to true. You ' +
"probably meant to just use 'fullReset' on its own");
}
if (this.opts.noReset === true) {
this.opts.fullReset = false;
}
if (this.opts.fullReset === true) {
this.opts.noReset = false;
}
this.opts.fastReset = !this.opts.fullReset && !this.opts.noReset;
this.opts.skipUninstall = this.opts.fastReset || this.opts.noReset;
// Prevents empty string caps so we don't need to test it everywhere
if (typeof this.opts.app === 'string' && this.opts.app.trim() === '') {
delete this.opts.app;
}
if (!lodash_1.default.isUndefined(this.caps.newCommandTimeout)) {
this.newCommandTimeoutMs = this.caps.newCommandTimeout * 1000;
}
this._log.prefix = helpers.generateDriverLogPrefix(this);
this.log.updateAsyncContext({
sessionId: this.sessionId,
sessionSignature: (0, session_1.calcSignature)(this.sessionId),
});
this.log.info(`Session created with session id: ${this.sessionId}`);
return [this.sessionId, caps];
}
/**
* Returns capabilities for the session and event history (if applicable)
* @deprecated Use {@linkcode getAppiumSessionCapabilities} instead for getting the capabilities.
* Use {@linkcode EventCommands.getLogEvents} instead to get the event history.
*/
async getSession() {
return (this.caps.eventTimings ? { ...this.caps, events: this.eventHistory } : this.caps);
}
/**
* Returns capabilities for the session
*/
async getAppiumSessionCapabilities() {
return { capabilities: this.caps };
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async deleteSession(sessionId) {
await this.clearNewCommandTimeout();
if (this.isCommandsQueueEnabled && this.commandsQueueGuard.isBusy()) {
// simple hack to release pending commands if they exist
// @ts-expect-error private API
const queues = this.commandsQueueGuard.queues;
for (const key of lodash_1.default.keys(queues)) {
queues[key] = [];
}
}
this.sessionId = null;
}
logExtraCaps(caps) {
const knownCaps = lodash_1.default.keys(this._desiredCapConstraints);
const extraCaps = lodash_1.default.difference(lodash_1.default.keys(caps), knownCaps);
if (extraCaps.length) {
this.log.warn(`The following provided capabilities were not recognized by this driver:`);
for (const cap of extraCaps) {
const suggestion = (0, levenshtein_match_1.getLevenshteinSuggestion)(cap, knownCaps);
this.log.warn(suggestion
? ` ${cap} (did you mean '${suggestion}'?)`
: ` ${cap}`);
}
}
}
validateDesiredCaps(caps) {
if (!this.shouldValidateCaps) {
return true;
}
try {
(0, capabilities_2.validateCaps)(caps, this._desiredCapConstraints);
}
catch (e) {
throw this.log.errorWithException(new protocol_1.errors.SessionNotCreatedError(`Session capabilities were not valid for the ` +
`following reason(s): ${e.message}`, e));
}
this.logExtraCaps(caps);
return true;
}
async updateSettings(newSettings) {
if (!this.settings) {
throw this.log.errorWithException('Cannot update settings; settings object not found');
}
return await this.settings.update(newSettings);
}
async getSettings() {
if (!this.settings) {
throw this.log.errorWithException('Cannot get settings; settings object not found');
}
return this.settings.getSettings();
}
}
exports.BaseDriver = BaseDriver;
__exportStar(require("./commands"), exports);
exports.default = BaseDriver;
//# sourceMappingURL=driver.js.map