UNPKG

11.5 kBJavaScriptView Raw
1'use strict';
2/* eslint-disable no-console */
3
4const execSync = require('child_process').execSync;
5const extractZip = require('extract-zip');
6const fs = require('fs');
7const path = require('path');
8const request = require('request');
9const shell = require('shelljs');
10const tar = require('tar');
11
12const BROWSER_MAJOR_VERSION_REGEX = new RegExp(/^(\d+)/);
13const CHROME_DRIVER_LATEST_RELEASE_VERSION_PREFIX = 'LATEST_RELEASE_';
14const CHROME_BROWSER_NAME = 'chrome';
15const CHROME_DRIVER_NAME = 'chromedriver';
16const CHROME_DRIVER_VERSION_REGEX = new RegExp(/\w+ ([0-9]+.[0-9]+).+/);
17const GECKO_DRIVER_NAME = 'geckodriver';
18const GECKO_DRIVER_VERSION_REGEX = new RegExp(/\w+\s(\d+.\d+.\d+)/);
19const FIREFOX_BROWSER_NAME = 'firefox';
20const VALID_BROWSER_NAMES = [CHROME_BROWSER_NAME, FIREFOX_BROWSER_NAME];
21
22async function browserDriverInstaller(browserName, browserVersion, targetPath)
23{
24 if (typeof browserName !== 'string' || typeof browserVersion !== 'string' || typeof targetPath !== 'string')
25 {
26 throw new Error('the parameters are not valid strings');
27 }
28
29 checkIfSupportedPlatform();
30
31 const browser2DriverMappingInformation = JSON.parse(
32 shell.cat(path.resolve(__dirname, 'browserVersion2DriverVersion.json')));
33
34 let browserVersion2DriverVersion = null;
35 let driverName = null;
36 const browserNameLowerCase = browserName.toLowerCase();
37
38 if (browserNameLowerCase === CHROME_BROWSER_NAME)
39 {
40 browserVersion2DriverVersion = browser2DriverMappingInformation.chromeDriverVersions;
41 driverName = CHROME_DRIVER_NAME;
42 }
43 else if (browserNameLowerCase === FIREFOX_BROWSER_NAME)
44 {
45 browserVersion2DriverVersion = browser2DriverMappingInformation.geckoDriverVersions;
46 driverName = GECKO_DRIVER_NAME;
47 }
48 else
49 {
50 throw new Error(
51 `"${browserName}" is not a valid browser name, the valid names are: ${(VALID_BROWSER_NAMES).join(', ')}`
52 );
53 }
54
55 browserVersion = majorBrowserVersion(browserVersion);
56 let driverVersion = browserVersion2DriverVersion[browserVersion];
57 if (!driverVersion)
58 {
59 if (browserNameLowerCase === CHROME_BROWSER_NAME && Number(browserVersion) > 72)
60 {
61 // Refer to https://chromedriver.chromium.org/downloads for version compatibility between chromedriver
62 // and Chrome
63 driverVersion = CHROME_DRIVER_LATEST_RELEASE_VERSION_PREFIX + browserVersion;
64 }
65 else if (browserNameLowerCase === FIREFOX_BROWSER_NAME && Number(browserVersion) > 60)
66 {
67 // Refer to https://firefox-source-docs.mozilla.org/testing/geckodriver/Support.html for version
68 // compatibility between geckodriver and Firefox
69 driverVersion = browserVersion2DriverVersion['60'];
70 }
71 else
72 {
73 throw new Error(
74 `failed to locate a version of the ${driverName} that matches the installed ${browserName} version ` +
75 `(${browserVersion}), the valid ${browserName} versions are: ` +
76 `${Object.keys(browserVersion2DriverVersion).join(', ')}`
77 );
78 }
79 }
80
81 return await installBrowserDriver(driverName, driverVersion, targetPath);
82}
83
84function checkIfSupportedPlatform()
85{
86 let arch = process.arch;
87 let platform = process.platform;
88
89 if (platform !== 'linux' || arch !== 'x64')
90 {
91 throw new Error(`Unsupported platform/architecture: ${platform} ${arch}. Only Linux x64 systems are supported`);
92 }
93}
94
95function doesDriverAlreadyExist(driverName, driverExpectedVersion, targetPath)
96{
97 // in the case of Chrome/chromedriver, when we query the latest version of chromedriver that matches a specific
98 // Chrome version (say 77, greater than the last one in the browserVersion2DriverVersion.json, > 72),
99 // driverExpectedVersion will be LATEST_RELEASE_77 and so the actual driverExpectedVersion should be 77.X (e.g.
100 // 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
101 // first dot)
102 let matchReleaseOnly = false;
103 if (driverExpectedVersion.startsWith(CHROME_DRIVER_LATEST_RELEASE_VERSION_PREFIX))
104 {
105 driverExpectedVersion = driverExpectedVersion.replace(CHROME_DRIVER_LATEST_RELEASE_VERSION_PREFIX, '');
106 matchReleaseOnly = true;
107 }
108
109 targetPath = path.resolve(targetPath);
110 console.log(`checking if the '${targetPath}' installation directory for the '${driverName}' driver exists`);
111 if (!shell.test('-e', targetPath))
112 {
113 console.log(`the '${targetPath}' installation directory for the '${driverName}' driver does not exist`);
114 return false;
115 }
116
117 console.log(`the '${targetPath}' installation directory exists, checking if it contains the ${driverName}`);
118 if (!shell.test('-e', path.join(targetPath, driverName)))
119 {
120 console.log(`failed to find the ${driverName} in the '${targetPath}' installation directory`);
121 return false;
122 }
123
124 console.log(`the '${driverName}' driver was found in the '${targetPath}' installation directory`);
125 const driverVersion_ = driverVersion(driverName, targetPath);
126 if (driverVersion_ === driverExpectedVersion ||
127 matchReleaseOnly && driverVersion_.split('.')[0] === driverExpectedVersion)
128 {
129 console.log(`the expected version (${driverExpectedVersion}) for the '${driverName}' is already installed`);
130 return true;
131 }
132 else
133 {
134 console.log(
135 `the expected version (${driverExpectedVersion}) for the '${driverName}' driver does not match the ` +
136 `installed one (${driverVersion_}), removing the old version`
137 );
138 shell.rm('-rf', path.join(targetPath, driverName));
139 return false;
140 }
141}
142
143async function downloadChromeDriverPackage(driverVersion, targetPath)
144{
145 const downloadUrlBase = 'https://chromedriver.storage.googleapis.com';
146 const driverFileName = 'chromedriver_linux64.zip';
147 const downloadedFilePath = path.resolve(targetPath, driverFileName);
148
149 if (driverVersion.startsWith(CHROME_DRIVER_LATEST_RELEASE_VERSION_PREFIX))
150 {
151 const versionQueryUrl = `${downloadUrlBase}/${driverVersion}`;
152 const httpRequestOptions = prepareHttpGetRequest(versionQueryUrl);
153 driverVersion = await new Promise((resolve, reject) =>
154 {
155 request(httpRequestOptions, (error, _response, body) =>
156 {
157 if (error) { return reject(error); }
158 resolve(body);
159 });
160 });
161 }
162
163 const downloadUrl = `${downloadUrlBase}/${driverVersion}/${driverFileName}`;
164 await downloadFile(downloadUrl, downloadedFilePath);
165 return downloadedFilePath;
166}
167
168async function downloadFile(downloadUrl, downloadedFilePath)
169{
170 return new Promise((resolve, reject) =>
171 {
172 console.log('Downloading from URL: ', downloadUrl);
173 console.log('Saving to file:', downloadedFilePath);
174 const httpRequestOptions = prepareHttpGetRequest(downloadUrl);
175 let count = 0;
176 let notifiedCount = 0;
177 const outFile = fs.openSync(downloadedFilePath, 'w');
178 const response = request(httpRequestOptions);
179 response.on('error', function (err)
180 {
181 fs.closeSync(outFile);
182 reject(new Error('Error downloading file: ' + err));
183 });
184 response.on('data', function (data)
185 {
186 fs.writeSync(outFile, data, 0, data.length, null);
187 count += data.length;
188 if ((count - notifiedCount) > 800000)
189 {
190 console.log('Received ' + Math.floor(count / 1024) + 'K...');
191 notifiedCount = count;
192 }
193 });
194 response.on('complete', function ()
195 {
196 console.log('Received ' + Math.floor(count / 1024) + 'K total.');
197 fs.closeSync(outFile);
198 resolve();
199 });
200 });
201}
202
203async function downloadGeckoDriverPackage(driverVersion, targetPath)
204{
205 const downloadUrlBase = 'https://github.com/mozilla/geckodriver/releases/download';
206 const driverFileName = 'geckodriver-v' + driverVersion + '-linux64.tar.gz';
207 const downloadedFilePath = path.resolve(targetPath, driverFileName);
208 const downloadUrl = `${downloadUrlBase}/v${driverVersion}/${driverFileName}`;
209 await downloadFile(downloadUrl, downloadedFilePath);
210 return downloadedFilePath;
211}
212
213function driverVersion(driverName, targetPath)
214{
215 const versionOutput = execSync(path.join(targetPath, driverName) + ' --version').toString();
216
217 if (driverName === CHROME_DRIVER_NAME)
218 {
219 return versionOutput.match(CHROME_DRIVER_VERSION_REGEX)[1];
220 }
221
222 return versionOutput.match(GECKO_DRIVER_VERSION_REGEX)[1];
223}
224
225async function installBrowserDriver(driverName, driverVersion, targetPath)
226{
227 if (doesDriverAlreadyExist(driverName, driverVersion, targetPath))
228 {
229 return false;
230 }
231
232 // make sure the target directory exists
233 shell.mkdir('-p', targetPath);
234
235 if (driverName === CHROME_DRIVER_NAME)
236 {
237 await installChromeDriver(driverVersion, targetPath);
238 }
239 else
240 {
241 await installGeckoDriver(driverVersion, targetPath);
242 }
243
244 return true;
245}
246
247async function installChromeDriver(driverVersion, targetPath)
248{
249 const downloadedFilePath = await downloadChromeDriverPackage(driverVersion, targetPath);
250 console.log('Extracting driver package contents');
251 await new Promise((resolve, reject) =>
252 extractZip(downloadedFilePath, { dir: path.resolve(targetPath) }, error => error ? reject(error) : resolve())
253 );
254 shell.rm(downloadedFilePath);
255 // make sure the driver file is user executable
256 const driverFilePath = path.join(targetPath, CHROME_DRIVER_NAME);
257 fs.chmodSync(driverFilePath, '755');
258}
259
260async function installGeckoDriver(driverVersion, targetPath)
261{
262 const downloadedFilePath = await downloadGeckoDriverPackage(driverVersion, targetPath);
263 console.log('Extracting driver package contents');
264 tar.extract({ cwd: targetPath, file: downloadedFilePath, sync: true });
265 shell.rm(downloadedFilePath);
266 // make sure the driver file is user executable
267 const driverFilePath = path.join(targetPath, GECKO_DRIVER_NAME);
268 fs.chmodSync(driverFilePath, '755');
269}
270
271function majorBrowserVersion(browserVersionString)
272{
273 let browserVersionStringType = typeof browserVersionString;
274 if (browserVersionStringType !== 'string')
275 {
276 throw new Error(
277 'invalid type for the \'browserVersionString\' argument, details: expected a string, found ' +
278 `${browserVersionStringType}`
279 );
280 }
281 let matches = browserVersionString.match(BROWSER_MAJOR_VERSION_REGEX);
282 if (matches === null || matches.length < 1)
283 {
284 throw new Error(`unable to extract the browser version from the '${browserVersionString}' string`);
285 }
286 return matches[0];
287}
288
289function prepareHttpGetRequest(downloadUrl)
290{
291 const options = {
292 method: 'GET',
293 uri: downloadUrl
294 };
295
296 const proxyUrl = process.env.npm_config_proxy || process.env.npm_config_http_proxy;
297 if (proxyUrl)
298 {
299 options.proxy = proxyUrl;
300 }
301
302 const userAgent = process.env.npm_config_user_agent;
303 if (userAgent)
304 {
305 options.headers = { 'User-Agent': userAgent };
306 }
307
308 return options;
309}
310
311module.exports.browserDriverInstaller = browserDriverInstaller;
\No newline at end of file