-
Notifications
You must be signed in to change notification settings - Fork 3
/
installer.js
395 lines (352 loc) · 14.8 KB
/
installer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
'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;