appium-chromedriver
Version:
Node.js wrapper around chromedriver.
799 lines • 36.3 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 events_1 = __importDefault(require("events"));
const base_driver_1 = require("@appium/base-driver");
const child_process_1 = __importDefault(require("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 path_1 = __importDefault(require("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 events_1.default.EventEmitter {
/**
*
* @param {import('./types').ChromedriverOpts} args
*/
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 = port;
this.adb = adb;
this.cmdArgs = cmdArgs;
this.proc = null;
this.useSystemExecutable = useSystemExecutable;
this.chromedriver = executable;
this.executableDir = executableDir;
this.mappingPath = mappingPath;
this.bundleId = bundleId;
this.executableVerified = false;
this.state = Chromedriver.STATE_STOPPED;
/** @type {Record<string, any>} */
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 (this.executableDir) {
// Expects the user set the executable directory explicitly
this.isCustomExecutableDir = true;
}
else {
this.isCustomExecutableDir = false;
this.executableDir = (0, utils_1.getChromedriverDir)();
}
this.verbose = verbose;
this.logPath = logPath;
this.disableBuildCheck = !!disableBuildCheck;
this.storageClient = isAutodownloadEnabled
? new storage_client_1.ChromedriverStorageClient({ chromedriverDir: this.executableDir })
: null;
this.details = details;
/** @type {any} */
this.capabilities = {};
/** @type {keyof PROTOCOLS | null} */
this._desiredProtocol = null;
// Store the running driver version
/** @type {string|null} */
this._driverVersion = null;
/** @type {Record<string, any> | null} */
this._onlineStatus = null;
}
get log() {
return this._log;
}
/**
* @returns {string | null}
*/
get driverVersion() {
return this._driverVersion;
}
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 = /** @type {Error} */ (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;
}
/**
* @param {ChromedriverVersionMapping} 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) => {
/**
* @param {{message: string, stdout?: string, stderr?: string}} opts
*/
const logError = ({ message, stdout, stderr }) => {
let errMsg = `Cannot retrieve version number from '${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 (0, teen_process_1.exec)(executable, ['--version'], {
timeout: CD_VERSION_TIMEOUT,
}));
}
catch (e) {
const err = /** @type {import('teen_process').ExecError} */ (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];
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 = /** @type {keyof typeof mapping} */ (`${coercedVersion.major}.${coercedVersion.minor}`);
minChromeVersion = mapping[version];
}
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;
}
/**
*
* @param {ChromedriverVersionMapping} newMapping
* @returns {Promise<void>}
*/
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 = /** @type {Error} */ (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);
}
}
/**
* When executableDir is given explicitly for non-adb environment,
* this method will respect the executableDir rather than the system installed binary.
* @returns {Promise<string>}
*/
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;
/**
*
* @param {import('semver').SemVer} chromeVersion
*/
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;
}, /** @type {ChromedriverVersionMapping} */ ({}));
Object.assign(mapping, synchronizedDriversMapping);
await this.updateDriversMapping(mapping);
return true;
};
do {
const cds = await this.getChromedrivers(mapping);
/** @type {ChromedriverVersionMapping} */
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 = /** @type {Error} */ (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;
// eslint-disable-next-line no-constant-condition
} while (true);
}
async initChromedriverPath() {
if (this.executableVerified && this.chromedriver) {
return /** @type {string} */ (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 ` +
`${this.chromedriver}, but it doesn't exist!`);
}
this.executableVerified = true;
this.log.info(`Set chromedriver binary as: ${this.chromedriver}`);
return /** @type {string} */ (this.chromedriver);
}
/**
* Determines the driver communication protocol
* based on various validation rules.
*
* @returns {keyof PROTOCOLS}
*/
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 explcitly,
// 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;
}
/**
*
* @param {object} caps
* @param {boolean} emitStartingState
*/
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 = [`--port=${this.proxyPort}`];
if (this.adb && 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');
// what are the process stdout/stderr conditions wherein we know that
// the process has started to our satisfaction?
const startDetector = /** @param {string} stdout */ (stdout) => stdout.startsWith('Starting ');
let processIsAlive = false;
/** @type {string|undefined} */
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 = /** @type {Error} */ (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);
}
}
sessionId() {
return this.state === Chromedriver.STATE_ONLINE ? this.jwproxy.sessionId : null;
}
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);
await this.start(this.capabilities, false);
}
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;
}
/** @type {any} */
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 = /** @type {NewSessionResponse} */ (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;
}
async stop(emitStates = true) {
if (emitStates) {
this.changeState(Chromedriver.STATE_STOPPING);
}
/**
*
* @param {() => Promise<any>|any} f
*/
const runSafeStep = async (f) => {
try {
return await f();
}
catch (e) {
const err = /** @type {Error} */ (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);
}
}
/**
*
* @param {string} state
*/
changeState(state) {
this.state = state;
this.log.debug(`Changed state to '${state}'`);
this.emit(Chromedriver.EVENT_CHANGED, { state });
}
/**
*
* @param {string} url
* @param {'POST'|'GET'|'DELETE'} method
* @param {any} body
* @returns
*/
async sendCommand(url, method, body) {
return await this.jwproxy.command(url, method, body);
}
/**
*
* @param {any} req
* @param {any} res
* @privateRemarks req / res probably from Express
*/
async proxyReq(req, res) {
return await this.jwproxy.proxyReqRes(req, res);
}
async killAll() {
let 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(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 (let 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;
}
let params = conn.split(/\s+/);
if (params.length > 1) {
await this.adb.removePortForward(params[1].replace(/[\D]*/, ''));
}
}
}
catch (e) {
const err = /** @type {Error} */ (e);
this.log.warn(`Unable to clean forwarded ports. Error: '${err.message}'. Continuing.`);
}
}
}
async hasWorkingWebview() {
// sometimes chromedriver stops automating webviews. this method runs a
// simple command to determine our state, and responds accordingly
try {
await this.jwproxy.command('/url', 'GET');
return true;
}
catch {
return false;
}
}
}
exports.Chromedriver = Chromedriver;
Chromedriver.EVENT_ERROR = 'chromedriver_error';
Chromedriver.EVENT_CHANGED = 'stateChanged';
Chromedriver.STATE_STOPPED = 'stopped';
Chromedriver.STATE_STARTING = 'starting';
Chromedriver.STATE_ONLINE = 'online';
Chromedriver.STATE_STOPPING = 'stopping';
Chromedriver.STATE_RESTARTING = 'restarting';
/**
* @typedef {import('./types').ChromedriverVersionMapping} ChromedriverVersionMapping
*/
/**
* @typedef {{capabilities: Record<string, any>}} NewSessionResponse
*/
//# sourceMappingURL=chromedriver.js.map