From 93ecc5727e1f3086ca9e1972f580fda6f451d35b Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Thu, 23 Jan 2025 12:03:21 +0100 Subject: [PATCH] feat: Determine chromedriver version from the /status API output --- lib/chromedriver.js | 124 +++++++++++++++++--------------------------- 1 file changed, 49 insertions(+), 75 deletions(-) diff --git a/lib/chromedriver.js b/lib/chromedriver.js index b67d4b1d..62f7c067 100644 --- a/lib/chromedriver.js +++ b/lib/chromedriver.js @@ -27,7 +27,6 @@ const CHROME_BUNDLE_ID = 'com.android.chrome'; const WEBVIEW_SHELL_BUNDLE_ID = 'org.chromium.webview_shell'; const WEBVIEW_BUNDLE_IDS = ['com.google.android.webview', 'com.android.webview']; const VERSION_PATTERN = /([\d.]+)/; -const WEBDRIVER_VERSION_PATTERN = /Starting (ChromeDriver|Microsoft Edge WebDriver) ([.\d]+)/; const CD_VERSION_TIMEOUT = 5000; @@ -92,17 +91,25 @@ export class Chromedriver extends events.EventEmitter { this.details = details; /** @type {any} */ this.capabilities = {}; - /** @type {keyof PROTOCOLS} */ - this.desiredProtocol = PROTOCOLS.MJSONWP; + /** @type {keyof PROTOCOLS | null} */ + this._desiredProtocol = null; // Store the running driver version - this.driverVersion = /** @type {string|null} */ null; + /** @type {string|null} */ + this._driverVersion = null; } get log() { return this._log; } + /** + * @returns {string | null} + */ + get driverVersion() { + return this._driverVersion; + } + async getDriversMapping() { let mapping = _.cloneDeep(CHROMEDRIVER_CHROME_MAPPING); if (this.mappingPath) { @@ -513,25 +520,23 @@ export class Chromedriver extends events.EventEmitter { /** * Sync the WebDriver protocol if current on going protocol is W3C or MJSONWP. - * Does nothing if this.driverVersion is null. + * Does nothing if driverVersion is null. * - * @returns {typeof PROTOCOLS[keyof typeof PROTOCOLS]} + * @returns {keyof PROTOCOLS} */ syncProtocol() { - if (!this.driverVersion) { - // Keep the default protocol if the driverVersion was unsure. - return this.desiredProtocol; + if (this.driverVersion) { + const coercedVersion = semver.coerce(this.driverVersion); + if (!coercedVersion || coercedVersion.major < MIN_CD_VERSION_WITH_W3C_SUPPORT) { + this.log.info( + `The ChromeDriver v. ${this.driverVersion} does not fully support ${PROTOCOLS.W3C} protocol. ` + + `Defaulting to ${PROTOCOLS.MJSONWP}`, + ); + this._desiredProtocol = PROTOCOLS.MJSONWP; + return this._desiredProtocol; + } } - this.desiredProtocol = PROTOCOLS.MJSONWP; - const coercedVersion = semver.coerce(this.driverVersion); - if (!coercedVersion || coercedVersion.major < MIN_CD_VERSION_WITH_W3C_SUPPORT) { - this.log.info( - `The ChromeDriver v. ${this.driverVersion} does not fully support ${PROTOCOLS.W3C} protocol. ` + - `Defaulting to ${PROTOCOLS.MJSONWP}`, - ); - return this.desiredProtocol; - } // Check only chromeOptions for now. const chromeOptions = getCapValue(this.capabilities, 'chromeOptions', {}); if (chromeOptions.w3c === false) { @@ -539,45 +544,12 @@ export class Chromedriver extends events.EventEmitter { `The ChromeDriver v. ${this.driverVersion} supports ${PROTOCOLS.W3C} protocol, ` + `but ${PROTOCOLS.MJSONWP} one has been explicitly requested`, ); - return this.desiredProtocol; + this._desiredProtocol = PROTOCOLS.MJSONWP; + return this._desiredProtocol; } - this.desiredProtocol = PROTOCOLS.W3C; - this.log.info(`Set ChromeDriver communication protocol to ${PROTOCOLS.W3C}`); - return this.desiredProtocol; - } - - /** - * Sync the protocol by reading the given output - * - * @param {string} line The output of ChromeDriver process - * @returns {typeof PROTOCOLS[keyof typeof PROTOCOLS] | null} - */ - detectWebDriverProtocol(line) { - if (this.driverVersion) { - return this.syncProtocol(); - } - - // also print chromedriver version to logs - // will output something like - // Starting ChromeDriver 2.33.506106 (8a06c39c4582fbfbab6966dbb1c38a9173bfb1a2) on port 9515 - // Or MSEdge: - // Starting Microsoft Edge WebDriver 111.0.1661.41 (57be51b50d1be232a9e8186a10017d9e06b1fd16) on port 9515 - const match = WEBDRIVER_VERSION_PATTERN.exec(line); - if (match && match.length === 3) { - this.log.debug(`${match[1]} version: '${match[2]}'`); - this.driverVersion = match[2]; - try { - return this.syncProtocol(); - } catch (e) { - this.driverVersion = null; - this.log.error(`Stopping the chromedriver process. Cannot determinate the protocol: ${e}`); - this.stop(); - } - // Does not print else condition log since the log could be - // very noisy when this.verbose option is true. - } - return null; + this._desiredProtocol = PROTOCOLS.W3C; + return this._desiredProtocol; } /** @@ -619,7 +591,6 @@ export class Chromedriver extends events.EventEmitter { let processIsAlive = false; /** @type {string|undefined} */ let webviewVersion; - let didDetectProtocol = false; try { const chromedriverPath = await this.initChromedriverPath(); await this.killAll(); @@ -648,17 +619,6 @@ export class Chromedriver extends events.EventEmitter { } } - if (!didDetectProtocol) { - const proto = this.detectWebDriverProtocol(line); - if (proto === PROTOCOLS.W3C) { - // given caps might not be properly prefixed - // so we try to fix them in order to properly init - // the new W3C session - this.capabilities = toW3cCapNames(this.capabilities); - } - didDetectProtocol = true; - } - if (this.verbose) { // give the output if it is requested this.log.debug(`[${streamName.toUpperCase()}] ${line}`); @@ -668,7 +628,8 @@ export class Chromedriver extends events.EventEmitter { // handle out-of-bound exit by simply emitting a stopped state this.proc.once('exit', (code, signal) => { - this.driverVersion = null; + this._driverVersion = null; + this._desiredProtocol = null; processIsAlive = false; if ( this.state !== Chromedriver.STATE_STOPPED && @@ -682,10 +643,11 @@ export class Chromedriver extends events.EventEmitter { this.proc?.removeAllListeners(); this.proc = null; }); - this.log.info(`Spawning chromedriver with: ${this.chromedriver} ${args.join(' ')}`); + this.log.info(`Spawning Chromedriver with: ${this.chromedriver} ${args.join(' ')}`); // start subproc and wait for startDetector await this.proc.start(startDetector); await this.waitForOnline(); + this.syncProtocol(); return await this.startSession(); } catch (e) { const err = /** @type {Error} */ (e); @@ -743,7 +705,19 @@ export class Chromedriver extends events.EventEmitter { chromedriverStopped = true; return; } - await this.getStatus(); + /** @type {any} */ + const status = await this.getStatus(); + this.log.debug(JSON.stringify(status)); + if (!_.isPlainObject(status) || !status.ready) { + throw new Error(`The response to the /status API is not valid: ${JSON.stringify(status)}`); + } + const versionMatch = VERSION_PATTERN.exec(status.build?.version ?? ''); + if (versionMatch) { + this._driverVersion = versionMatch[1]; + this.log.info(`Chromedriver version: ${this._driverVersion}`); + } else { + this.log.info('Chromedriver version cannot be determined from the /status API response'); + } }); if (chromedriverStopped) { throw new Error('ChromeDriver crashed during startup.'); @@ -756,19 +730,19 @@ export class Chromedriver extends events.EventEmitter { async startSession() { const sessionCaps = - this.desiredProtocol === PROTOCOLS.W3C - ? {capabilities: {alwaysMatch: this.capabilities}} + this._desiredProtocol === PROTOCOLS.W3C + ? {capabilities: {alwaysMatch: toW3cCapNames(this.capabilities)}} : {desiredCapabilities: this.capabilities}; this.log.info( - `Starting ${this.desiredProtocol} Chromedriver session with capabilities: ` + + `Starting ${this._desiredProtocol} Chromedriver session with capabilities: ` + JSON.stringify(sessionCaps, null, 2), ); - const {capabilities} = /** @type {NewSessionResponse} */ ( + const response = /** @type {NewSessionResponse} */ ( await this.jwproxy.command('/session', 'POST', sessionCaps) ); this.log.prefix = generateLogPrefix(this, this.jwproxy.sessionId); this.changeState(Chromedriver.STATE_ONLINE); - return capabilities; + return _.has(response, 'capabilities') ? response.capabilities : response; } async stop(emitStates = true) {