Skip to content

Commit

Permalink
feat: Determine chromedriver version from the /status API output
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach committed Jan 23, 2025
1 parent 5a066ac commit 93ecc57
Showing 1 changed file with 49 additions and 75 deletions.
124 changes: 49 additions & 75 deletions lib/chromedriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -513,71 +520,36 @@ 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) {
this.log.info(
`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;
}

/**
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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}`);
Expand All @@ -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 &&
Expand All @@ -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);
Expand Down Expand Up @@ -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.');
Expand All @@ -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) {
Expand Down

0 comments on commit 93ecc57

Please sign in to comment.