1 | 'use strict';
|
2 |
|
3 |
|
4 | const fs = require('fs');
|
5 | const helper = require('./lib/chromedriver');
|
6 | const axios = require('axios').default;
|
7 | const path = require('path');
|
8 | const del = require('del');
|
9 | const child_process = require('child_process');
|
10 | const os = require('os');
|
11 | const url = require('url');
|
12 | const https = require('https');
|
13 | const extractZip = require('extract-zip');
|
14 | const { getChromeVersion } = require('@testim/chrome-version');
|
15 | const HttpsProxyAgent = require('https-proxy-agent');
|
16 | const getProxyForUrl = require("proxy-from-env").getProxyForUrl;
|
17 |
|
18 | const skipDownload = process.env.npm_config_chromedriver_skip_download || process.env.CHROMEDRIVER_SKIP_DOWNLOAD;
|
19 | if (skipDownload === 'true') {
|
20 | console.log('Found CHROMEDRIVER_SKIP_DOWNLOAD variable, skipping installation.');
|
21 | process.exit(0);
|
22 | }
|
23 |
|
24 | const libPath = path.join(__dirname, 'lib', 'chromedriver');
|
25 | let cdnUrl = process.env.npm_config_chromedriver_cdnurl || process.env.CHROMEDRIVER_CDNURL || 'https://chromedriver.storage.googleapis.com';
|
26 | const configuredfilePath = process.env.npm_config_chromedriver_filepath || process.env.CHROMEDRIVER_FILEPATH;
|
27 |
|
28 |
|
29 | cdnUrl = cdnUrl.replace(/\/+$/, '');
|
30 | const platform = validatePlatform();
|
31 | const detect_chromedriver_version = process.env.npm_config_detect_chromedriver_version || process.env.DETECT_CHROMEDRIVER_VERSION;
|
32 | let chromedriver_version = process.env.npm_config_chromedriver_version || process.env.CHROMEDRIVER_VERSION || helper.version;
|
33 | let chromedriverBinaryFilePath;
|
34 | let downloadedFile = '';
|
35 |
|
36 | (async function install() {
|
37 | try {
|
38 | if (detect_chromedriver_version === 'true') {
|
39 |
|
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 |
|
73 | function validatePlatform() {
|
74 |
|
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 |
|
97 | async 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 |
|
113 | function 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 |
|
146 | function findSuitableTempDirectory() {
|
147 | const now = Date.now();
|
148 | const candidateTmpDirs = [
|
149 | process.env.npm_config_tmp,
|
150 | process.env.XDG_CACHE_HOME,
|
151 |
|
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 |
|
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 |
|
175 | function getRequestOptions(downloadPath) {
|
176 |
|
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 |
|
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 |
|
210 | httpsProxyAgentOptions.ca = ca;
|
211 |
|
212 | httpsProxyAgentOptions.rejectUnauthorized = !!process.env.npm_config_strict_ssl;
|
213 |
|
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 |
|
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 |
|
235 |
|
236 | async 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 |
|
246 |
|
247 |
|
248 | async 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 |
|
284 | async 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 |
|
298 | async function copyIntoPlace(originPath, targetPath) {
|
299 | await del(targetPath, { force: true });
|
300 | console.log("Copying to target path", targetPath);
|
301 | fs.mkdirSync(targetPath);
|
302 |
|
303 |
|
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 |
|
319 | function fixFilePermissions() {
|
320 |
|
321 | if (process.platform != 'win32') {
|
322 | const stat = fs.statSync(helper.path);
|
323 |
|
324 | if (!(stat.mode & 64)) {
|
325 | console.log('Fixing file permissions.');
|
326 | fs.chmodSync(helper.path, '755');
|
327 | }
|
328 | }
|
329 | }
|
330 |
|
331 | function 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 | }
|