1 | module.exports = install;
|
2 |
|
3 | var async = require('async');
|
4 | var crypto = require('crypto');
|
5 | var debug = require('debug')('selenium-standalone:install');
|
6 | var fs = require('fs');
|
7 | var os = require('os');
|
8 | var merge = require('lodash').merge;
|
9 | var assign = require('lodash').assign;
|
10 | var mapValues = require('lodash').mapValues;
|
11 | var mkdirp = require('mkdirp');
|
12 | var path = require('path');
|
13 | var request = require('request');
|
14 | var tarStream = require('tar-stream');
|
15 |
|
16 | var computeDownloadUrls = require('./compute-download-urls');
|
17 | var computeFsPaths = require('./compute-fs-paths');
|
18 | var defaultConfig = require('./default-config');
|
19 | var noop = require('./noop');
|
20 |
|
21 | function install(opts, cb) {
|
22 | debug('Install API called with', opts);
|
23 |
|
24 | var total = 0;
|
25 | var progress = 0;
|
26 | var startedRequests = 0;
|
27 | var expectedRequests;
|
28 |
|
29 | if (typeof opts === 'function') {
|
30 | cb = opts;
|
31 | opts = {};
|
32 | }
|
33 |
|
34 | var logger = opts.logger || noop;
|
35 |
|
36 | if (!opts.baseURL) {
|
37 | opts.baseURL = defaultConfig.baseURL;
|
38 | }
|
39 |
|
40 | if (!opts.version) {
|
41 | opts.version = defaultConfig.version;
|
42 | }
|
43 |
|
44 | if (opts.drivers) {
|
45 |
|
46 | opts.drivers = mapValues(opts.drivers, function(config, name) {
|
47 | return merge({}, defaultConfig.drivers[name], config);
|
48 | });
|
49 | } else {
|
50 | opts.drivers = defaultConfig.drivers;
|
51 | }
|
52 |
|
53 | if (opts.singleDriverInstall) {
|
54 | if(defaultConfig.drivers[opts.singleDriverInstall]) {
|
55 | opts.drivers = {};
|
56 | opts.drivers[opts.singleDriverInstall] = defaultConfig.drivers[opts.singleDriverInstall];
|
57 | }
|
58 | }
|
59 |
|
60 | if (process.platform !== 'win32') {
|
61 | delete opts.drivers.ie;
|
62 | delete opts.drivers.edge;
|
63 | }
|
64 | expectedRequests = Object.keys(opts.drivers).length + 1;
|
65 |
|
66 | var requestOpts = assign({ followAllRedirects: true }, opts.requestOpts);
|
67 | if (opts.proxy) {
|
68 | requestOpts.proxy = opts.proxy;
|
69 | }
|
70 |
|
71 | opts.progressCb = opts.progressCb || noop;
|
72 |
|
73 | logger('----------');
|
74 | logger('selenium-standalone installation starting');
|
75 | logger('----------');
|
76 | logger('');
|
77 |
|
78 | var fsPaths = computeFsPaths({
|
79 | seleniumVersion: opts.version,
|
80 | drivers: opts.drivers,
|
81 | basePath: opts.basePath
|
82 | });
|
83 |
|
84 | var urls = computeDownloadUrls({
|
85 | seleniumVersion: opts.version,
|
86 | seleniumBaseURL: opts.baseURL,
|
87 | drivers: opts.drivers
|
88 | });
|
89 |
|
90 | logInstallSummary(logger, fsPaths, urls);
|
91 |
|
92 | var tasks = [
|
93 | createDirs.bind(null, fsPaths),
|
94 | download.bind(null, {
|
95 | urls: urls,
|
96 | fsPaths: fsPaths
|
97 | }),
|
98 | asyncLogEnd.bind(null, logger)
|
99 | ];
|
100 |
|
101 | if (fsPaths.chrome) {
|
102 | tasks.push(setDriverFilePermissions.bind(null, fsPaths.chrome.installPath));
|
103 | }
|
104 |
|
105 | if (fsPaths.firefox) {
|
106 | tasks.push(setDriverFilePermissions.bind(null, fsPaths.firefox.installPath));
|
107 | }
|
108 |
|
109 | async.series(tasks, function(err) {
|
110 | cb(err, fsPaths);
|
111 | });
|
112 |
|
113 | function onlyInstallMissingFiles(opts, cb) {
|
114 | async.waterfall([
|
115 | checksum.bind(null, opts.to),
|
116 | isUpToDate.bind(null, opts.from, requestOpts)
|
117 | ], function (error, isLatest) {
|
118 | if (error) {
|
119 | return cb(error);
|
120 | }
|
121 |
|
122 |
|
123 | if (isLatest) {
|
124 | logger('---');
|
125 | logger('File from ' + opts.from + ' has already been downloaded');
|
126 | expectedRequests -= 1;
|
127 | return cb();
|
128 | }
|
129 |
|
130 | opts.installer.call(null, {
|
131 | to: opts.to,
|
132 | from: opts.from
|
133 | }, cb);
|
134 | });
|
135 | }
|
136 |
|
137 | function download(opts, cb) {
|
138 | var installers = [{
|
139 | installer: installSelenium,
|
140 | from: opts.urls.selenium,
|
141 | to: opts.fsPaths.selenium.downloadPath
|
142 | }];
|
143 |
|
144 | if (opts.fsPaths.chrome) {
|
145 | installers.push({
|
146 | installer: installChromeDr,
|
147 | from: opts.urls.chrome,
|
148 | to: opts.fsPaths.chrome.downloadPath
|
149 | });
|
150 | }
|
151 |
|
152 | if (process.platform === 'win32' && opts.fsPaths.ie) {
|
153 | installers.push({
|
154 | installer: installIeDr,
|
155 | from: opts.urls.ie,
|
156 | to: opts.fsPaths.ie.downloadPath
|
157 | });
|
158 | }
|
159 |
|
160 | if (process.platform === 'win32' && opts.fsPaths.edge) {
|
161 | installers.push({
|
162 | installer: installEdgeDr,
|
163 | from: opts.urls.edge,
|
164 | to: opts.fsPaths.edge.downloadPath
|
165 | });
|
166 | }
|
167 |
|
168 | if (opts.fsPaths.firefox) {
|
169 | installers.push({
|
170 | installer: installFirefoxDr,
|
171 | from: opts.urls.firefox,
|
172 | to: opts.fsPaths.firefox.downloadPath
|
173 | })
|
174 | }
|
175 |
|
176 | var steps = installers.map(function (opts) {
|
177 | return onlyInstallMissingFiles.bind(null, opts);
|
178 | });
|
179 |
|
180 | async.parallel(steps, cb);
|
181 | }
|
182 |
|
183 | function installSelenium(opts, cb) {
|
184 | installSingleFile(opts.from, opts.to, cb);
|
185 | }
|
186 |
|
187 | function installEdgeDr(opts, cb) {
|
188 | if (path.extname(opts.from) === '.msi') {
|
189 | downloadInstallerFile(opts.from, opts.to, cb);
|
190 | } else {
|
191 | installSingleFile(opts.from, opts.to, cb);
|
192 | }
|
193 | }
|
194 |
|
195 | function installSingleFile(from, to, cb) {
|
196 | getDownloadStream(from, function(err, stream) {
|
197 | if (err) {
|
198 | return cb(err);
|
199 | }
|
200 |
|
201 | stream
|
202 | .pipe(fs.createWriteStream(to))
|
203 | .once('error', cb.bind(null, new Error('Could not write to ' + to)))
|
204 | .once('finish', cb);
|
205 | });
|
206 | }
|
207 |
|
208 | function downloadInstallerFile(from, to, cb) {
|
209 | if (process.platform !== 'win32') {
|
210 | throw new Error('Could not install an `msi` file on the current platform');
|
211 | }
|
212 |
|
213 | getDownloadStream(from, function(err, stream) {
|
214 | if (err) {
|
215 | return cb(err);
|
216 | }
|
217 |
|
218 | var installerFile = getTempFileName('installer.msi');
|
219 | var msiWriteStream = fs.createWriteStream(installerFile)
|
220 | .once('error', cb.bind(null, new Error('Could not write to ' + to)));
|
221 | stream.pipe(msiWriteStream);
|
222 |
|
223 | msiWriteStream.once('finish', runInstaller.bind(null, installerFile, from, to, cb));
|
224 | });
|
225 | }
|
226 |
|
227 | function getTempFileName(suffix) {
|
228 | return os.tmpdir() + path.sep + os.uptime() + suffix
|
229 | }
|
230 |
|
231 | function runInstaller(installerFile, from, to, cb) {
|
232 | var logFile = getTempFileName('installer.log');
|
233 | var options = [
|
234 | '/passive',
|
235 | '/l*', logFile,
|
236 | '/i', installerFile
|
237 | ];
|
238 |
|
239 | var spawn = require('cross-spawn');
|
240 | var runner = spawn('msiexec', options, {stdio: 'inherit'});
|
241 |
|
242 | runner.on('exit', function (code) {
|
243 | fs.readFile(logFile, 'utf16le', function (err, data) {
|
244 | if (err) {
|
245 | return cb(err);
|
246 | }
|
247 |
|
248 | var installDir = data.split(os.EOL).map(function (line) {
|
249 | var match = line.match(/INSTALLDIR = (.+)$/);
|
250 | return match && match[1]
|
251 | }).filter(function (line) {
|
252 | return line != null;
|
253 | })[0];
|
254 |
|
255 | if (installDir) {
|
256 | fs.createReadStream(installDir + 'MicrosoftWebDriver.exe', {autoClose: true})
|
257 | .pipe(fs.createWriteStream(to, {autoClose: true}))
|
258 | .once('finish', function () {
|
259 | cb();
|
260 | })
|
261 | .once('error', function (err) {
|
262 | cb(err);
|
263 | });
|
264 | } else {
|
265 | cb(new Error('Could not find installed driver'));
|
266 | }
|
267 | });
|
268 | })
|
269 |
|
270 | runner.on('error', function (err) {
|
271 | cb(err)
|
272 | })
|
273 | }
|
274 |
|
275 | function installChromeDr(opts, cb) {
|
276 | installZippedFile(opts.from, opts.to, cb);
|
277 | }
|
278 |
|
279 | function installIeDr(opts, cb) {
|
280 | installZippedFile(opts.from, opts.to, cb);
|
281 | }
|
282 |
|
283 | function installFirefoxDr(opts, cb) {
|
284 |
|
285 | if (path.extname(opts.from) === '.zip') {
|
286 | installZippedFile(opts.from, opts.to, cb);
|
287 | } else {
|
288 | installGzippedFile(opts.from, opts.to, cb);
|
289 | }
|
290 | }
|
291 |
|
292 | function installGzippedFile(from, to, cb) {
|
293 | getDownloadStream(from, function(err, stream) {
|
294 | if (err) {
|
295 | return cb(err);
|
296 | }
|
297 |
|
298 | var gzipWriteStream = fs.createWriteStream(to)
|
299 | .once('error', cb.bind(null, new Error('Could not write to ' + to)));
|
300 | stream.pipe(gzipWriteStream);
|
301 |
|
302 | gzipWriteStream.once('finish', uncompressGzippedFile.bind(null, from, to, cb));
|
303 | });
|
304 | }
|
305 |
|
306 | function uncompressGzippedFile(from, gzipFilePath, cb) {
|
307 | var gunzip = require('zlib').createGunzip();
|
308 | var extractPath = path.join(path.dirname(gzipFilePath), path.basename(gzipFilePath, '.gz'));
|
309 | var writeStream = fs.createWriteStream(extractPath).once('error',
|
310 | function(error) {
|
311 | cb.bind(null, new Error('Could not write to ' + extractPath));
|
312 | }
|
313 | );
|
314 | var gunzippedContent = fs.createReadStream(gzipFilePath).pipe(gunzip)
|
315 | .once('error', cb.bind(null, new Error('Could not read ' + gzipFilePath)));
|
316 |
|
317 | if (from.substr(-7) === '.tar.gz') {
|
318 | var extractor = tarStream.extract();
|
319 | var fileAlreadyUnarchived = false;
|
320 | var cbCalled = false;
|
321 |
|
322 | extractor
|
323 | .on('entry', function(header, stream, callback) {
|
324 | if (fileAlreadyUnarchived) {
|
325 | if (!cbCalled) {
|
326 | cb(new Error('Tar archive contains more than one file'));
|
327 | cbCalled = true;
|
328 | }
|
329 | fileAlreadyUnarchived = true;
|
330 | }
|
331 | stream.pipe(writeStream);
|
332 | stream.on('end', function() {
|
333 | callback();
|
334 | })
|
335 | stream.resume();
|
336 | })
|
337 | .on('finish', function() {
|
338 | if (!cbCalled) {
|
339 | cb();
|
340 | cbCalled = true;
|
341 | }
|
342 | });
|
343 | gunzippedContent.pipe(extractor);
|
344 | } else {
|
345 | gunzippedContent.pipe(writeStream).on('finish', function() { cb(); });
|
346 | }
|
347 | }
|
348 |
|
349 | function installZippedFile(from, to, cb) {
|
350 | getDownloadStream(from, function(err, stream) {
|
351 | if (err) {
|
352 | return cb(err);
|
353 | }
|
354 |
|
355 |
|
356 | var zipWriteStream = fs.createWriteStream(to)
|
357 | .once('error', cb.bind(null, new Error('Could not write to ' + to)));
|
358 | stream.pipe(zipWriteStream);
|
359 |
|
360 |
|
361 | zipWriteStream.once('finish',
|
362 | uncompressDownloadedFile.bind(null, to, cb)
|
363 | );
|
364 | });
|
365 | }
|
366 |
|
367 | function getDownloadStream(downloadUrl, cb) {
|
368 | var r = request(downloadUrl, requestOpts)
|
369 | .on('response', function(res) {
|
370 | startedRequests += 1;
|
371 |
|
372 | if (res.statusCode !== 200) {
|
373 | return cb(new Error('Could not download ' + downloadUrl));
|
374 | }
|
375 |
|
376 | res.on('data', function(chunk) {
|
377 | progress += chunk.length;
|
378 | updateProgressPercentage(chunk.length);
|
379 | });
|
380 |
|
381 | total += parseInt(res.headers['content-length'], 10);
|
382 |
|
383 | cb(null, res);
|
384 | })
|
385 | .once('error', function(error) {
|
386 | cb(new Error('Could not download ' + downloadUrl + ': ' + error));
|
387 | });
|
388 |
|
389 |
|
390 | r.end();
|
391 | }
|
392 |
|
393 | function uncompressDownloadedFile(zipFilePath, cb) {
|
394 | debug('unzip ' + zipFilePath);
|
395 |
|
396 | var yauzl = require('yauzl');
|
397 | var extractPath = path.join(path.dirname(zipFilePath), path.basename(zipFilePath, '.zip'));
|
398 |
|
399 | yauzl.open(zipFilePath, {lazyEntries: true}, function onOpenZipFile(err, zipFile) {
|
400 | if (err) {
|
401 | cb(err);
|
402 | return;
|
403 | }
|
404 | zipFile.readEntry();
|
405 | zipFile.once('entry', function (entry) {
|
406 | zipFile.openReadStream(entry, function onOpenZipFileEntryReadStream(err, readStream) {
|
407 | if (err) {
|
408 | cb(err);
|
409 | return;
|
410 | }
|
411 | var extractWriteStream = fs.createWriteStream(extractPath)
|
412 | .once('error', cb.bind(null, new Error('Could not write to ' + extractPath)));
|
413 | readStream
|
414 | .pipe(extractWriteStream)
|
415 | .once('error', cb.bind(null, new Error('Could not read ' + zipFilePath)))
|
416 | .once('finish', function onExtracted() {
|
417 | zipFile.close();
|
418 | cb();
|
419 | });
|
420 | });
|
421 | });
|
422 | })
|
423 | }
|
424 |
|
425 | function updateProgressPercentage(chunk) {
|
426 | if (expectedRequests === startedRequests) {
|
427 | opts.progressCb(total, progress, chunk);
|
428 | }
|
429 | }
|
430 | }
|
431 |
|
432 | function asyncLogEnd(logger, cb) {
|
433 | setImmediate(function() {
|
434 | logger('');
|
435 | logger('');
|
436 | logger('-----');
|
437 | logger('selenium-standalone installation finished');
|
438 | logger('-----');
|
439 | cb();
|
440 | });
|
441 | }
|
442 |
|
443 | function createDirs(paths, cb) {
|
444 | var installDirectories =
|
445 | Object
|
446 | .keys(paths)
|
447 | .map(function(name) {
|
448 | return paths[name].installPath;
|
449 | });
|
450 |
|
451 | async.eachSeries(
|
452 | installDirectories.map(basePath),
|
453 | mkdirp,
|
454 | cb
|
455 | );
|
456 | }
|
457 |
|
458 | function basePath(fullPath) {
|
459 | return path.dirname(fullPath);
|
460 | }
|
461 |
|
462 | function setDriverFilePermissions(where, cb) {
|
463 | debug('setDriverFilePermissions', where);
|
464 |
|
465 | var chmod = function () {
|
466 | debug('chmod 0755 on', where);
|
467 | fs.chmod(where, '0755', cb);
|
468 | };
|
469 |
|
470 |
|
471 | if (fs.access) {
|
472 | fs.stat(where, function(err, stat) {
|
473 | debug('%s stats : %O', where, stat);
|
474 | });
|
475 | fs.access(where, fs.R_OK | fs.X_OK, function(err) {
|
476 | if (err) {
|
477 | debug('error in fs.access', where, err);
|
478 | chmod();
|
479 | } else {
|
480 | return cb();
|
481 | }
|
482 | }.bind(this));
|
483 | } else {
|
484 | chmod();
|
485 | }
|
486 | }
|
487 |
|
488 | function logInstallSummary(logger, paths, urls) {
|
489 | ['selenium', 'chrome', 'ie', 'firefox', 'edge'].forEach(function log(name) {
|
490 | if (!paths[name]) {
|
491 | return;
|
492 | }
|
493 |
|
494 | logger('---');
|
495 | logger(name + ' install:');
|
496 | logger('from: ' + urls[name]);
|
497 | logger('to: ' + paths[name].installPath);
|
498 | });
|
499 | }
|
500 |
|
501 | function checksum (filepath, cb) {
|
502 | if (!fs.existsSync(filepath)) {
|
503 | return cb(null, null);
|
504 | }
|
505 |
|
506 | var hash = crypto.createHash('md5');
|
507 | var stream = fs.createReadStream(filepath);
|
508 |
|
509 | stream.on('data', function (data) {
|
510 | hash.update(data, 'utf8');
|
511 | }).on('end', function () {
|
512 | cb(null, hash.digest('hex'));
|
513 | }).once('error', cb);
|
514 | }
|
515 |
|
516 | function unquote (str, quoteChar) {
|
517 | quoteChar = quoteChar || '"';
|
518 |
|
519 | if (str[0] === quoteChar && str[str.length - 1] === quoteChar) {
|
520 | return str.slice(1, str.length - 1);
|
521 | }
|
522 |
|
523 | return str;
|
524 | }
|
525 |
|
526 | function isUpToDate (url, requestOpts, hash, cb) {
|
527 | if (!hash) {
|
528 | return cb(null, false);
|
529 | }
|
530 |
|
531 | var query = merge({}, requestOpts, {
|
532 | url: url,
|
533 | headers: {
|
534 | 'If-None-Match': '"' + hash + '"'
|
535 | }
|
536 | });
|
537 |
|
538 | var req = request.get(query);
|
539 | req.on('response', function (res) {
|
540 | req.abort();
|
541 |
|
542 | if (res.statusCode === 304) {
|
543 | return cb(null, true);
|
544 | }
|
545 |
|
546 | if (res.statusCode !== 200) {
|
547 | return cb(new Error('Could not request headers from ' + url + ': ', res.statusCode));
|
548 | }
|
549 |
|
550 | cb(null, false);
|
551 | }).once('error', function (err) {
|
552 | cb(new Error('Could not request headers from ' + url + ': ' + err));
|
553 | });
|
554 | }
|