appium-geckodriver
Version:
Appium driver for Gecko-based browsers and web views
255 lines • 9.52 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GeckoDriverServer = exports.GeckoProxy = exports.GECKO_SERVER_HOST = void 0;
const lodash_1 = __importDefault(require("lodash"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const driver_1 = require("appium/driver");
const support_1 = require("appium/support");
const teen_process_1 = require("teen_process");
const asyncbox_1 = require("asyncbox");
const portscanner_1 = require("portscanner");
const child_process_1 = require("child_process");
const constants_1 = require("./constants");
const GD_BINARY = `geckodriver${support_1.system.isWindows() ? '.exe' : ''}`;
const STARTUP_TIMEOUT_MS = 10000; // 10 seconds
const GECKO_PORT_RANGE = [5200, 5300];
const GECKO_SERVER_GUARD = support_1.util.getLockFileGuard(path_1.default.resolve(os_1.default.tmpdir(), 'gecko_server_guard.lock'), { timeout: 5, tryRecovery: true });
const DEFAULT_MARIONETTE_PORT = 2828;
const PROCESS_SPECIFIC_OPTION_NAMES_MAP = Object.freeze({
noReset: 'noReset',
verbosity: 'verbosity',
androidStorage: 'androidStorage',
marionettePort: 'marionettePort',
systemPort: 'port',
});
exports.GECKO_SERVER_HOST = '127.0.0.1';
class GeckoProxy extends driver_1.JWProxy {
async proxyCommand(url, method, body = null) {
if (this.didProcessExit) {
throw new driver_1.errors.InvalidContextError(`'${method} ${url}' cannot be proxied to Gecko Driver server because ` +
'its process is not running (probably crashed). Check the Appium log for more details');
}
return await super.proxyCommand(url, method, body);
}
}
exports.GeckoProxy = GeckoProxy;
class GeckoDriverProcess {
/**
*
* @param {import('@appium/types').AppiumLogger} log
* @param {import('@appium/types').StringRecord} opts
*/
constructor(log, opts = {}) {
for (const [optName, propName] of lodash_1.default.toPairs(PROCESS_SPECIFIC_OPTION_NAMES_MAP)) {
this[propName] = opts[optName];
}
this.log = log;
this.proc = null;
}
get isRunning() {
return !!(this.proc?.isRunning);
}
async init() {
if (this.isRunning) {
return;
}
if (!this.port) {
await GECKO_SERVER_GUARD(async () => {
const [startPort, endPort] = GECKO_PORT_RANGE;
try {
this.port = await (0, portscanner_1.findAPortNotInUse)(startPort, endPort);
}
catch {
throw new Error(`Cannot find any free port in range ${startPort}..${endPort}. ` +
`Double check the processes that are locking ports within this range and terminate ` +
`these which are not needed anymore or set any free port number to the 'systemPort' capability`);
}
});
}
let driverBin;
try {
driverBin = await support_1.fs.which(GD_BINARY);
}
catch {
throw new Error(`${GD_BINARY} binary cannot be found in PATH. ` +
`Please make sure it is present on your system`);
}
/** @type {string[]} */
const args = [];
/* #region Options */
switch (lodash_1.default.toLower(this.verbosity)) {
case constants_1.VERBOSITY.DEBUG:
args.push('-v');
break;
case constants_1.VERBOSITY.TRACE:
args.push('-vv');
break;
}
if (this.noReset) {
args.push('--connect-existing');
// https://firefox-source-docs.mozilla.org/testing/geckodriver/Flags.html#code-connect-existing-code
if (lodash_1.default.isNil(this.marionettePort)) {
this.log.info(`'marionettePort' capability value is not provided while 'noReset' is enabled`);
this.log.info(`Assigning 'marionettePort' to the default value (${DEFAULT_MARIONETTE_PORT})`);
}
args.push('--marionette-port', `${this.marionettePort ?? DEFAULT_MARIONETTE_PORT}`);
}
else if (!lodash_1.default.isNil(this.marionettePort)) {
args.push('--marionette-port', `${this.marionettePort}`);
}
/* #endregion */
args.push('-p', `${this.port}`);
if (this.androidStorage) {
args.push('--android-storage', this.androidStorage);
}
this.proc = new teen_process_1.SubProcess(driverBin, args);
this.proc.on('output', (stdout, stderr) => {
const line = lodash_1.default.trim(stdout || stderr);
if (line) {
this.log.debug(`[${GD_BINARY}] ${line}`);
}
});
this.proc.on('exit', (code, signal) => {
this.log.info(`${GD_BINARY} has exited with code ${code}, signal ${signal}`);
});
this.log.info(`Starting '${driverBin}' with args ${JSON.stringify(args)}`);
await this.proc.start(0);
}
async stop() {
if (this.isRunning) {
await this.proc?.stop('SIGTERM');
}
}
async kill() {
if (this.isRunning) {
try {
await this.proc?.stop('SIGKILL');
}
catch { }
}
}
}
const RUNNING_PROCESS_IDS = [];
process.once('exit', () => {
if (lodash_1.default.isEmpty(RUNNING_PROCESS_IDS)) {
return;
}
const command = support_1.system.isWindows()
? ('taskkill.exe ' + RUNNING_PROCESS_IDS.map((pid) => `/PID ${pid}`).join(' '))
: `kill ${RUNNING_PROCESS_IDS.join(' ')}`;
try {
(0, child_process_1.execSync)(command);
}
catch { }
});
class GeckoDriverServer {
/**
*
* @param {import('@appium/types').AppiumLogger} log
* @param {import('@appium/types').StringRecord} caps
*/
constructor(log, caps) {
this.process = new GeckoDriverProcess(log, caps);
this.log = log;
// @ts-ignore That's ok
this.proxy = null;
}
get isRunning() {
return !!(this.process?.isRunning);
}
/**
*
* @param {import('@appium/types').StringRecord} geckoCaps
* @param {SessionOptions} [opts={}]
* @returns {Promise<import('@appium/types').StringRecord>}
*/
async start(geckoCaps, opts = {}) {
await this.process.init();
const proxyOpts = {
server: exports.GECKO_SERVER_HOST,
port: this.process.port,
log: this.log,
base: '',
keepAlive: true,
};
if (opts.reqBasePath) {
proxyOpts.reqBasePath = opts.reqBasePath;
}
this.proxy = new GeckoProxy(proxyOpts);
this.proxy.didProcessExit = false;
this.process?.proc?.on('exit', () => {
if (this.proxy) {
this.proxy.didProcessExit = true;
}
});
try {
await (0, asyncbox_1.waitForCondition)(async () => {
try {
await this.proxy?.command('/status', 'GET');
return true;
}
catch (err) {
if (this.proxy?.didProcessExit) {
throw new Error(err.message);
}
return false;
}
}, {
waitMs: STARTUP_TIMEOUT_MS,
intervalMs: 1000,
});
}
catch (e) {
if (this.process.isRunning) {
// avoid "frozen" processes,
await this.process.kill();
}
if (/Condition unmet/.test(e.message)) {
throw new Error(`Gecko Driver server is not listening within ${STARTUP_TIMEOUT_MS}ms timeout. ` +
`Make sure it could be started manually from a terminal`);
}
throw e;
}
const pid = this.process.proc?.pid;
RUNNING_PROCESS_IDS.push(pid);
this.process.proc?.on('exit', () => void lodash_1.default.pull(RUNNING_PROCESS_IDS, pid));
return /** @type {import('@appium/types').StringRecord} */ (await this.proxy.command('/session', 'POST', {
capabilities: {
firstMatch: [{}],
alwaysMatch: geckoCaps,
}
}));
}
async stop() {
if (!this.isRunning) {
this.log.info(`Gecko Driver session cannot be stopped, because the server is not running`);
return;
}
if (this.proxy?.sessionId) {
try {
await this.proxy.command(`/session/${this.proxy.sessionId}`, 'DELETE');
}
catch (e) {
this.log.info(`Gecko Driver session cannot be deleted. Original error: ${e.message}`);
}
}
try {
await this.process.stop();
}
catch (e) {
this.log.warn(`Gecko Driver process cannot be stopped (${e.message}). Killing it forcefully`);
await this.process.kill();
}
}
}
exports.GeckoDriverServer = GeckoDriverServer;
exports.default = GeckoDriverServer;
/**
* @typedef {Object} SessionOptions
* @property {string} [reqBasePath]
*/
//# sourceMappingURL=gecko.js.map