1 | 'use strict';
|
2 |
|
3 |
|
4 | const fs = require('node:fs');
|
5 | const helper = require('./lib/chromedriver');
|
6 | const axios = require('axios').default;
|
7 | const path = require('node:path');
|
8 | const child_process = require('node:child_process');
|
9 | const os = require('node:os');
|
10 | const { ProxyAgent } = require('proxy-agent');
|
11 | const { promisify } = require('node:util');
|
12 | const { finished } = require('node:stream');
|
13 | const extractZip = require('extract-zip');
|
14 | const { getChromeVersion } = require('@testim/chrome-version');
|
15 | const { compareVersions } = require('compare-versions');
|
16 | const finishedAsync = promisify(finished);
|
17 | const process = require('node:process');
|
18 | const console = require('node:console');
|
19 |
|
20 | class Installer {
|
21 | async install() {
|
22 | const skipDownload = (process.env.npm_config_chromedriver_skip_download || process.env.CHROMEDRIVER_SKIP_DOWNLOAD) === 'true';
|
23 | if (skipDownload) {
|
24 | console.log('Found CHROMEDRIVER_SKIP_DOWNLOAD variable, skipping installation.');
|
25 | process.exit(0);
|
26 | }
|
27 | const cdnUrl = (process.env.npm_config_chromedriver_cdnurl || process.env.CHROMEDRIVER_CDNURL || 'https://googlechromelabs.github.io').replace(/\/+$/, '');
|
28 | const legacyCdnUrl = (process.env.npm_config_chromedriver_legacy_cdnurl || process.env.CHROMEDRIVER_LEGACY_CDNURL || 'https://chromedriver.storage.googleapis.com').replace(/\/+$/, '');
|
29 | let chromedriverVersion = process.env.npm_config_chromedriver_version || process.env.CHROMEDRIVER_VERSION || helper.version;
|
30 | const detectChromedriverVersion = (process.env.npm_config_detect_chromedriver_version || process.env.DETECT_CHROMEDRIVER_VERSION) === 'true';
|
31 | try {
|
32 | if (detectChromedriverVersion) {
|
33 | const includeChromium = (process.env.npm_config_include_chromium || process.env.INCLUDE_CHROMIUM) === 'true';
|
34 |
|
35 | const chromeVersion = await getChromeVersion(includeChromium);
|
36 | console.log("Your Chrome version is " + chromeVersion);
|
37 | const versionMatch = /^(.*?)\.\d+$/.exec(chromeVersion);
|
38 | if (versionMatch) {
|
39 | chromedriverVersion = await this.getChromeDriverVersion(cdnUrl, legacyCdnUrl, parseInt(versionMatch[1]));
|
40 | console.log("Compatible ChromeDriver version is " + chromedriverVersion);
|
41 | }
|
42 | } else if (chromedriverVersion === 'LATEST') {
|
43 | chromedriverVersion = await this.getChromeDriverVersion(cdnUrl, legacyCdnUrl);
|
44 | } else {
|
45 | const latestReleaseForVersionMatch = chromedriverVersion.match(/LATEST_(\d+)/);
|
46 | if (latestReleaseForVersionMatch) {
|
47 | chromedriverVersion = await this.getChromeDriverVersion(cdnUrl, legacyCdnUrl, parseInt(latestReleaseForVersionMatch[1]));
|
48 | }
|
49 | }
|
50 | let tmpPath = this.findSuitableTempDirectory(chromedriverVersion);
|
51 | const extractDirectory = tmpPath;
|
52 | const majorVersion = parseInt(chromedriverVersion.split('.')[0]);
|
53 | const useLegacyMethod = majorVersion <= 114;
|
54 | const platform = this.getPlatform(chromedriverVersion);
|
55 | let downloadedFile = this.getDownloadFilePath(useLegacyMethod, tmpPath, platform);
|
56 | if (!useLegacyMethod)
|
57 | tmpPath = path.join(tmpPath, path.basename(downloadedFile, path.extname(downloadedFile)));
|
58 | const chromedriverBinaryFileName = process.platform === 'win32' ? 'chromedriver.exe' : 'chromedriver';
|
59 | const chromedriverBinaryFilePath = path.resolve(tmpPath, chromedriverBinaryFileName);
|
60 | const chromedriverIsAvailable = await this.verifyIfChromedriverIsAvailableAndHasCorrectVersion(chromedriverVersion, chromedriverBinaryFilePath);
|
61 | if (!chromedriverIsAvailable) {
|
62 | console.log('Current existing ChromeDriver binary is unavailable, proceeding with download and extraction.');
|
63 | const configuredfilePath = process.env.npm_config_chromedriver_filepath || process.env.CHROMEDRIVER_FILEPATH;
|
64 | if (configuredfilePath) {
|
65 | console.log('Using file: ', configuredfilePath);
|
66 | downloadedFile = configuredfilePath;
|
67 | } else {
|
68 | if (useLegacyMethod)
|
69 | await this.downloadFileLegacy(legacyCdnUrl, downloadedFile, chromedriverVersion);
|
70 | else
|
71 | await this.downloadFile(cdnUrl, downloadedFile, chromedriverVersion, platform);
|
72 | }
|
73 | await this.extractDownload(extractDirectory, chromedriverBinaryFilePath, downloadedFile);
|
74 | }
|
75 | const libPath = path.join(__dirname, 'lib', 'chromedriver');
|
76 | await this.copyIntoPlace(tmpPath, libPath);
|
77 | this.fixFilePermissions();
|
78 | console.log('Done. ChromeDriver binary available at', helper.path);
|
79 | } catch (err) {
|
80 | console.error('ChromeDriver installation failed', err);
|
81 | process.exit(1);
|
82 | }
|
83 | }
|
84 |
|
85 | |
86 |
|
87 |
|
88 | getPlatform(chromedriverVersion) {
|
89 | const thePlatform = process.platform;
|
90 | if (thePlatform === 'linux') {
|
91 | if (process.arch === 'arm64' || process.arch === 's390x' || process.arch === 'x64') {
|
92 | return 'linux64';
|
93 | } else {
|
94 | console.error('Only Linux 64 bits supported.');
|
95 | process.exit(1);
|
96 | }
|
97 | } else if (thePlatform === 'darwin' || thePlatform === 'freebsd') {
|
98 | const osxPlatform = this.getMacOsRealArch(chromedriverVersion);
|
99 |
|
100 | if (!osxPlatform) {
|
101 | console.error('Only Mac 64 bits supported.');
|
102 | process.exit(1);
|
103 | }
|
104 |
|
105 | return osxPlatform;
|
106 | } else if (thePlatform === 'win32') {
|
107 | if (compareVersions(chromedriverVersion, '115') < 0) {
|
108 | return 'win32';
|
109 | }
|
110 | return (process.arch === 'x64') ? 'win64' : 'win32';
|
111 | }
|
112 | console.error('Unexpected platform or architecture:', process.platform, process.arch);
|
113 | process.exit(1);
|
114 | }
|
115 |
|
116 | |
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 | async downloadFile(cdnUrl, downloadedFile, chromedriverVersion, platform) {
|
123 | const cdnBinariesUrl = (process.env.npm_config_chromedriver_cdnbinariesurl || process.env.CHROMEDRIVER_CDNBINARIESURL)?.replace(/\/+$/, '');
|
124 | const url = cdnBinariesUrl
|
125 | ? `${cdnBinariesUrl}/${chromedriverVersion}/${platform}/${path.basename(downloadedFile)}`
|
126 | : await this.getDownloadUrl(cdnUrl, chromedriverVersion, platform);
|
127 | if (!url) {
|
128 | console.error(`Download url could not be found for version ${chromedriverVersion} and platform '${platform}'`);
|
129 | process.exit(1);
|
130 | }
|
131 | console.log('Downloading from file: ', url);
|
132 | await this.requestBinary(this.getRequestOptions(url), downloadedFile);
|
133 | }
|
134 |
|
135 | |
136 |
|
137 |
|
138 |
|
139 |
|
140 | async downloadFileLegacy(cdnUrl, downloadedFile, chromedriverVersion) {
|
141 | const url = `${cdnUrl}/${chromedriverVersion}/${path.basename(downloadedFile)}`;
|
142 | console.log('Downloading from file: ', url);
|
143 | await this.requestBinary(this.getRequestOptions(url), downloadedFile);
|
144 | }
|
145 |
|
146 | |
147 |
|
148 |
|
149 |
|
150 |
|
151 | getDownloadFilePath(useLegacyPath, dirToLoadTo, platform) {
|
152 | const fileName = useLegacyPath ? `chromedriver_${platform}.zip` : `chromedriver-${platform}.zip`;
|
153 | const downloadedFile = path.resolve(dirToLoadTo, fileName);
|
154 | console.log('Saving to file:', downloadedFile);
|
155 | return downloadedFile;
|
156 | }
|
157 |
|
158 | |
159 |
|
160 |
|
161 |
|
162 | verifyIfChromedriverIsAvailableAndHasCorrectVersion(chromedriverVersion, chromedriverBinaryFilePath) {
|
163 | if (!fs.existsSync(chromedriverBinaryFilePath))
|
164 | return Promise.resolve(false);
|
165 | const forceDownload = process.env.npm_config_chromedriver_force_download === 'true' || process.env.CHROMEDRIVER_FORCE_DOWNLOAD === 'true';
|
166 | if (forceDownload)
|
167 | return Promise.resolve(false);
|
168 | console.log('ChromeDriver binary exists. Validating...');
|
169 | const deferred = new Deferred();
|
170 | try {
|
171 | fs.accessSync(chromedriverBinaryFilePath, fs.constants.X_OK);
|
172 | const cp = child_process.spawn(chromedriverBinaryFilePath, ['--version']);
|
173 | let str = '';
|
174 | cp.stdout.on('data', data => str += data);
|
175 | cp.on('error', () => deferred.resolve(false));
|
176 | cp.on('close', code => {
|
177 | if (code !== 0)
|
178 | return deferred.resolve(false);
|
179 | const parts = str.split(' ');
|
180 | if (parts.length < 3)
|
181 | return deferred.resolve(false);
|
182 | if (parts[1].startsWith(chromedriverVersion)) {
|
183 | console.log(`ChromeDriver is already available at '${chromedriverBinaryFilePath}'.`);
|
184 | return deferred.resolve(true);
|
185 | }
|
186 | deferred.resolve(false);
|
187 | });
|
188 | }
|
189 | catch (error) {
|
190 | deferred.resolve(false);
|
191 | }
|
192 | return deferred.promise;
|
193 | }
|
194 |
|
195 | |
196 |
|
197 |
|
198 | findSuitableTempDirectory(chromedriverVersion) {
|
199 | const now = Date.now();
|
200 | const candidateTmpDirs = [
|
201 | process.env.npm_config_tmp,
|
202 | process.env.XDG_CACHE_HOME,
|
203 |
|
204 | os.tmpdir(),
|
205 | path.join(process.cwd(), 'tmp')
|
206 | ];
|
207 |
|
208 | for (const tempDir of candidateTmpDirs) {
|
209 | if (!tempDir) continue;
|
210 | const namespace = chromedriverVersion;
|
211 | const candidatePath = path.join(tempDir, namespace, 'chromedriver');
|
212 | try {
|
213 | fs.mkdirSync(candidatePath, { recursive: true });
|
214 | const testFile = path.join(candidatePath, now + '.tmp');
|
215 | fs.writeFileSync(testFile, 'test');
|
216 | fs.unlinkSync(testFile);
|
217 | return candidatePath;
|
218 | } catch (e) {
|
219 | console.log(candidatePath, 'is not writable:', e.message);
|
220 | }
|
221 | }
|
222 | 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.');
|
223 | process.exit(1);
|
224 | }
|
225 |
|
226 | getRequestOptions(downloadPath) {
|
227 |
|
228 | const options = { url: downloadPath, method: "GET" };
|
229 | const urlParts = new URL(downloadPath);
|
230 | const isHttps = urlParts.protocol === 'https:';
|
231 |
|
232 | if (isHttps) {
|
233 |
|
234 | let ca = process.env.npm_config_ca;
|
235 | if (ca)
|
236 | console.log('Using npmconf ca.');
|
237 |
|
238 | if (!ca && process.env.npm_config_cafile) {
|
239 | try {
|
240 | ca = fs.readFileSync(process.env.npm_config_cafile, { encoding: 'utf8' });
|
241 | } catch (e) {
|
242 | console.error('Could not read cafile', process.env.npm_config_cafile, e);
|
243 | }
|
244 | console.log('Using npmconf cafile.');
|
245 | }
|
246 | options.httpsAgent = new ProxyAgent({
|
247 | rejectUnauthorized: !!process.env.npm_config_strict_ssl,
|
248 | ca: ca
|
249 | });
|
250 | options.proxy = false;
|
251 | } else {
|
252 | const { getProxyForUrl } = require("proxy-from-env");
|
253 | const proxyUrl = getProxyForUrl(downloadPath);
|
254 | if (proxyUrl) {
|
255 | const proxyUrlParts = new URL(proxyUrl);
|
256 | if (proxyUrlParts.hostname && proxyUrlParts.protocol)
|
257 | options.proxy = {
|
258 | host: proxyUrlParts.hostname,
|
259 | port: proxyUrlParts.port ? parseInt(proxyUrlParts.port) : 80,
|
260 | protocol: proxyUrlParts.protocol
|
261 | };
|
262 | }
|
263 | }
|
264 |
|
265 |
|
266 | if (process.env.npm_config_user_agent) {
|
267 | options.headers = { 'User-Agent': process.env.npm_config_user_agent };
|
268 | }
|
269 |
|
270 | return options;
|
271 | }
|
272 |
|
273 | |
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 | async getChromeDriverVersion(cdnUrl, legacyCdnUrl, majorVersion) {
|
280 | if (majorVersion == null || majorVersion > 114) {
|
281 | console.log('Finding Chromedriver version.');
|
282 | let chromedriverVersion;
|
283 | if (majorVersion) {
|
284 | const requestOptions = this.getRequestOptions(`${cdnUrl}/chrome-for-testing/latest-versions-per-milestone.json`);
|
285 | const response = await axios.request(requestOptions);
|
286 | chromedriverVersion = response.data?.milestones[majorVersion.toString()]?.version;
|
287 | } else {
|
288 | const requestOptions = this.getRequestOptions(`${cdnUrl}/chrome-for-testing/last-known-good-versions.json`);
|
289 | const response = await axios.request(requestOptions);
|
290 | chromedriverVersion = response.data?.channels?.Stable?.version;
|
291 | }
|
292 | console.log(`Chromedriver version is ${chromedriverVersion}.`);
|
293 | return chromedriverVersion;
|
294 | } else {
|
295 | console.log('Finding Chromedriver version using legacy method.');
|
296 | const urlPath = majorVersion ? `LATEST_RELEASE_${majorVersion}` : 'LATEST_RELEASE';
|
297 | const requestOptions = this.getRequestOptions(`${legacyCdnUrl}/${urlPath}`);
|
298 | const response = await axios.request(requestOptions);
|
299 | const chromedriverVersion = response.data.trim();
|
300 | console.log(`Chromedriver version is ${chromedriverVersion}.`);
|
301 | return chromedriverVersion;
|
302 | }
|
303 | }
|
304 |
|
305 | |
306 |
|
307 |
|
308 |
|
309 |
|
310 |
|
311 | async getDownloadUrl(cdnUrl, version, platform) {
|
312 | const getUrlUrl = `${cdnUrl}/chrome-for-testing/${version}.json`;
|
313 | const requestOptions = this.getRequestOptions(getUrlUrl);
|
314 | const response = await axios.request(requestOptions);
|
315 | const url = response.data?.downloads?.chromedriver?.find((/** @type {{ platform: string; }} */ c) => c.platform == platform)?.url;
|
316 | return url;
|
317 | }
|
318 |
|
319 | |
320 |
|
321 |
|
322 |
|
323 |
|
324 | async requestBinary(requestOptions, filePath) {
|
325 | const outFile = fs.createWriteStream(filePath);
|
326 | let response;
|
327 | try {
|
328 | response = await axios.request({ responseType: 'stream', ...requestOptions });
|
329 | } catch (error) {
|
330 | let errorData = '';
|
331 | if (error && error.response) {
|
332 | if (error.response.status)
|
333 | console.error('Error status code:', error.response.status);
|
334 | if (error.response.data) {
|
335 | error.response.data.on('data', data => errorData += data.toString('utf8'));
|
336 | try {
|
337 | await finishedAsync(error.response.data);
|
338 | } catch (error) {
|
339 | console.error('Error downloading entire response:', error);
|
340 | }
|
341 | }
|
342 | }
|
343 | console.error(`Error with http(s) request:\n${error}\nError data:\n${errorData}`);
|
344 | process.exit(1);
|
345 | }
|
346 | let count = 0;
|
347 | let notifiedCount = 0;
|
348 | response.data.on('data', data => {
|
349 | count += data.length;
|
350 | if ((count - notifiedCount) > 1024 * 1024) {
|
351 | console.log('Received ' + Math.floor(count / 1024) + 'K...');
|
352 | notifiedCount = count;
|
353 | }
|
354 | });
|
355 | response.data.on('end', () => console.log('Received ' + Math.floor(count / 1024) + 'K total.'));
|
356 | const pipe = response.data.pipe(outFile);
|
357 | await new Promise((resolve, reject) => {
|
358 | pipe.on('finish', resolve);
|
359 | pipe.on('error', reject);
|
360 | });
|
361 | }
|
362 |
|
363 | |
364 |
|
365 |
|
366 |
|
367 |
|
368 | async extractDownload(dirToExtractTo, chromedriverBinaryFilePath, downloadedFile) {
|
369 | if (path.extname(downloadedFile) !== '.zip') {
|
370 | fs.mkdirSync(path.dirname(chromedriverBinaryFilePath), { recursive: true });
|
371 | fs.copyFileSync(downloadedFile, chromedriverBinaryFilePath);
|
372 | console.log('Skipping zip extraction - binary file found.');
|
373 | return;
|
374 | }
|
375 | console.log(`Extracting zip contents to ${dirToExtractTo}.`);
|
376 | try {
|
377 | await extractZip(path.resolve(downloadedFile), { dir: dirToExtractTo });
|
378 | } catch (error) {
|
379 | console.error('Error extracting archive: ' + error);
|
380 | process.exit(1);
|
381 | }
|
382 | }
|
383 |
|
384 | |
385 |
|
386 |
|
387 |
|
388 | async copyIntoPlace(originPath, targetPath) {
|
389 | fs.rmSync(targetPath, { recursive: true, force: true });
|
390 | console.log(`Copying from ${originPath} to target path ${targetPath}`);
|
391 | fs.mkdirSync(targetPath);
|
392 |
|
393 |
|
394 | const files = fs.readdirSync(originPath, { withFileTypes: true })
|
395 | .filter(dirent => dirent.isFile() && dirent.name.startsWith('chromedriver') && !dirent.name.endsWith(".debug") && !dirent.name.endsWith(".zip"))
|
396 | .map(dirent => dirent.name);
|
397 | const promises = files.map(name => {
|
398 | return (new Promise((resolve) => {
|
399 | const file = path.join(originPath, name);
|
400 | const reader = fs.createReadStream(file);
|
401 | const targetFile = path.join(targetPath, name);
|
402 | const writer = fs.createWriteStream(targetFile);
|
403 | writer.on("close", () => resolve());
|
404 | reader.pipe(writer);
|
405 | }));
|
406 | });
|
407 | await Promise.all(promises);
|
408 | }
|
409 |
|
410 |
|
411 | fixFilePermissions() {
|
412 |
|
413 | if (process.platform != 'win32') {
|
414 | const stat = fs.statSync(helper.path);
|
415 |
|
416 | if (!(stat.mode & 64)) {
|
417 | console.log('Fixing file permissions.');
|
418 | fs.chmodSync(helper.path, '755');
|
419 | }
|
420 | }
|
421 | }
|
422 |
|
423 | |
424 |
|
425 |
|
426 | getMacOsRealArch(chromedriverVersion) {
|
427 | if (process.arch === 'arm64' || this.isEmulatedRosettaEnvironment()) {
|
428 | return compareVersions(chromedriverVersion, '106.0.5249.61') < 0
|
429 | ? 'mac64_m1'
|
430 | : compareVersions(chromedriverVersion, '115') < 0 ? 'mac_arm64' : 'mac-arm64';
|
431 | }
|
432 |
|
433 | if (process.arch === 'x64') {
|
434 | return compareVersions(chromedriverVersion, '115') < 0 ? 'mac64' : 'mac-x64';
|
435 | }
|
436 |
|
437 | return null;
|
438 | }
|
439 |
|
440 | isEmulatedRosettaEnvironment() {
|
441 | const archName = child_process.spawnSync('uname', ['-m']).stdout.toString().trim();
|
442 |
|
443 | if (archName === 'x86_64') {
|
444 | const proc = child_process.spawnSync('sysctl', ['-in', 'sysctl.proc_translated']);
|
445 |
|
446 |
|
447 | if (proc.status) {
|
448 | console.error('Unexpected return code from sysctl: ' + proc.status);
|
449 | process.exit(1);
|
450 | }
|
451 |
|
452 |
|
453 |
|
454 | if (!proc.stdout) {
|
455 | return false;
|
456 | }
|
457 |
|
458 | const processTranslated = proc.stdout.toString().trim();
|
459 |
|
460 | return processTranslated === '1';
|
461 | }
|
462 |
|
463 | return false;
|
464 | }
|
465 |
|
466 | }
|
467 |
|
468 | function Deferred() {
|
469 | this.resolve = null;
|
470 | this.reject = null;
|
471 | this.promise = new Promise(function (resolve, reject) {
|
472 | this.resolve = resolve;
|
473 | this.reject = reject;
|
474 | }.bind(this));
|
475 | Object.freeze(this);
|
476 | }
|
477 |
|
478 | if (require.main === module)
|
479 | new Installer().install();
|
480 | else if (process.env.NODE_ENV === 'test')
|
481 | module.exports = Installer;
|