1 | 'use strict';
|
2 |
|
3 |
|
4 | const execSync = require('child_process').execSync;
|
5 | const extractZip = require('extract-zip');
|
6 | const fs = require('fs');
|
7 | const path = require('path');
|
8 | const request = require('request');
|
9 | const shell = require('shelljs');
|
10 | const tar = require('tar');
|
11 |
|
12 | const BROWSER_MAJOR_VERSION_REGEX = new RegExp(/^(\d+)/);
|
13 | const CHROME_DRIVER_LATEST_RELEASE_VERSION_PREFIX = 'LATEST_RELEASE_';
|
14 | const CHROME_BROWSER_NAME = 'chrome';
|
15 | const CHROME_DRIVER_NAME = 'chromedriver';
|
16 | const CHROME_DRIVER_VERSION_REGEX = new RegExp(/\w+ ([0-9]+.[0-9]+).+/);
|
17 | const GECKO_DRIVER_NAME = 'geckodriver';
|
18 | const GECKO_DRIVER_VERSION_REGEX = new RegExp(/\w+\s(\d+.\d+.\d+)/);
|
19 | const FIREFOX_BROWSER_NAME = 'firefox';
|
20 | const VALID_BROWSER_NAMES = [CHROME_BROWSER_NAME, FIREFOX_BROWSER_NAME];
|
21 |
|
22 | async 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 |
|
62 |
|
63 | driverVersion = CHROME_DRIVER_LATEST_RELEASE_VERSION_PREFIX + browserVersion;
|
64 | }
|
65 | else if (browserNameLowerCase === FIREFOX_BROWSER_NAME && Number(browserVersion) > 60)
|
66 | {
|
67 |
|
68 |
|
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 |
|
84 | function 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 |
|
95 | function doesDriverAlreadyExist(driverName, driverExpectedVersion, targetPath)
|
96 | {
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
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 |
|
143 | async 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 |
|
168 | async 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 |
|
203 | async 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 |
|
213 | function 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 |
|
225 | async function installBrowserDriver(driverName, driverVersion, targetPath)
|
226 | {
|
227 | if (doesDriverAlreadyExist(driverName, driverVersion, targetPath))
|
228 | {
|
229 | return false;
|
230 | }
|
231 |
|
232 |
|
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 |
|
247 | async 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 |
|
256 | const driverFilePath = path.join(targetPath, CHROME_DRIVER_NAME);
|
257 | fs.chmodSync(driverFilePath, '755');
|
258 | }
|
259 |
|
260 | async 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 |
|
267 | const driverFilePath = path.join(targetPath, GECKO_DRIVER_NAME);
|
268 | fs.chmodSync(driverFilePath, '755');
|
269 | }
|
270 |
|
271 | function 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 |
|
289 | function 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 |
|
311 | module.exports.browserDriverInstaller = browserDriverInstaller; |
\ | No newline at end of file |