UNPKG

chromedriver

Version:
481 lines (443 loc) 18.9 kB
'use strict'; // @ts-check const fs = require('node:fs'); const helper = require('./lib/chromedriver'); const axios = require('axios').default; const path = require('node:path'); const child_process = require('node:child_process'); const os = require('node:os'); const { ProxyAgent } = require('proxy-agent'); const { promisify } = require('node:util'); const { finished } = require('node:stream'); const extractZip = require('extract-zip'); const { getChromeVersion } = require('@testim/chrome-version'); const { compareVersions } = require('compare-versions'); const finishedAsync = promisify(finished); const process = require('node:process'); const console = require('node:console'); class Installer { async install() { const skipDownload = (process.env.npm_config_chromedriver_skip_download || process.env.CHROMEDRIVER_SKIP_DOWNLOAD) === 'true'; if (skipDownload) { console.log('Found CHROMEDRIVER_SKIP_DOWNLOAD variable, skipping installation.'); process.exit(0); } const cdnUrl = (process.env.npm_config_chromedriver_cdnurl || process.env.CHROMEDRIVER_CDNURL || 'https://googlechromelabs.github.io').replace(/\/+$/, ''); const legacyCdnUrl = (process.env.npm_config_chromedriver_legacy_cdnurl || process.env.CHROMEDRIVER_LEGACY_CDNURL || 'https://chromedriver.storage.googleapis.com').replace(/\/+$/, ''); let chromedriverVersion = process.env.npm_config_chromedriver_version || process.env.CHROMEDRIVER_VERSION || helper.version; const detectChromedriverVersion = (process.env.npm_config_detect_chromedriver_version || process.env.DETECT_CHROMEDRIVER_VERSION) === 'true'; try { if (detectChromedriverVersion) { const includeChromium = (process.env.npm_config_include_chromium || process.env.INCLUDE_CHROMIUM) === 'true'; // Refer http://chromedriver.chromium.org/downloads/version-selection const chromeVersion = await getChromeVersion(includeChromium); console.log("Your Chrome version is " + chromeVersion); const versionMatch = /^(.*?)\.\d+$/.exec(chromeVersion); if (versionMatch) { chromedriverVersion = await this.getChromeDriverVersion(cdnUrl, legacyCdnUrl, parseInt(versionMatch[1])); console.log("Compatible ChromeDriver version is " + chromedriverVersion); } } else if (chromedriverVersion === 'LATEST') { chromedriverVersion = await this.getChromeDriverVersion(cdnUrl, legacyCdnUrl); } else { const latestReleaseForVersionMatch = chromedriverVersion.match(/LATEST_(\d+)/); if (latestReleaseForVersionMatch) { chromedriverVersion = await this.getChromeDriverVersion(cdnUrl, legacyCdnUrl, parseInt(latestReleaseForVersionMatch[1])); } } let tmpPath = this.findSuitableTempDirectory(chromedriverVersion); const extractDirectory = tmpPath; const majorVersion = parseInt(chromedriverVersion.split('.')[0]); const useLegacyMethod = majorVersion <= 114; const platform = this.getPlatform(chromedriverVersion); const downloadedFile = this.getDownloadFilePath(useLegacyMethod, tmpPath, platform); if (!useLegacyMethod) tmpPath = path.join(tmpPath, path.basename(downloadedFile, path.extname(downloadedFile))); const chromedriverBinaryFileName = process.platform === 'win32' ? 'chromedriver.exe' : 'chromedriver'; const chromedriverBinaryFilePath = path.resolve(tmpPath, chromedriverBinaryFileName); const chromedriverIsAvailable = await this.verifyIfChromedriverIsAvailableAndHasCorrectVersion(chromedriverVersion, chromedriverBinaryFilePath); if (!chromedriverIsAvailable) { console.log('Current existing ChromeDriver binary is unavailable, proceeding with download and extraction.'); const configuredfilePath = process.env.npm_config_chromedriver_filepath || process.env.CHROMEDRIVER_FILEPATH; if (configuredfilePath) { console.log('Using file: ', configuredfilePath); return configuredfilePath; } if (useLegacyMethod) await this.downloadFileLegacy(legacyCdnUrl, downloadedFile, chromedriverVersion); else await this.downloadFile(cdnUrl, downloadedFile, chromedriverVersion, platform); await this.extractDownload(extractDirectory, chromedriverBinaryFilePath, downloadedFile); } const libPath = path.join(__dirname, 'lib', 'chromedriver'); await this.copyIntoPlace(tmpPath, libPath); this.fixFilePermissions(); console.log('Done. ChromeDriver binary available at', helper.path); } catch (err) { console.error('ChromeDriver installation failed', err); process.exit(1); } } /** * @param {string} chromedriverVersion */ getPlatform(chromedriverVersion) { const thePlatform = process.platform; if (thePlatform === 'linux') { if (process.arch === 'arm64' || process.arch === 's390x' || process.arch === 'x64') { return 'linux64'; } else { console.error('Only Linux 64 bits supported.'); process.exit(1); } } else if (thePlatform === 'darwin' || thePlatform === 'freebsd') { const osxPlatform = this.getMacOsRealArch(chromedriverVersion); if (!osxPlatform) { console.error('Only Mac 64 bits supported.'); process.exit(1); } return osxPlatform; } else if (thePlatform === 'win32') { if (compareVersions(chromedriverVersion, '115') < 0) { return 'win32'; } return (process.arch === 'x64') ? 'win64' : 'win32'; } console.error('Unexpected platform or architecture:', process.platform, process.arch); process.exit(1); } /** * @param {string} cdnUrl * @param {string} downloadedFile * @param {string} chromedriverVersion * @param {string} platform */ async downloadFile(cdnUrl, downloadedFile, chromedriverVersion, platform) { const cdnBinariesUrl = (process.env.npm_config_chromedriver_cdnbinariesurl || process.env.CHROMEDRIVER_CDNBINARIESURL)?.replace(/\/+$/, ''); const url = cdnBinariesUrl ? `${cdnBinariesUrl}/${chromedriverVersion}/${platform}/${path.basename(downloadedFile)}` : await this.getDownloadUrl(cdnUrl, chromedriverVersion, platform); if (!url) { console.error(`Download url could not be found for version ${chromedriverVersion} and platform '${platform}'`); process.exit(1); } console.log('Downloading from file: ', url); await this.requestBinary(this.getRequestOptions(url), downloadedFile); } /** * @param {string} cdnUrl * @param {string} downloadedFile * @param {string} chromedriverVersion */ async downloadFileLegacy(cdnUrl, downloadedFile, chromedriverVersion) { const url = `${cdnUrl}/${chromedriverVersion}/${path.basename(downloadedFile)}`; console.log('Downloading from file: ', url); await this.requestBinary(this.getRequestOptions(url), downloadedFile); } /** * @param {any} useLegacyPath * @param {string} dirToLoadTo * @param {string} platform */ getDownloadFilePath(useLegacyPath, dirToLoadTo, platform) { const fileName = useLegacyPath ? `chromedriver_${platform}.zip` : `chromedriver-${platform}.zip`; const downloadedFile = path.resolve(dirToLoadTo, fileName); console.log('Saving to file:', downloadedFile); return downloadedFile; } /** * @param {string} chromedriverVersion * @param {string} chromedriverBinaryFilePath */ verifyIfChromedriverIsAvailableAndHasCorrectVersion(chromedriverVersion, chromedriverBinaryFilePath) { if (!fs.existsSync(chromedriverBinaryFilePath)) return Promise.resolve(false); const forceDownload = process.env.npm_config_chromedriver_force_download === 'true' || process.env.CHROMEDRIVER_FORCE_DOWNLOAD === 'true'; if (forceDownload) return Promise.resolve(false); console.log('ChromeDriver binary exists. Validating...'); const deferred = new Deferred(); try { fs.accessSync(chromedriverBinaryFilePath, fs.constants.X_OK); const cp = child_process.spawn(chromedriverBinaryFilePath, ['--version']); let str = ''; cp.stdout.on('data', data => str += data); cp.on('error', () => deferred.resolve(false)); cp.on('close', code => { if (code !== 0) return deferred.resolve(false); const parts = str.split(' '); if (parts.length < 3) return deferred.resolve(false); if (parts[1].startsWith(chromedriverVersion)) { console.log(`ChromeDriver is already available at '${chromedriverBinaryFilePath}'.`); return deferred.resolve(true); } deferred.resolve(false); }); } catch (error) { deferred.resolve(false); } return deferred.promise; } /** * @param {string} chromedriverVersion */ findSuitableTempDirectory(chromedriverVersion) { const now = Date.now(); const candidateTmpDirs = [ process.env.npm_config_tmp, process.env.XDG_CACHE_HOME, // Platform specific default, including TMPDIR/TMP/TEMP env os.tmpdir(), path.join(process.cwd(), 'tmp') ]; for (const tempDir of candidateTmpDirs) { if (!tempDir) continue; const namespace = chromedriverVersion; const candidatePath = path.join(tempDir, namespace, 'chromedriver'); try { fs.mkdirSync(candidatePath, { recursive: true }); const testFile = path.join(candidatePath, now + '.tmp'); fs.writeFileSync(testFile, 'test'); fs.unlinkSync(testFile); return candidatePath; } catch (e) { console.log(candidatePath, 'is not writable:', e.message); } } console.error('Can not find a writable tmp directory, please report issue on https://github.com/giggio/chromedriver/issues/ with as much information as possible.'); process.exit(1); } getRequestOptions(downloadPath) { /** @type import('axios').AxiosRequestConfig */ const options = { url: downloadPath, method: "GET" }; const urlParts = new URL(downloadPath); const isHttps = urlParts.protocol === 'https:'; if (isHttps) { // Use certificate authority settings from npm let ca = process.env.npm_config_ca; if (ca) console.log('Using npmconf ca.'); if (!ca && process.env.npm_config_cafile) { try { ca = fs.readFileSync(process.env.npm_config_cafile, { encoding: 'utf8' }); } catch (e) { console.error('Could not read cafile', process.env.npm_config_cafile, e); } console.log('Using npmconf cafile.'); } options.httpsAgent = new ProxyAgent({ rejectUnauthorized: !!process.env.npm_config_strict_ssl, ca: ca }); options.proxy = false; } else { const { getProxyForUrl } = require("proxy-from-env"); const proxyUrl = getProxyForUrl(downloadPath); if (proxyUrl) { const proxyUrlParts = new URL(proxyUrl); if (proxyUrlParts.hostname && proxyUrlParts.protocol) options.proxy = { host: proxyUrlParts.hostname, port: proxyUrlParts.port ? parseInt(proxyUrlParts.port) : 80, protocol: proxyUrlParts.protocol }; } } // Use specific User-Agent if (process.env.npm_config_user_agent) { options.headers = { 'User-Agent': process.env.npm_config_user_agent }; } return options; } /** * @param {string} cdnUrl * @param {string} legacyCdnUrl * @param {number} [majorVersion] * @returns {Promise<string>} */ async getChromeDriverVersion(cdnUrl, legacyCdnUrl, majorVersion) { if (majorVersion == null || majorVersion > 114) { console.log('Finding Chromedriver version.'); let chromedriverVersion; if (majorVersion) { const requestOptions = this.getRequestOptions(`${cdnUrl}/chrome-for-testing/latest-versions-per-milestone.json`); const response = await axios.request(requestOptions); chromedriverVersion = response.data?.milestones[majorVersion.toString()]?.version; } else { const requestOptions = this.getRequestOptions(`${cdnUrl}/chrome-for-testing/last-known-good-versions.json`); const response = await axios.request(requestOptions); chromedriverVersion = response.data?.channels?.Stable?.version; } console.log(`Chromedriver version is ${chromedriverVersion}.`); return chromedriverVersion; } else { console.log('Finding Chromedriver version using legacy method.'); const urlPath = majorVersion ? `LATEST_RELEASE_${majorVersion}` : 'LATEST_RELEASE'; const requestOptions = this.getRequestOptions(`${legacyCdnUrl}/${urlPath}`); const response = await axios.request(requestOptions); const chromedriverVersion = response.data.trim(); console.log(`Chromedriver version is ${chromedriverVersion}.`); return chromedriverVersion; } } /** * @param {string} cdnUrl * @param {string} version * @param {string} platform * @returns {Promise<[string]>} */ async getDownloadUrl(cdnUrl, version, platform) { const getUrlUrl = `${cdnUrl}/chrome-for-testing/${version}.json`; const requestOptions = this.getRequestOptions(getUrlUrl); const response = await axios.request(requestOptions); const url = response.data?.downloads?.chromedriver?.find((/** @type {{ platform: string; }} */ c) => c.platform == platform)?.url; return url; } /** * * @param {import('axios').AxiosRequestConfig} requestOptions * @param {string} filePath */ async requestBinary(requestOptions, filePath) { const outFile = fs.createWriteStream(filePath); let response; try { response = await axios.request({ responseType: 'stream', ...requestOptions }); } catch (error) { let errorData = ''; if (error && error.response) { if (error.response.status) console.error('Error status code:', error.response.status); if (error.response.data) { error.response.data.on('data', data => errorData += data.toString('utf8')); try { await finishedAsync(error.response.data); } catch (error) { console.error('Error downloading entire response:', error); } } } console.error(`Error with http(s) request:\n${error}\nError data:\n${errorData}`); process.exit(1); } let count = 0; let notifiedCount = 0; response.data.on('data', data => { count += data.length; if ((count - notifiedCount) > 1024 * 1024) { console.log('Received ' + Math.floor(count / 1024) + 'K...'); notifiedCount = count; } }); response.data.on('end', () => console.log('Received ' + Math.floor(count / 1024) + 'K total.')); const pipe = response.data.pipe(outFile); await new Promise((resolve, reject) => { pipe.on('finish', resolve); pipe.on('error', reject); }); } /** * @param {string} dirToExtractTo * @param {string} chromedriverBinaryFilePath * @param {string} downloadedFile */ async extractDownload(dirToExtractTo, chromedriverBinaryFilePath, downloadedFile) { if (path.extname(downloadedFile) !== '.zip') { fs.mkdirSync(path.dirname(chromedriverBinaryFilePath), { recursive: true }); fs.copyFileSync(downloadedFile, chromedriverBinaryFilePath); console.log('Skipping zip extraction - binary file found.'); return; } console.log(`Extracting zip contents to ${dirToExtractTo}.`); try { await extractZip(path.resolve(downloadedFile), { dir: dirToExtractTo }); } catch (error) { console.error('Error extracting archive: ' + error); process.exit(1); } } /** * @param {string} originPath * @param {string} targetPath */ async copyIntoPlace(originPath, targetPath) { fs.rmSync(targetPath, { recursive: true, force: true }); console.log(`Copying from ${originPath} to target path ${targetPath}`); fs.mkdirSync(targetPath); // Look for the extracted directory, so we can rename it. const files = fs.readdirSync(originPath, { withFileTypes: true }) .filter(dirent => dirent.isFile() && dirent.name.startsWith('chromedriver') && !dirent.name.endsWith(".debug") && !dirent.name.endsWith(".zip")) .map(dirent => dirent.name); const promises = files.map(name => { return /** @type {Promise<void>} */(new Promise((resolve) => { const file = path.join(originPath, name); const reader = fs.createReadStream(file); const targetFile = path.join(targetPath, name); const writer = fs.createWriteStream(targetFile); writer.on("close", () => resolve()); reader.pipe(writer); })); }); await Promise.all(promises); } fixFilePermissions() { // Check that the binary is user-executable and fix it if it isn't (problems with unzip library) if (process.platform != 'win32') { const stat = fs.statSync(helper.path); // 64 == 0100 (no octal literal in strict mode) if (!(stat.mode & 64)) { console.log('Fixing file permissions.'); fs.chmodSync(helper.path, '755'); } } } /** * @param {string} chromedriverVersion */ getMacOsRealArch(chromedriverVersion) { if (process.arch === 'arm64' || this.isEmulatedRosettaEnvironment()) { return compareVersions(chromedriverVersion, '106.0.5249.61') < 0 ? 'mac64_m1' : compareVersions(chromedriverVersion, '115') < 0 ? 'mac_arm64' : 'mac-arm64'; } if (process.arch === 'x64') { return compareVersions(chromedriverVersion, '115') < 0 ? 'mac64' : 'mac-x64'; } return null; } isEmulatedRosettaEnvironment() { const archName = child_process.spawnSync('uname', ['-m']).stdout.toString().trim(); if (archName === 'x86_64') { const proc = child_process.spawnSync('sysctl', ['-in', 'sysctl.proc_translated']); // When run with `-in`, the return code is 0 even if there is no `sysctl.proc_translated` if (proc.status) { console.error('Unexpected return code from sysctl: ' + proc.status); process.exit(1); } // If there is no `sysctl.proc_translated` (i.e. not rosetta) then nothing is printed to // stdout if (!proc.stdout) { return false; } const processTranslated = proc.stdout.toString().trim(); return processTranslated === '1'; } return false; } } function Deferred() { this.resolve = null; this.reject = null; this.promise = new Promise(function (resolve, reject) { this.resolve = resolve; this.reject = reject; }.bind(this)); Object.freeze(this); } if (require.main === module) new Installer().install(); else if (process.env.NODE_ENV === 'test') module.exports = Installer;