appium-chromedriver
Version:
Node.js wrapper around chromedriver.
806 lines • 36.9 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
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 node_child_process_1 = __importDefault(require("node:child_process"));
const support_1 = require("@appium/support");
const asyncbox_1 = require("asyncbox");
const teen_process_1 = require("teen_process");
const bluebird_1 = __importDefault(require("bluebird"));
const utils_1 = require("./utils");
const semver = __importStar(require("semver"));
const lodash_1 = __importDefault(require("lodash"));
const node_path_1 = __importDefault(require("node:path"));
const compare_versions_1 = require("compare-versions");
const storage_client_1 = require("./storage-client/storage-client");
const protocol_helpers_1 = require("./protocol-helpers");
const NEW_CD_VERSION_FORMAT_MAJOR_VERSION = 73;
const DEFAULT_HOST = '127.0.0.1';
const MIN_CD_VERSION_WITH_W3C_SUPPORT = 75;
const DEFAULT_PORT = 9515;
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 CD_VERSION_TIMEOUT = 5000;
class Chromedriver extends node_events_1.default.EventEmitter {
static EVENT_ERROR = 'chromedriver_error';
static EVENT_CHANGED = 'stateChanged';
static STATE_STOPPED = 'stopped';
static STATE_STARTING = 'starting';
static STATE_ONLINE = 'online';
static STATE_STOPPING = 'stopping';
static STATE_RESTARTING = 'restarting';
_log;
proxyHost;
proxyPort;
adb;
cmdArgs;
proc;
useSystemExecutable;
chromedriver;
executableDir;
mappingPath;
bundleId;
executableVerified;
state;
_execFunc;
jwproxy;
isCustomExecutableDir;
verbose;
logPath;
disableBuildCheck;
storageClient;
details;
capabilities;
_desiredProtocol;
_driverVersion;
_onlineStatus;
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;
// to mock in unit test
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) {
// Expects the user set the executable directory explicitly
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;
// Store the running driver version
this._driverVersion = null;
this._onlineStatus = null;
}
/**
* Gets the logger instance for this Chromedriver instance.
* @returns The logger instance.
*/
get log() {
return this._log;
}
/**
* Gets the version of the currently running Chromedriver.
* @returns The driver version string, or null if not yet determined.
*/
get driverVersion() {
return this._driverVersion;
}
/**
* Starts a new Chromedriver session with the given capabilities.
* @param caps - The session capabilities to use.
* @param emitStartingState - Whether to emit the starting state event (default: true).
* @returns A promise that resolves to the session capabilities returned by Chromedriver.
* @throws {Error} If Chromedriver fails to start or crashes during startup.
*/
async start(caps, emitStartingState = true) {
this.capabilities = lodash_1.default.cloneDeep(caps);
// set the logging preferences to ALL the console logs
this.capabilities.loggingPrefs = lodash_1.default.cloneDeep((0, protocol_helpers_1.getCapValue)(caps, 'loggingPrefs', {}));
if (lodash_1.default.isEmpty(this.capabilities.loggingPrefs.browser)) {
this.capabilities.loggingPrefs.browser = 'ALL';
}
if (emitStartingState) {
this.changeState(Chromedriver.STATE_STARTING);
}
const args = this.buildChromedriverArgs();
// what are the process stdout/stderr conditions wherein we know that
// the process has started to our satisfaction?
const startDetector = (stdout) => stdout.startsWith('Starting ');
let processIsAlive = false;
let webviewVersion;
try {
const chromedriverPath = await this.initChromedriverPath();
await this.killAll();
// set up our subprocess object
this.proc = new teen_process_1.SubProcess(chromedriverPath, args);
processIsAlive = true;
// handle log output
for (const streamName of ['stderr', 'stdout']) {
this.proc.on(`line-${streamName}`, (line) => {
// if the cd output is not printed, find the chrome version and print
// will get a response like
// DevTools response: {
// "Android-Package": "io.appium.sampleapp",
// "Browser": "Chrome/55.0.2883.91",
// "Protocol-Version": "1.2",
// "User-Agent": "...",
// "WebKit-Version": "537.36"
// }
if (!webviewVersion) {
const match = /"Browser": "([^"]+)"/.exec(line);
if (match) {
webviewVersion = match[1];
this.log.debug(`Webview version: '${webviewVersion}'`);
}
}
if (this.verbose) {
// give the output if it is requested
this.log.debug(`[${streamName.toUpperCase()}] ${line}`);
}
});
}
// handle out-of-bound exit by simply emitting a stopped state
this.proc.once('exit', (code, signal) => {
this._driverVersion = null;
this._desiredProtocol = null;
this._onlineStatus = null;
processIsAlive = false;
if (this.state !== Chromedriver.STATE_STOPPED &&
this.state !== Chromedriver.STATE_STOPPING &&
this.state !== Chromedriver.STATE_RESTARTING) {
const msg = `Chromedriver exited unexpectedly with code ${code}, signal ${signal}`;
this.log.error(msg);
this.changeState(Chromedriver.STATE_STOPPED);
}
this.proc?.removeAllListeners();
this.proc = null;
});
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 = e;
this.log.debug(err);
this.emit(Chromedriver.EVENT_ERROR, err);
// just because we had an error doesn't mean the chromedriver process
// finished; we should clean up if necessary
if (processIsAlive) {
await this.proc?.stop();
}
this.proc?.removeAllListeners();
this.proc = null;
let message = '';
// often the user's Chrome version is not supported by the version of Chromedriver
if (err.message.includes('Chrome version must be')) {
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';
}
message += err.message;
throw this.log.errorWithException(message);
}
}
/**
* Gets the current session ID if the driver is online.
* @returns The session ID string, or null if the driver is not online.
*/
sessionId() {
return this.state === Chromedriver.STATE_ONLINE ? this.jwproxy.sessionId : null;
}
/**
* Restarts the Chromedriver session.
* The session will be stopped and then started again with the same capabilities.
* @returns A promise that resolves to the session capabilities returned by Chromedriver.
* @throws {Error} If the driver is not online or if restart fails.
*/
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 Chromedriver session and terminates the process.
* @param emitStates - Whether to emit state change events during shutdown (default: true).
* @returns A promise that resolves when the session has been stopped.
*/
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(() => {
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 command to the Chromedriver server.
* @param url - The endpoint URL (e.g., '/url', '/session').
* @param method - The HTTP method to use ('POST', 'GET', or 'DELETE').
* @param body - Optional request body for POST requests.
* @returns A promise that resolves to the response from Chromedriver.
*/
async sendCommand(url, method, body = null) {
return await this.jwproxy.command(url, method, body);
}
/**
* Proxies an HTTP request/response to the Chromedriver server.
* @param req - The incoming HTTP request object.
* @param res - The outgoing HTTP response object.
* @returns A promise that resolves when the proxying is complete.
*/
async proxyReq(req, res) {
await this.jwproxy.proxyReqRes(req, res);
}
/**
* Checks if Chromedriver is currently able to automate webviews.
* Sometimes Chromedriver stops automating webviews; this method runs a simple
* command to determine the current state.
* @returns A promise that resolves to true if webviews are working, false otherwise.
*/
async hasWorkingWebview() {
try {
await this.jwproxy.command('/url', 'GET');
return true;
}
catch {
return false;
}
}
// Private methods at the tail of the class
buildChromedriverArgs() {
const args = [`--port=${this.proxyPort}`];
if (this.adb?.adbPort) {
args.push(`--adb-port=${this.adb.adbPort}`);
}
if (lodash_1.default.isArray(this.cmdArgs)) {
args.push(...this.cmdArgs);
}
if (this.logPath) {
args.push(`--log-path=${this.logPath}`);
}
if (this.disableBuildCheck) {
args.push('--disable-build-check');
}
args.push('--verbose');
return args;
}
async getDriversMapping() {
let mapping = lodash_1.default.cloneDeep(utils_1.CHROMEDRIVER_CHROME_MAPPING);
if (this.mappingPath) {
this.log.debug(`Attempting to use Chromedriver->Chrome mapping from '${this.mappingPath}'`);
if (!(await support_1.fs.exists(this.mappingPath))) {
this.log.warn(`No file found at '${this.mappingPath}'`);
this.log.info('Defaulting to the static Chromedriver->Chrome mapping');
}
else {
try {
mapping = JSON.parse(await support_1.fs.readFile(this.mappingPath, 'utf8'));
}
catch (e) {
const err = e;
this.log.warn(`Error parsing mapping from '${this.mappingPath}': ${err.message}`);
this.log.info('Defaulting to the static Chromedriver->Chrome mapping');
}
}
}
else {
this.log.debug('Using the static Chromedriver->Chrome mapping');
}
// make sure that the values for minimum chrome version are semver compliant
for (const [cdVersion, chromeVersion] of lodash_1.default.toPairs(mapping)) {
const coercedVersion = semver.coerce(chromeVersion);
if (coercedVersion) {
mapping[cdVersion] = coercedVersion.version;
}
else {
this.log.info(`'${chromeVersion}' is not a valid version number. Skipping it`);
}
}
return mapping;
}
async getChromedrivers(mapping) {
// go through the versions available
const executables = await support_1.fs.glob('*', {
cwd: this.executableDir,
nodir: true,
absolute: true,
});
this.log.debug(`Found ${support_1.util.pluralize('executable', executables.length, true)} ` +
`in '${this.executableDir}'`);
const cds = (await (0, asyncbox_1.asyncmap)(executables, async (executable) => {
const logError = ({ message, stdout, stderr, }) => {
let errMsg = `Cannot retrieve version number from '${node_path_1.default.basename(executable)}' Chromedriver binary. ` +
`Make sure it returns a valid version string in response to '--version' command line argument. ${message}`;
if (stdout) {
errMsg += `\nStdout: ${stdout}`;
}
if (stderr) {
errMsg += `\nStderr: ${stderr}`;
}
this.log.warn(errMsg);
return null;
};
let stdout;
let stderr;
try {
({ stdout, stderr } = await this._execFunc(executable, ['--version'], {
timeout: CD_VERSION_TIMEOUT,
}));
}
catch (e) {
const err = e;
if (!(err.message || '').includes('timed out') &&
!(err.stdout || '').includes('Starting ChromeDriver')) {
return logError(err);
}
// if this has timed out, it has actually started Chromedriver,
// in which case there will also be the version string in the output
stdout = err.stdout;
}
const match = /ChromeDriver\s+\(?v?([\d.]+)\)?/i.exec(stdout); // https://regex101.com/r/zpj5wA/1
if (!match) {
return logError({ message: 'Cannot parse the version string', stdout, stderr });
}
let version = match[1];
let minChromeVersion = mapping[version] || null;
const coercedVersion = semver.coerce(version);
if (coercedVersion) {
// before 2019-03-06 versions were of the form major.minor
if (coercedVersion.major < NEW_CD_VERSION_FORMAT_MAJOR_VERSION) {
version = `${coercedVersion.major}.${coercedVersion.minor}`;
minChromeVersion = mapping[version] || null;
}
if (!minChromeVersion && coercedVersion.major >= NEW_CD_VERSION_FORMAT_MAJOR_VERSION) {
// Assume the major Chrome version is the same as the corresponding driver major version
minChromeVersion = `${coercedVersion.major}`;
}
}
return {
executable,
version,
minChromeVersion,
};
}))
.filter((cd) => !!cd)
.sort((a, b) => (0, compare_versions_1.compareVersions)(b.version, a.version));
if (lodash_1.default.isEmpty(cds)) {
this.log.info(`No Chromedrivers were found in '${this.executableDir}'`);
return cds;
}
this.log.debug(`The following Chromedriver executables were found:`);
for (const cd of cds) {
this.log.debug(` '${cd.executable}' (version '${cd.version}', minimum Chrome version '${cd.minChromeVersion ? cd.minChromeVersion : 'Unknown'}')`);
}
return cds;
}
async getChromeVersion() {
// Try to retrieve the version from `details` property if it is set
// The `info` item must contain the output of /json/version CDP command
// where `Browser` field looks like `Chrome/72.0.3601.0``
if (this.details?.info) {
this.log.debug(`Browser version in the supplied details: ${this.details?.info?.Browser}`);
}
const versionMatch = VERSION_PATTERN.exec(this.details?.info?.Browser ?? '');
if (versionMatch) {
const coercedVersion = semver.coerce(versionMatch[1]);
if (coercedVersion) {
return coercedVersion;
}
}
let chromeVersion;
// in case of WebView Browser Tester, simply try to find the underlying webview
if (this.bundleId === WEBVIEW_SHELL_BUNDLE_ID) {
if (this.adb) {
for (const bundleId of WEBVIEW_BUNDLE_IDS) {
chromeVersion = await (0, utils_1.getChromeVersion)(this.adb, bundleId);
if (chromeVersion) {
this.bundleId = bundleId;
return semver.coerce(chromeVersion);
}
}
}
return null;
}
// on Android 7-9 webviews are backed by the main Chrome, not the system webview
if (this.adb) {
const apiLevel = await this.adb.getApiLevel();
if (apiLevel >= 24 &&
apiLevel <= 28 &&
[WEBVIEW_SHELL_BUNDLE_ID, ...WEBVIEW_BUNDLE_IDS].includes(this.bundleId ?? '')) {
this.bundleId = CHROME_BUNDLE_ID;
}
}
// try out webviews when no bundle id is sent in
if (!this.bundleId) {
// default to the generic Chrome bundle
this.bundleId = CHROME_BUNDLE_ID;
// we have a webview of some sort, so try to find the bundle version
for (const bundleId of WEBVIEW_BUNDLE_IDS) {
if (this.adb) {
chromeVersion = await (0, utils_1.getChromeVersion)(this.adb, bundleId);
if (chromeVersion) {
this.bundleId = bundleId;
break;
}
}
}
}
// if we do not have a chrome version, it must not be a webview
if (!chromeVersion && this.adb) {
chromeVersion = await (0, utils_1.getChromeVersion)(this.adb, this.bundleId);
}
// make sure it is semver, so later checks won't fail
return chromeVersion ? semver.coerce(chromeVersion) : null;
}
async updateDriversMapping(newMapping) {
let shouldUpdateStaticMapping = true;
if (!this.mappingPath) {
this.log.warn('No mapping path provided');
return;
}
if (await support_1.fs.exists(this.mappingPath)) {
try {
await support_1.fs.writeFile(this.mappingPath, JSON.stringify(newMapping, null, 2), 'utf8');
shouldUpdateStaticMapping = false;
}
catch (e) {
const err = e;
this.log.warn(`Cannot store the updated chromedrivers mapping into '${this.mappingPath}'. ` +
`This may reduce the performance of further executions. Original error: ${err.message}`);
}
}
if (shouldUpdateStaticMapping) {
Object.assign(utils_1.CHROMEDRIVER_CHROME_MAPPING, newMapping);
}
}
async getCompatibleChromedriver() {
if (!this.adb && !this.isCustomExecutableDir) {
return await (0, utils_1.getChromedriverBinaryPath)();
}
const mapping = await this.getDriversMapping();
if (!lodash_1.default.isEmpty(mapping)) {
this.log.debug(`The most recent known Chrome version: ${lodash_1.default.values(mapping)[0]}`);
}
let didStorageSync = false;
const syncChromedrivers = async (chromeVersion) => {
didStorageSync = true;
if (!this.storageClient) {
return false;
}
const retrievedMapping = await this.storageClient.retrieveMapping();
this.log.debug('Got chromedrivers mapping from the storage: ' +
lodash_1.default.truncate(JSON.stringify(retrievedMapping, null, 2), { length: 500 }));
const driverKeys = await this.storageClient.syncDrivers({
minBrowserVersion: chromeVersion.major,
});
if (lodash_1.default.isEmpty(driverKeys)) {
return false;
}
const synchronizedDriversMapping = driverKeys.reduce((acc, x) => {
const { version, minBrowserVersion } = retrievedMapping[x];
acc[version] = minBrowserVersion;
return acc;
}, {});
Object.assign(mapping, synchronizedDriversMapping);
await this.updateDriversMapping(mapping);
return true;
};
while (true) {
const cds = await this.getChromedrivers(mapping);
const missingVersions = {};
for (const { version, minChromeVersion } of cds) {
if (!minChromeVersion || mapping[version]) {
continue;
}
const coercedVer = semver.coerce(version);
if (!coercedVer || coercedVer.major < NEW_CD_VERSION_FORMAT_MAJOR_VERSION) {
continue;
}
missingVersions[version] = minChromeVersion;
}
if (!lodash_1.default.isEmpty(missingVersions)) {
this.log.info(`Found ${support_1.util.pluralize('Chromedriver', lodash_1.default.size(missingVersions), true)}, ` +
`which ${lodash_1.default.size(missingVersions) === 1 ? 'is' : 'are'} missing in the list of known versions: ` +
JSON.stringify(missingVersions));
await this.updateDriversMapping(Object.assign(mapping, missingVersions));
}
if (this.disableBuildCheck) {
if (lodash_1.default.isEmpty(cds)) {
throw this.log.errorWithException(`There must be at least one Chromedriver executable available for use if ` +
`'chromedriverDisableBuildCheck' capability is set to 'true'`);
}
const { version, executable } = cds[0];
this.log.warn(`Chrome build check disabled. Using most recent Chromedriver version (${version}, at '${executable}')`);
this.log.warn(`If this is wrong, set 'chromedriverDisableBuildCheck' capability to 'false'`);
return executable;
}
const chromeVersion = await this.getChromeVersion();
if (!chromeVersion) {
// unable to get the chrome version
if (lodash_1.default.isEmpty(cds)) {
throw this.log.errorWithException(`There must be at least one Chromedriver executable available for use if ` +
`the current Chrome version cannot be determined`);
}
const { version, executable } = cds[0];
this.log.warn(`Unable to discover Chrome version. Using Chromedriver ${version} at '${executable}'`);
return executable;
}
this.log.debug(`Found Chrome bundle '${this.bundleId}' version '${chromeVersion}'`);
const matchingDrivers = cds.filter(({ minChromeVersion }) => {
const minChromeVersionS = minChromeVersion && semver.coerce(minChromeVersion);
if (!minChromeVersionS) {
return false;
}
return chromeVersion.major > NEW_CD_VERSION_FORMAT_MAJOR_VERSION
? minChromeVersionS.major === chromeVersion.major
: semver.gte(chromeVersion, minChromeVersionS);
});
if (lodash_1.default.isEmpty(matchingDrivers)) {
if (this.storageClient && !didStorageSync) {
try {
if (await syncChromedrivers(chromeVersion)) {
continue;
}
}
catch (e) {
const err = e;
this.log.warn(`Cannot synchronize local chromedrivers with the remote storage: ${err.message}`);
this.log.debug(err.stack);
}
}
const autodownloadSuggestion = 'You could also try to enable automated chromedrivers download as ' +
'a possible workaround.';
throw new Error(`No Chromedriver found that can automate Chrome '${chromeVersion}'.` +
(this.storageClient ? '' : ` ${autodownloadSuggestion}`));
}
const binPath = matchingDrivers[0].executable;
this.log.debug(`Found ${support_1.util.pluralize('executable', matchingDrivers.length, true)} ` +
`capable of automating Chrome '${chromeVersion}'.\nChoosing the most recent, '${binPath}'.`);
this.log.debug(`If a specific version is required, specify it with the 'chromedriverExecutable'` +
` capability.`);
return binPath;
}
}
async initChromedriverPath() {
if (this.executableVerified && this.chromedriver) {
return this.chromedriver;
}
let chromedriver = this.chromedriver;
// the executable might be set (if passed in)
// or we might want to use the basic one installed with this driver
// or we want to figure out the best one
if (!chromedriver) {
chromedriver = this.chromedriver = this.useSystemExecutable
? await (0, utils_1.getChromedriverBinaryPath)()
: await this.getCompatibleChromedriver();
}
if (!(await support_1.fs.exists(chromedriver))) {
throw new Error(`Trying to use a chromedriver binary at the path ` +
`${chromedriver}, but it doesn't exist!`);
}
this.executableVerified = true;
this.log.info(`Set chromedriver binary as: ${chromedriver}`);
return chromedriver;
}
syncProtocol() {
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 ${base_driver_1.PROTOCOLS.W3C} protocol. ` +
`Defaulting to ${base_driver_1.PROTOCOLS.MJSONWP}`);
this._desiredProtocol = base_driver_1.PROTOCOLS.MJSONWP;
return this._desiredProtocol;
}
}
const isOperaDriver = lodash_1.default.includes(this._onlineStatus?.message, 'OperaDriver');
const chromeOptions = (0, protocol_helpers_1.getCapValue)(this.capabilities, 'chromeOptions');
if (lodash_1.default.isPlainObject(chromeOptions) && chromeOptions.w3c === false) {
this.log.info(`The ChromeDriver v. ${this.driverVersion} supports ${base_driver_1.PROTOCOLS.W3C} protocol, ` +
`but ${base_driver_1.PROTOCOLS.MJSONWP} one has been explicitly requested`);
this._desiredProtocol = base_driver_1.PROTOCOLS.MJSONWP;
return this._desiredProtocol;
}
else if (isOperaDriver) {
// OperaDriver needs the W3C protocol to be requested explicitly,
// otherwise it defaults to JWP
if (lodash_1.default.isPlainObject(chromeOptions)) {
chromeOptions.w3c = true;
}
else {
this.capabilities[(0, protocol_helpers_1.toW3cCapName)('chromeOptions')] = { w3c: true };
}
}
this._desiredProtocol = base_driver_1.PROTOCOLS.W3C;
return this._desiredProtocol;
}
async waitForOnline() {
// we need to make sure that CD hasn't crashed
let chromedriverStopped = false;
await (0, asyncbox_1.retryInterval)(20, 200, async () => {
if (this.state === Chromedriver.STATE_STOPPED) {
// we are either stopped or stopping, so something went wrong
chromedriverStopped = true;
return;
}
const status = await this.getStatus();
if (!lodash_1.default.isPlainObject(status) || !status.ready) {
throw new Error(`The response to the /status API is not valid: ${JSON.stringify(status)}`);
}
this._onlineStatus = 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.');
}
}
async getStatus() {
return await this.jwproxy.command('/status', 'GET');
}
async startSession() {
const sessionCaps = this._desiredProtocol === base_driver_1.PROTOCOLS.W3C
? { capabilities: { alwaysMatch: (0, protocol_helpers_1.toW3cCapNames)(this.capabilities) } }
: { desiredCapabilities: this.capabilities };
this.log.info(`Starting ${this._desiredProtocol} Chromedriver session with capabilities: ` +
JSON.stringify(sessionCaps, null, 2));
const response = (await this.jwproxy.command('/session', 'POST', sessionCaps));
this.log.prefix = (0, utils_1.generateLogPrefix)(this, this.jwproxy.sessionId);
this.changeState(Chromedriver.STATE_ONLINE);
return lodash_1.default.has(response, 'capabilities') && response.capabilities
? response.capabilities
: response;
}
changeState(state) {
this.state = state;
this.log.debug(`Changed state to '${state}'`);
this.emit(Chromedriver.EVENT_CHANGED, { state });
}
async killAll() {
const cmd = support_1.system.isWindows()
? `wmic process where "commandline like '%chromedriver.exe%--port=${this.proxyPort}%'" delete`
: `pkill -15 -f "${this.chromedriver}.*--port=${this.proxyPort}"`;
this.log.debug(`Killing any old chromedrivers, running: ${cmd}`);
try {
await bluebird_1.default.promisify(node_child_process_1.default.exec)(cmd);
this.log.debug('Successfully cleaned up old chromedrivers');
}
catch {
this.log.warn('No old chromedrivers seem to exist');
}
if (this.adb) {
const udidIndex = this.adb.executable.defaultArgs.findIndex((item) => item === '-s');
const udid = udidIndex > -1 ? this.adb.executable.defaultArgs[udidIndex + 1] : null;
if (udid) {
this.log.debug(`Cleaning this device's adb forwarded port socket connections: ${udid}`);
}
else {
this.log.debug(`Cleaning any old adb forwarded port socket connections`);
}
try {
for (const conn of await this.adb.getForwardList()) {
// chromedriver will ask ADB to forward a port like "deviceId tcp:port localabstract:webview_devtools_remote_port"
if (!(conn.includes('webview_devtools') && (!udid || conn.includes(udid)))) {
continue;
}
const params = conn.split(/\s+/);
if (params.length > 1) {
await this.adb.removePortForward(params[1].replace(/[\D]*/, ''));
}
}
}
catch (e) {
const err = e;
this.log.warn(`Unable to clean forwarded ports. Error: '${err.message}'. Continuing.`);
}
}
}
}
exports.Chromedriver = Chromedriver;
//# sourceMappingURL=chromedriver.js.map