UNPKG

14.5 kBJavaScriptView Raw
1module.exports = install;
2
3var async = require('async');
4var crypto = require('crypto');
5var debug = require('debug')('selenium-standalone:install');
6var fs = require('fs');
7var os = require('os');
8var merge = require('lodash').merge;
9var assign = require('lodash').assign;
10var mapValues = require('lodash').mapValues;
11var mkdirp = require('mkdirp');
12var path = require('path');
13var request = require('request');
14var tarStream = require('tar-stream');
15
16var computeDownloadUrls = require('./compute-download-urls');
17var computeFsPaths = require('./compute-fs-paths');
18var defaultConfig = require('./default-config');
19var noop = require('./noop');
20
21function 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 // Merge in missing driver options for those specified
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 // File already exists. Prevent download/installation.
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', // no user interaction, only show progress bar
235 '/l*', logFile, // save install log to this file
236 '/i', installerFile // msi file to install
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 // only windows build is a zip
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 // Store downloaded compressed file
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 // Store downloaded compressed file
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 // Uncompress downloaded file
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 // initiate request
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
432function 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
443function 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
458function basePath(fullPath) {
459 return path.dirname(fullPath);
460}
461
462function 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 // node.js 0.10.x does not support fs.access
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
488function 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
501function 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
516function 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
526function 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}