UNPKG

appium-geckodriver

Version:

Appium driver for Gecko-based browsers and web views

255 lines 9.52 kB
"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