UNPKG

12.3 kBJavaScriptView Raw
1'use strict';
2// @ts-check
3
4const fs = require('fs');
5const helper = require('./lib/chromedriver');
6const axios = require('axios').default;
7const path = require('path');
8const del = require('del');
9const child_process = require('child_process');
10const os = require('os');
11const url = require('url');
12const https = require('https');
13const extractZip = require('extract-zip');
14const { getChromeVersion } = require('@testim/chrome-version');
15const HttpsProxyAgent = require('https-proxy-agent');
16const getProxyForUrl = require("proxy-from-env").getProxyForUrl;
17
18const skipDownload = process.env.npm_config_chromedriver_skip_download || process.env.CHROMEDRIVER_SKIP_DOWNLOAD;
19if (skipDownload === 'true') {
20 console.log('Found CHROMEDRIVER_SKIP_DOWNLOAD variable, skipping installation.');
21 process.exit(0);
22}
23
24const libPath = path.join(__dirname, 'lib', 'chromedriver');
25let cdnUrl = process.env.npm_config_chromedriver_cdnurl || process.env.CHROMEDRIVER_CDNURL || 'https://chromedriver.storage.googleapis.com';
26const configuredfilePath = process.env.npm_config_chromedriver_filepath || process.env.CHROMEDRIVER_FILEPATH;
27
28// adapt http://chromedriver.storage.googleapis.com/
29cdnUrl = cdnUrl.replace(/\/+$/, '');
30const platform = validatePlatform();
31const detect_chromedriver_version = process.env.npm_config_detect_chromedriver_version || process.env.DETECT_CHROMEDRIVER_VERSION;
32let chromedriver_version = process.env.npm_config_chromedriver_version || process.env.CHROMEDRIVER_VERSION || helper.version;
33let chromedriverBinaryFilePath;
34let downloadedFile = '';
35
36(async function install() {
37 try {
38 if (detect_chromedriver_version === 'true') {
39 // Refer http://chromedriver.chromium.org/downloads/version-selection
40 const chromeVersion = await getChromeVersion();
41 console.log("Your Chrome version is " + chromeVersion);
42 const chromeVersionWithoutPatch = /^(.*?)\.\d+$/.exec(chromeVersion)[1];
43 await getChromeDriverVersion(getRequestOptions(cdnUrl + '/LATEST_RELEASE_' + chromeVersionWithoutPatch));
44 console.log("Compatible ChromeDriver version is " + chromedriver_version);
45 }
46 if (chromedriver_version === 'LATEST') {
47 await getChromeDriverVersion(getRequestOptions(`${cdnUrl}/LATEST_RELEASE`));
48 } else {
49 const latestReleaseForVersionMatch = chromedriver_version.match(/LATEST_(\d+)/);
50 if (latestReleaseForVersionMatch) {
51 const majorVersion = latestReleaseForVersionMatch[1];
52 await getChromeDriverVersion(getRequestOptions(`${cdnUrl}/LATEST_RELEASE_${majorVersion}`));
53 }
54 }
55 const tmpPath = findSuitableTempDirectory();
56 const chromedriverBinaryFileName = process.platform === 'win32' ? 'chromedriver.exe' : 'chromedriver';
57 chromedriverBinaryFilePath = path.resolve(tmpPath, chromedriverBinaryFileName);
58 const chromedriverIsAvailable = await verifyIfChromedriverIsAvailableAndHasCorrectVersion();
59 if (!chromedriverIsAvailable) {
60 console.log('Current existing ChromeDriver binary is unavailable, proceeding with download and extraction.');
61 await downloadFile(tmpPath);
62 await extractDownload(tmpPath);
63 }
64 await copyIntoPlace(tmpPath, libPath);
65 fixFilePermissions();
66 console.log('Done. ChromeDriver binary available at', helper.path);
67 } catch (err) {
68 console.error('ChromeDriver installation failed', err);
69 process.exit(1);
70 }
71})();
72
73function validatePlatform() {
74 /** @type string */
75 let thePlatform = process.platform;
76 if (thePlatform === 'linux') {
77 if (process.arch === 'arm64' || process.arch === 'x64') {
78 thePlatform += '64';
79 } else {
80 console.log('Only Linux 64 bits supported.');
81 process.exit(1);
82 }
83 } else if (thePlatform === 'darwin' || thePlatform === 'freebsd') {
84 if (process.arch === 'x64' || process.arch === 'arm64') {
85 thePlatform = 'mac64';
86 } else {
87 console.log('Only Mac 64 bits supported.');
88 process.exit(1);
89 }
90 } else if (thePlatform !== 'win32') {
91 console.log('Unexpected platform or architecture:', process.platform, process.arch);
92 process.exit(1);
93 }
94 return thePlatform;
95}
96
97async function downloadFile(dirToLoadTo) {
98 if (detect_chromedriver_version !== 'true' && configuredfilePath) {
99 downloadedFile = configuredfilePath;
100 console.log('Using file: ', downloadedFile);
101 return;
102 } else {
103 const fileName = `chromedriver_${platform}.zip`;
104 const tempDownloadedFile = path.resolve(dirToLoadTo, fileName);
105 downloadedFile = tempDownloadedFile;
106 const formattedDownloadUrl = `${cdnUrl}/${chromedriver_version}/${fileName}`;
107 console.log('Downloading from file: ', formattedDownloadUrl);
108 console.log('Saving to file:', downloadedFile);
109 await requestBinary(getRequestOptions(formattedDownloadUrl), downloadedFile);
110 }
111}
112
113function verifyIfChromedriverIsAvailableAndHasCorrectVersion() {
114 if (!fs.existsSync(chromedriverBinaryFilePath))
115 return Promise.resolve(false);
116 const forceDownload = process.env.npm_config_chromedriver_force_download === 'true' || process.env.CHROMEDRIVER_FORCE_DOWNLOAD === 'true';
117 if (forceDownload)
118 return Promise.resolve(false);
119 console.log('ChromeDriver binary exists. Validating...');
120 const deferred = new Deferred();
121 try {
122 fs.accessSync(chromedriverBinaryFilePath, fs.constants.X_OK);
123 const cp = child_process.spawn(chromedriverBinaryFilePath, ['--version']);
124 let str = '';
125 cp.stdout.on('data', data => str += data);
126 cp.on('error', () => deferred.resolve(false));
127 cp.on('close', code => {
128 if (code !== 0)
129 return deferred.resolve(false);
130 const parts = str.split(' ');
131 if (parts.length < 3)
132 return deferred.resolve(false);
133 if (parts[1].startsWith(chromedriver_version)) {
134 console.log(`ChromeDriver is already available at '${chromedriverBinaryFilePath}'.`);
135 return deferred.resolve(true);
136 }
137 deferred.resolve(false);
138 });
139 }
140 catch (error) {
141 deferred.resolve(false);
142 }
143 return deferred.promise;
144}
145
146function findSuitableTempDirectory() {
147 const now = Date.now();
148 const candidateTmpDirs = [
149 process.env.npm_config_tmp,
150 process.env.XDG_CACHE_HOME,
151 // Platform specific default, including TMPDIR/TMP/TEMP env
152 os.tmpdir(),
153 path.join(process.cwd(), 'tmp')
154 ];
155
156 for (let i = 0; i < candidateTmpDirs.length; i++) {
157 if (!candidateTmpDirs[i]) continue;
158 // Prevent collision with other versions in the dependency tree
159 const namespace = chromedriver_version;
160 const candidatePath = path.join(candidateTmpDirs[i], namespace, 'chromedriver');
161 try {
162 fs.mkdirSync(candidatePath, { recursive: true });
163 const testFile = path.join(candidatePath, now + '.tmp');
164 fs.writeFileSync(testFile, 'test');
165 fs.unlinkSync(testFile);
166 return candidatePath;
167 } catch (e) {
168 console.log(candidatePath, 'is not writable:', e.message);
169 }
170 }
171 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.');
172 process.exit(1);
173}
174
175function getRequestOptions(downloadPath) {
176 /** @type import('axios').AxiosRequestConfig */
177 const options = { url: downloadPath, method: "GET" };
178 const urlParts = url.parse(downloadPath);
179 const isHttps = urlParts.protocol === 'https:';
180 const proxyUrl = getProxyForUrl(downloadPath);
181
182 if (proxyUrl) {
183 const proxyUrlParts = url.parse(proxyUrl);
184 options.proxy = {
185 host: proxyUrlParts.hostname,
186 port: proxyUrlParts.port ? parseInt(proxyUrlParts.port) : 80,
187 protocol: proxyUrlParts.protocol
188 };
189 }
190
191 if (isHttps) {
192 // Use certificate authority settings from npm
193 let ca = process.env.npm_config_ca;
194 if (ca)
195 console.log('Using npmconf ca.');
196
197 if (!ca && process.env.npm_config_cafile) {
198 try {
199 ca = fs.readFileSync(process.env.npm_config_cafile, { encoding: 'utf8' });
200 } catch (e) {
201 console.error('Could not read cafile', process.env.npm_config_cafile, e);
202 }
203 console.log('Using npmconf cafile.');
204 }
205
206 if (proxyUrl) {
207 console.log('Using workaround for https-url combined with a proxy.');
208 const httpsProxyAgentOptions = url.parse(proxyUrl);
209 // @ts-ignore
210 httpsProxyAgentOptions.ca = ca;
211 // @ts-ignore
212 httpsProxyAgentOptions.rejectUnauthorized = !!process.env.npm_config_strict_ssl;
213 // @ts-ignore
214 options.httpsAgent = new HttpsProxyAgent(httpsProxyAgentOptions);
215 options.proxy = false;
216 } else {
217 options.httpsAgent = new https.Agent({
218 rejectUnauthorized: !!process.env.npm_config_strict_ssl,
219 ca: ca
220 });
221 }
222 }
223
224 // Use specific User-Agent
225 if (process.env.npm_config_user_agent) {
226 options.headers = { 'User-Agent': process.env.npm_config_user_agent };
227 }
228
229 return options;
230}
231
232/**
233 *
234 * @param {import('axios').AxiosRequestConfig} requestOptions
235 */
236async function getChromeDriverVersion(requestOptions) {
237 console.log('Finding Chromedriver version.');
238 const response = await axios(requestOptions);
239 chromedriver_version = response.data.trim();
240 console.log(`Chromedriver version is ${chromedriver_version}.`);
241}
242
243/**
244 *
245 * @param {import('axios').AxiosRequestConfig} requestOptions
246 * @param {string} filePath
247 */
248async function requestBinary(requestOptions, filePath) {
249 const outFile = fs.createWriteStream(filePath);
250 let response;
251 try {
252 response = await axios({ responseType: 'stream', ...requestOptions });
253 } catch (error) {
254 if (error && error.response) {
255 if (error.response.status)
256 console.error('Error status code:', error.response.status);
257 if (error.response.data) {
258 error.response.data.on('data', data => console.error(data.toString('utf8')));
259 await new Promise((resolve) => {
260 error.response.data.on('finish', resolve);
261 error.response.data.on('error', resolve);
262 });
263 }
264 }
265 throw new Error('Error with http(s) request: ' + error);
266 }
267 let count = 0;
268 let notifiedCount = 0;
269 response.data.on('data', data => {
270 count += data.length;
271 if ((count - notifiedCount) > 1024 * 1024) {
272 console.log('Received ' + Math.floor(count / 1024) + 'K...');
273 notifiedCount = count;
274 }
275 });
276 response.data.on('end', () => console.log('Received ' + Math.floor(count / 1024) + 'K total.'));
277 const pipe = response.data.pipe(outFile);
278 await new Promise((resolve, reject) => {
279 pipe.on('finish', resolve);
280 pipe.on('error', reject);
281 });
282}
283
284async function extractDownload(dirToExtractTo) {
285 if (path.extname(downloadedFile) !== '.zip') {
286 fs.copyFileSync(downloadedFile, chromedriverBinaryFilePath);
287 console.log('Skipping zip extraction - binary file found.');
288 return;
289 }
290 console.log(`Extracting zip contents to ${dirToExtractTo}.`);
291 try {
292 await extractZip(path.resolve(downloadedFile), { dir: dirToExtractTo });
293 } catch (error) {
294 throw new Error('Error extracting archive: ' + error);
295 }
296}
297
298async function copyIntoPlace(originPath, targetPath) {
299 await del(targetPath, { force: true });
300 console.log("Copying to target path", targetPath);
301 fs.mkdirSync(targetPath);
302
303 // Look for the extracted directory, so we can rename it.
304 const files = fs.readdirSync(originPath);
305 const promises = files.map(name => {
306 return new Promise((resolve) => {
307 const file = path.join(originPath, name);
308 const reader = fs.createReadStream(file);
309 const targetFile = path.join(targetPath, name);
310 const writer = fs.createWriteStream(targetFile);
311 writer.on("close", () => resolve());
312 reader.pipe(writer);
313 });
314 });
315 await Promise.all(promises);
316}
317
318
319function fixFilePermissions() {
320 // Check that the binary is user-executable and fix it if it isn't (problems with unzip library)
321 if (process.platform != 'win32') {
322 const stat = fs.statSync(helper.path);
323 // 64 == 0100 (no octal literal in strict mode)
324 if (!(stat.mode & 64)) {
325 console.log('Fixing file permissions.');
326 fs.chmodSync(helper.path, '755');
327 }
328 }
329}
330
331function Deferred() {
332 this.resolve = null;
333 this.reject = null;
334 this.promise = new Promise(function (resolve, reject) {
335 this.resolve = resolve;
336 this.reject = reject;
337 }.bind(this));
338 Object.freeze(this);
339}