appium-chromedriver
Version:
Node.js wrapper around chromedriver.
892 lines (821 loc) • 30.2 kB
JavaScript
import events from 'events';
import {JWProxy, PROTOCOLS} from '@appium/base-driver';
import cp from 'child_process';
import {system, fs, logger, util} from '@appium/support';
import {retryInterval, asyncmap} from 'asyncbox';
import {SubProcess, exec} from 'teen_process';
import B from 'bluebird';
import {
getChromeVersion,
getChromedriverDir,
CHROMEDRIVER_CHROME_MAPPING,
getChromedriverBinaryPath,
generateLogPrefix,
} from './utils';
import * as semver from 'semver';
import _ from 'lodash';
import path from 'path';
import {compareVersions} from 'compare-versions';
import {ChromedriverStorageClient} from './storage-client/storage-client';
import {toW3cCapNames, getCapValue, toW3cCapName} from './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;
export class Chromedriver extends events.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 = logger.getLogger(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 JWProxy(proxyOpts);
if (this.executableDir) {
// Expects the user set the executable directory explicitly
this.isCustomExecutableDir = true;
} else {
this.isCustomExecutableDir = false;
this.executableDir = getChromedriverDir();
}
this.verbose = verbose;
this.logPath = logPath;
this.disableBuildCheck = !!disableBuildCheck;
this.storageClient = isAutodownloadEnabled
? new 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 = _.cloneDeep(CHROMEDRIVER_CHROME_MAPPING);
if (this.mappingPath) {
this.log.debug(`Attempting to use Chromedriver->Chrome mapping from '${this.mappingPath}'`);
if (!(await 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 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 _.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 fs.glob('*', {
cwd: this.executableDir,
nodir: true,
absolute: true,
});
this.log.debug(
`Found ${util.pluralize('executable', executables.length, true)} ` +
`in '${this.executableDir}'`,
);
const cds = (
await 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.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 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) => compareVersions(b.version, a.version));
if (_.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 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 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 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 fs.exists(this.mappingPath)) {
try {
await 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(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 getChromedriverBinaryPath();
}
const mapping = await this.getDriversMapping();
if (!_.isEmpty(mapping)) {
this.log.debug(`The most recent known Chrome version: ${_.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: ' +
_.truncate(JSON.stringify(retrievedMapping, null, 2), {length: 500}),
);
const driverKeys = await this.storageClient.syncDrivers({
minBrowserVersion: chromeVersion.major,
});
if (_.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 (!_.isEmpty(missingVersions)) {
this.log.info(
`Found ${util.pluralize('Chromedriver', _.size(missingVersions), true)}, ` +
`which ${
_.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 (_.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 (_.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 (_.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 ${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 getChromedriverBinaryPath()
: await this.getCompatibleChromedriver();
}
if (!(await 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 ${PROTOCOLS.W3C} protocol. ` +
`Defaulting to ${PROTOCOLS.MJSONWP}`,
);
this._desiredProtocol = PROTOCOLS.MJSONWP;
return this._desiredProtocol;
}
}
const isOperaDriver = _.includes(this._onlineStatus?.message, 'OperaDriver');
const chromeOptions = getCapValue(this.capabilities, 'chromeOptions');
if (_.isPlainObject(chromeOptions) && chromeOptions.w3c === false) {
this.log.info(
`The ChromeDriver v. ${this.driverVersion} supports ${PROTOCOLS.W3C} protocol, ` +
`but ${PROTOCOLS.MJSONWP} one has been explicitly requested`,
);
this._desiredProtocol = PROTOCOLS.MJSONWP;
return this._desiredProtocol;
} else if (isOperaDriver) {
// OperaDriver needs the W3C protocol to be requested explicitly,
// otherwise it defaults to JWP
if (_.isPlainObject(chromeOptions)) {
chromeOptions.w3c = true;
} else {
this.capabilities[toW3cCapName('chromeOptions')] = {w3c: true};
}
}
this._desiredProtocol = PROTOCOLS.W3C;
return this._desiredProtocol;
}
/**
*
* @param {object} caps
* @param {boolean} emitStartingState
*/
async start(caps, emitStartingState = true) {
this.capabilities = _.cloneDeep(caps);
// set the logging preferences to ALL the console logs
this.capabilities.loggingPrefs = _.cloneDeep(getCapValue(caps, 'loggingPrefs', {}));
if (_.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?.adbPort) {
args.push(`--adb-port=${this.adb.adbPort}`);
}
if (_.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 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 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 (!_.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 === PROTOCOLS.W3C
? {capabilities: {alwaysMatch: 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 = generateLogPrefix(this, this.jwproxy.sessionId);
this.changeState(Chromedriver.STATE_ONLINE);
return _.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 = 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 = 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 B.promisify(cp.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;
}
}
}
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
*/