UNPKG

appium-chromedriver

Version:
314 lines 12.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Chromedriver = void 0; const node_events_1 = __importDefault(require("node:events")); const base_driver_1 = require("@appium/base-driver"); const support_1 = require("@appium/support"); const teen_process_1 = require("teen_process"); const utils_1 = require("./utils"); const storage_client_1 = require("./storage-client/storage-client"); const constants_1 = require("./constants"); const binary_1 = require("./commands/binary"); const version_1 = require("./commands/version"); const process_1 = require("./commands/process"); const session_1 = require("./commands/session"); // Keep this import marked as used at runtime when it is otherwise only referenced in type positions. void base_driver_1.PROTOCOLS; const DEFAULT_HOST = '127.0.0.1'; const DEFAULT_PORT = 9515; // consider chromedriver ready once startup banner appears const chromedriverStdoutStartDetector = (stdout) => stdout.startsWith('Starting '); class Chromedriver extends node_events_1.default.EventEmitter { static EVENT_ERROR = constants_1.CHROMEDRIVER_EVENTS.ERROR; static EVENT_CHANGED = constants_1.CHROMEDRIVER_EVENTS.CHANGED; static STATE_STOPPED = constants_1.CHROMEDRIVER_STATES.STOPPED; static STATE_STARTING = constants_1.CHROMEDRIVER_STATES.STARTING; static STATE_ONLINE = constants_1.CHROMEDRIVER_STATES.ONLINE; static STATE_STOPPING = constants_1.CHROMEDRIVER_STATES.STOPPING; static STATE_RESTARTING = constants_1.CHROMEDRIVER_STATES.RESTARTING; proxyPort; adb; cmdArgs; proc; useSystemExecutable; chromedriver; executableDir; mappingPath; bundleId; executableVerified; state; _execFunc; jwproxy; isCustomExecutableDir; verbose; logPath; disableBuildCheck; storageClient; details; capabilities; _desiredProtocol; _driverVersion; _onlineStatus; _log; proxyHost; buildChromedriverArgs = process_1.buildChromedriverArgs; getDriversMapping = binary_1.getDriversMapping; getChromedrivers = binary_1.getChromedrivers; updateDriversMapping = binary_1.updateDriversMapping; getCompatibleChromedriver = binary_1.getCompatibleChromedriver; initChromedriverPath = binary_1.initChromedriverPath; getChromeVersion = version_1.getChromeVersionForAutodetection; syncProtocol = session_1.syncProtocol; waitForOnline = process_1.waitForOnline; getStatus = process_1.getStatus; killAll = process_1.killAll; changeState = session_1.changeState; startSession = session_1.startSession; constructor(args = {}) { super(); const { host = DEFAULT_HOST, port = DEFAULT_PORT, useSystemExecutable = false, executable, executableDir, bundleId, mappingPath, cmdArgs, adb, verbose, logPath, disableBuildCheck, details, isAutodownloadEnabled = false, reqBasePath, } = args; this._log = support_1.logger.getLogger((0, utils_1.generateLogPrefix)(this)); this.proxyHost = host; this.proxyPort = parseInt(String(port), 10); this.adb = adb; this.cmdArgs = cmdArgs; this.proc = null; this.useSystemExecutable = useSystemExecutable; this.chromedriver = executable; this.mappingPath = mappingPath; this.bundleId = bundleId; this.executableVerified = false; this.state = Chromedriver.STATE_STOPPED; this._execFunc = teen_process_1.exec; const proxyOpts = { server: this.proxyHost, port: this.proxyPort, log: this._log }; if (reqBasePath) { proxyOpts.reqBasePath = reqBasePath; } this.jwproxy = new base_driver_1.JWProxy(proxyOpts); if (executableDir) { this.executableDir = executableDir; this.isCustomExecutableDir = true; } else { this.executableDir = (0, utils_1.getChromedriverDir)(); this.isCustomExecutableDir = false; } this.verbose = verbose; this.logPath = logPath; this.disableBuildCheck = !!disableBuildCheck; this.storageClient = isAutodownloadEnabled ? new storage_client_1.ChromedriverStorageClient({ chromedriverDir: this.executableDir }) : null; this.details = details; this.capabilities = {}; this._desiredProtocol = null; this._driverVersion = null; this._onlineStatus = null; } get log() { return this._log; } get driverVersion() { return this._driverVersion; } /** * Starts a new Chromedriver session with the given capabilities. * * @param caps - Capabilities passed to Chromedriver session creation. * @param emitStartingState - Whether to emit the `starting` state transition. * @returns Session capabilities returned by Chromedriver. */ async start(caps, emitStartingState = true) { this.capabilities = this.prepareCapabilitiesForSessionStart(caps); if (emitStartingState) { this.changeState(Chromedriver.STATE_STARTING); } const args = this.buildChromedriverArgs(); const webviewVersionHolder = {}; try { await this.launchChromedriverProcess(args, webviewVersionHolder); this.syncProtocol(); return await this.startSession(); } catch (e) { return await this.handleChromedriverStartFailure(e, webviewVersionHolder.version); } } /** * Gets active Chromedriver session id if the driver is online. * * @returns The session id or `null` when driver is not online. */ sessionId() { return this.state === Chromedriver.STATE_ONLINE ? this.jwproxy.sessionId : null; } /** * Restarts current Chromedriver session with previously stored capabilities. * * @returns Session capabilities returned by the restarted session. */ async restart() { this.log.info('Restarting chromedriver'); if (this.state !== Chromedriver.STATE_ONLINE) { throw new Error("Can't restart when we're not online"); } this.changeState(Chromedriver.STATE_RESTARTING); await this.stop(false); return await this.start(this.capabilities, false); } /** * Stops the current Chromedriver session and underlying subprocess. * * @param emitStates - Whether to emit stopping/stopped state transitions. */ async stop(emitStates = true) { if (emitStates) { this.changeState(Chromedriver.STATE_STOPPING); } const runSafeStep = async (f) => { try { return await f(); } catch (e) { const err = e; this.log.warn(err.message); this.log.debug(err.stack); } }; await runSafeStep(() => this.jwproxy.command('', 'DELETE')); await runSafeStep(async () => { await this.proc?.stop('SIGTERM', 20000); this.proc?.removeAllListeners(); this.proc = null; }); this.log.prefix = (0, utils_1.generateLogPrefix)(this); if (emitStates) { this.changeState(Chromedriver.STATE_STOPPED); } } /** * Sends a direct command to Chromedriver through the JSONWP/W3C proxy. * * @param url - Chromedriver endpoint path. * @param method - HTTP method used for the command. * @param body - Optional request payload. * @returns Command response payload. */ async sendCommand(url, method, body = null) { return await this.jwproxy.command(url, method, body); } /** * Proxies an incoming Express request/response pair to Chromedriver. * * @param req - Incoming request object. * @param res - Outgoing response object. */ async proxyReq(req, res) { await this.jwproxy.proxyReqRes(req, res); } /** * Checks whether the active webview connection is currently responsive. * * @returns `true` if `/url` command succeeds, otherwise `false`. */ async hasWorkingWebview() { try { await this.jwproxy.command('/url', 'GET'); return true; } catch { return false; } } prepareCapabilitiesForSessionStart(caps) { const capabilities = structuredClone(caps); // set the logging preferences to ALL browser console logs by default capabilities.loggingPrefs = structuredClone((0, session_1.getCapValue)(caps, 'loggingPrefs', {})); if (support_1.util.isEmpty(capabilities.loggingPrefs.browser)) { capabilities.loggingPrefs.browser = 'ALL'; } return capabilities; } attachChromedriverProcessListeners(webviewVersionHolder) { const proc = this.proc; if (!proc) { throw new Error('Chromedriver subprocess must be assigned before attaching listeners'); } for (const streamName of ['stderr', 'stdout']) { proc.on(`line-${streamName}`, (line) => { // if chromedriver does not print explicit Chrome version support, // infer webview version from DevTools banner for better errors if (!webviewVersionHolder.version) { const match = /"Browser": "([^"]+)"/.exec(line); if (match) { webviewVersionHolder.version = match[1]; this.log.debug(`Webview version: '${webviewVersionHolder.version}'`); } } if (this.verbose) { this.log.debug(`[${streamName.toUpperCase()}] ${line}`); } }); } proc.once('exit', (code, signal) => { this._driverVersion = null; this._desiredProtocol = null; this._onlineStatus = null; if (this.state !== Chromedriver.STATE_STOPPED && this.state !== Chromedriver.STATE_STOPPING && this.state !== Chromedriver.STATE_RESTARTING) { this.log.error(`Chromedriver exited unexpectedly with code ${code}, signal ${signal}`); this.changeState(Chromedriver.STATE_STOPPED); } this.proc?.removeAllListeners(); this.proc = null; }); } async launchChromedriverProcess(args, webviewVersionHolder) { const chromedriverPath = await this.initChromedriverPath(); // remove stale chromedriver/adb-forward leftovers before launching await this.killAll(); this.proc = new teen_process_1.SubProcess(chromedriverPath, args); this.attachChromedriverProcessListeners(webviewVersionHolder); this.log.info(`Spawning Chromedriver with: ${this.chromedriver} ${args.join(' ')}`); await this.proc.start(chromedriverStdoutStartDetector); // wait until /status says ready, then negotiate protocol and start session await this.waitForOnline(); } formatChromeVersionMismatchHint(err, webviewVersion) { if (!err.message.includes('Chrome version must be')) { return ''; } // enrich the common version-mismatch error with actionable context let message = 'Unable to automate Chrome version because it is not supported by this version of Chromedriver.\n'; if (webviewVersion) { message += `Chrome version on the device: ${webviewVersion}\n`; } const versionsSupportedByDriver = /Chrome version must be (.+)/.exec(err.message)?.[1] || ''; if (versionsSupportedByDriver) { message += `Chromedriver supports Chrome version(s): ${versionsSupportedByDriver}\n`; } message += 'Check the driver tutorial for troubleshooting.\n'; return message; } async handleChromedriverStartFailure(err, webviewVersion) { this.log.debug(err); this.emit(Chromedriver.EVENT_ERROR, err); // an error does not always mean subprocess has already exited if (this.proc) { try { await this.proc.stop(); } catch { } } this.proc?.removeAllListeners(); this.proc = null; const message = this.formatChromeVersionMismatchHint(err, webviewVersion) + err.message; throw new Error(message); } } exports.Chromedriver = Chromedriver; //# sourceMappingURL=chromedriver.js.map