UNPKG

browser-driver-installer

Version:

Installs Chrome and Gecko drivers that matches with the specified browser versions

395 lines (352 loc) 15.2 kB
'use strict'; /* eslint-disable no-console */ const execSync = require('child_process').execSync; const extractZip = require('extract-zip'); const fs = require('fs'); const path = require('path'); const request = require('request'); const shell = require('shelljs'); const tar = require('tar'); const BROWSER_MAJOR_VERSION_REGEX = new RegExp(/^(\d+)/); const CHROME_DRIVER_LATEST_RELEASE_VERSION_PREFIX = 'LATEST_RELEASE_'; const CHROME_BROWSER_NAME = 'chrome'; const CHROME_DRIVER_NAME = 'chromedriver'; const CHROME_DRIVER_VERSION_REGEX = new RegExp(/\w+ ((\d+\.)+\d+)/); const CHROME_DRIVER_MAJOR_VERSION_REGEX = new RegExp(/^\d+/); const GECKO_DRIVER_NAME = 'geckodriver'; const GECKO_DRIVER_VERSION_REGEX = new RegExp(/\w+\s(\d+\.\d+\.\d+)/); const FIREFOX_BROWSER_NAME = 'firefox'; const VALID_BROWSER_NAMES = [CHROME_BROWSER_NAME, FIREFOX_BROWSER_NAME]; async function browserDriverInstaller(browserName, browserVersion, targetPath) { if (typeof browserName !== 'string' || typeof browserVersion !== 'string' || typeof targetPath !== 'string') { throw new Error('the parameters are not valid strings'); } checkIfSupportedPlatform(); const browser2DriverMappingInformation = JSON.parse( shell.cat(path.resolve(__dirname, 'browserVersion2DriverVersion.json'))); let browserVersion2DriverVersion = null; let driverName = null; const browserNameLowerCase = browserName.toLowerCase(); if (browserNameLowerCase === CHROME_BROWSER_NAME) { browserVersion2DriverVersion = browser2DriverMappingInformation.chromeDriverVersions; driverName = CHROME_DRIVER_NAME; } else if (browserNameLowerCase === FIREFOX_BROWSER_NAME) { browserVersion2DriverVersion = browser2DriverMappingInformation.geckoDriverVersions; driverName = GECKO_DRIVER_NAME; } else { throw new Error( `"${browserName}" is not a valid browser name, the valid names are: ${(VALID_BROWSER_NAMES).join(', ')}` ); } let browserMajorVersion = majorBrowserVersion(browserVersion); let driverVersion = browserVersion2DriverVersion[browserMajorVersion]; if (!driverVersion) { if (browserNameLowerCase === CHROME_BROWSER_NAME && Number(browserMajorVersion) > 114) { // Refer to https://chromedriver.chromium.org/downloads/version-selection for versions >= 115 driverVersion = browserVersion; } else if (browserNameLowerCase === CHROME_BROWSER_NAME && Number(browserMajorVersion) > 72) { // Refer to https://chromedriver.chromium.org/downloads for version compatibility between chromedriver // and Chrome driverVersion = CHROME_DRIVER_LATEST_RELEASE_VERSION_PREFIX + browserMajorVersion; } else if (browserNameLowerCase === FIREFOX_BROWSER_NAME && Number(browserMajorVersion) > 60) { // Refer to https://firefox-source-docs.mozilla.org/testing/geckodriver/Support.html for version // compatibility between geckodriver and Firefox driverVersion = browserVersion2DriverVersion['60']; } else { throw new Error( `failed to locate a version of the ${driverName} that matches the installed ${browserName} version ` + `(${browserVersion}), the valid ${browserName} versions are: ` + `${Object.keys(browserVersion2DriverVersion).join(', ')}` ); } } return await installBrowserDriver(driverName, driverVersion, targetPath); } function checkIfSupportedPlatform() { let arch = process.arch; let platform = process.platform; if (platform !== 'linux' || arch !== 'x64') { throw new Error(`Unsupported platform/architecture: ${platform} ${arch}. Only Linux x64 systems are supported`); } } function doesDriverAlreadyExist(driverName, driverExpectedVersion, targetPath) { // in the case of Chrome/chromedriver, when we query the latest version of chromedriver that matches a specific // Chrome version (say 77, greater than the last one in the browserVersion2DriverVersion.json, > 72), // driverExpectedVersion will be LATEST_RELEASE_77 and so the actual driverExpectedVersion should be 77.X (e.g. // 77.0.3865.40) so we don't know what X is, thus we match only the initial 'release' part which is 77 (up to the // first dot) let matchReleaseOnly = false; if (driverExpectedVersion.startsWith(CHROME_DRIVER_LATEST_RELEASE_VERSION_PREFIX)) { driverExpectedVersion = driverExpectedVersion.replace(CHROME_DRIVER_LATEST_RELEASE_VERSION_PREFIX, ''); matchReleaseOnly = true; } targetPath = path.resolve(targetPath); console.log(`checking if the '${targetPath}' installation directory for the '${driverName}' driver exists`); if (!shell.test('-e', targetPath)) { console.log(`the '${targetPath}' installation directory for the '${driverName}' driver does not exist`); return false; } console.log(`the '${targetPath}' installation directory exists, checking if it contains the ${driverName}`); if (!shell.test('-e', path.join(targetPath, driverName))) { console.log(`failed to find the ${driverName} in the '${targetPath}' installation directory`); return false; } console.log(`the '${driverName}' driver was found in the '${targetPath}' installation directory`); const driverVersion_ = driverVersion(driverName, targetPath); if (driverVersion_ === driverExpectedVersion || matchReleaseOnly && driverVersion_.split('.')[0] === driverExpectedVersion) { console.log(`the expected version (${driverExpectedVersion}) for the '${driverName}' is already installed`); return true; } else { console.log( `the expected version (${driverExpectedVersion}) for the '${driverName}' driver does not match the ` + `installed one (${driverVersion_}), removing the old version` ); shell.rm('-rf', path.join(targetPath, driverName)); return false; } } async function downloadChromeDriverPackage(driverVersion, targetPath) { console.log(`downloadChromeDriverPackage: driverVersion:${driverVersion}`); const driverFileName = 'chromedriver_linux64.zip'; const downloadedFilePath = path.resolve(targetPath, driverFileName); let downloadUrlBase = 'https://chromedriver.storage.googleapis.com'; let downloadUrl = `${downloadUrlBase}/${driverVersion}/${driverFileName}`;; if (driverVersion.startsWith(CHROME_DRIVER_LATEST_RELEASE_VERSION_PREFIX)) { const versionQueryUrl = `${downloadUrlBase}/${driverVersion}`; const httpRequestOptions = prepareHttpGetRequest(versionQueryUrl); driverVersion = await new Promise((resolve, reject) => { request(httpRequestOptions, (error, _response, body) => { if (error) { return reject(error); } resolve(body); }); }); downloadUrl = `${downloadUrlBase}/${driverVersion}/${driverFileName}`; } else { // for Chrome versions > 114, see https://chromedriver.chromium.org/downloads/version-selection const driverMajorVersion = majorChromeDriverVersion(driverVersion); if (Number(driverMajorVersion) > 114) { const jsonApiEndpoint = 'https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json' const httpRequestOptions = prepareHttpGetRequest(jsonApiEndpoint); const knownGoodVersionsWithDownloadsJson = await new Promise((resolve, reject) => { request(httpRequestOptions, (error, _response, body) => { if (error) { return reject(error); } resolve(body); }); }); const knownGoodVersionsWithDownloads = JSON.parse(knownGoodVersionsWithDownloadsJson); const matchingVersion = knownGoodVersionsWithDownloads.versions.find(x => x.version === driverVersion); if (matchingVersion === undefined) { throw new Error( `failed to find a matching Chrome Driver version for version=${driverVersion} from ${jsonApiEndpoint}` ); } downloadUrl = matchingVersion.downloads.chromedriver.find(x => x.platform === "linux64").url; } } await downloadFile(downloadUrl, downloadedFilePath); return downloadedFilePath; } async function downloadFile(downloadUrl, downloadedFilePath) { return new Promise((resolve, reject) => { console.log('Downloading from URL: ', downloadUrl); console.log('Saving to file:', downloadedFilePath); const httpRequestOptions = prepareHttpGetRequest(downloadUrl); let count = 0; let notifiedCount = 0; const outFile = fs.openSync(downloadedFilePath, 'w'); const response = request(httpRequestOptions); response.on('error', function (err) { fs.closeSync(outFile); reject(new Error('Error downloading file: ' + err)); }); response.on('data', function (data) { fs.writeSync(outFile, data, 0, data.length, null); count += data.length; if ((count - notifiedCount) > 800000) { console.log('Received ' + Math.floor(count / 1024) + 'K...'); notifiedCount = count; } }); response.on('complete', function () { console.log('Received ' + Math.floor(count / 1024) + 'K total.'); fs.closeSync(outFile); resolve(); }); }); } async function downloadGeckoDriverPackage(driverVersion, targetPath) { const downloadUrlBase = 'https://github.com/mozilla/geckodriver/releases/download'; const driverFileName = 'geckodriver-v' + driverVersion + '-linux64.tar.gz'; const downloadedFilePath = path.resolve(targetPath, driverFileName); const downloadUrl = `${downloadUrlBase}/v${driverVersion}/${driverFileName}`; await downloadFile(downloadUrl, downloadedFilePath); return downloadedFilePath; } function driverVersion(driverName, targetPath) { const versionOutput = execSync(path.join(targetPath, driverName) + ' --version').toString(); if (driverName === CHROME_DRIVER_NAME) { let version = versionOutput.match(CHROME_DRIVER_VERSION_REGEX)[1]; // for older versions defined in browserVersion2DriverVersion.json // we only need the first two version numbers, e.g.: // 2.45.615279 --> 2.45 if (version.startsWith('2.')) { version = version.match(new RegExp(/\d+\.\d+/))[0]; } return version; } return versionOutput.match(GECKO_DRIVER_VERSION_REGEX)[1]; } async function installBrowserDriver(driverName, driverVersion, targetPath) { if (doesDriverAlreadyExist(driverName, driverVersion, targetPath)) { return false; } // make sure the target directory exists shell.mkdir('-p', targetPath); if (driverName === CHROME_DRIVER_NAME) { await installChromeDriver(driverVersion, targetPath); } else { await installGeckoDriver(driverVersion, targetPath); } return true; } async function installChromeDriver(driverVersion, targetPath) { const downloadedFilePath = await downloadChromeDriverPackage(driverVersion, targetPath); console.log('Extracting driver package contents'); await extractZip(downloadedFilePath, { dir: path.resolve(targetPath) }); shell.rm(downloadedFilePath); if (!driverVersion.startsWith(CHROME_DRIVER_LATEST_RELEASE_VERSION_PREFIX)) { const driverMajorVersion = majorChromeDriverVersion(driverVersion); if (Number(driverMajorVersion) > 114) { // Prior to version 115, the zip contained the chromedriver binary at the root level of the zip // Starting with version 115 and onwards, the zip now containes the chromedriver binary // inside a sub-directory named chromedriver-linux64, so move it one dir above to the ${targetPath} // where we expect it to be const filePath = path.join(targetPath, 'chromedriver-linux64', CHROME_DRIVER_NAME) if (shell.test('-e', filePath)) { shell.mv(filePath, targetPath); shell.rm('-fr', path.join(targetPath, 'chromedriver-linux64')); } } } // make sure the driver file is user executable const driverFilePath = path.join(targetPath, CHROME_DRIVER_NAME); fs.chmodSync(driverFilePath, '755'); } async function installGeckoDriver(driverVersion, targetPath) { const downloadedFilePath = await downloadGeckoDriverPackage(driverVersion, targetPath); console.log('Extracting driver package contents'); tar.extract({ cwd: targetPath, file: downloadedFilePath, sync: true }); shell.rm(downloadedFilePath); // make sure the driver file is user executable const driverFilePath = path.join(targetPath, GECKO_DRIVER_NAME); fs.chmodSync(driverFilePath, '755'); } function majorBrowserVersion(browserVersionString) { let browserVersionStringType = typeof browserVersionString; if (browserVersionStringType !== 'string') { throw new Error( 'invalid type for the \'browserVersionString\' argument, details: expected a string, found ' + `${browserVersionStringType}` ); } let matches = browserVersionString.match(BROWSER_MAJOR_VERSION_REGEX); if (matches === null || matches.length < 1) { throw new Error(`unable to extract the browser version from the '${browserVersionString}' string`); } return matches[0]; } function majorChromeDriverVersion(chromeDriverVersionString) { let chromeDriverVersionStringType = typeof chromeDriverVersionString; if (chromeDriverVersionStringType !== 'string') { throw new Error( 'invalid type for the \'chromeDriverVersionString\' argument, details: expected a string, found ' + `${chromeDriverVersionStringType}` ); } let matches = chromeDriverVersionString.match(CHROME_DRIVER_MAJOR_VERSION_REGEX); if (matches === null || matches.length < 1) { throw new Error(`unable to extract the ChromeDriver version from the '${chromeDriverVersionString}' string`); } return matches[0]; } function prepareHttpGetRequest(downloadUrl) { const options = { method: 'GET', uri: downloadUrl }; const proxyUrl = process.env.npm_config_proxy || process.env.npm_config_http_proxy; if (proxyUrl) { options.proxy = proxyUrl; } const userAgent = process.env.npm_config_user_agent; if (userAgent) { options.headers = { 'User-Agent': userAgent }; } return options; } module.exports.browserDriverInstaller = browserDriverInstaller;