UNPKG

selenium-standalone

Version:

installs a `selenium-standalone` command line to install and start a standalone selenium server

446 lines (375 loc) 11.2 kB
module.exports = install; var async = require('async'); var crypto = require('crypto'); var fs = require('fs'); var merge = require('lodash').merge; var mapValues = require('lodash').mapValues; var mkdirp = require('mkdirp'); var path = require('path'); var request = require('request'); var tarStream = require('tar-stream'); var computeDownloadUrls = require('./compute-download-urls'); var computeFsPaths = require('./compute-fs-paths'); var defaultConfig = require('./default-config'); var noop = require('./noop'); function install(opts, cb) { var total = 0; var progress = 0; var startedRequests = 0; var expectedRequests; var requestOpts = {followAllRedirects: true}; if (typeof opts === 'function') { cb = opts; opts = {}; } var logger = opts.logger || noop; if (!opts.baseURL) { opts.baseURL = defaultConfig.baseURL; } if (!opts.version) { opts.version = defaultConfig.version; } if (opts.drivers) { // Merge in missing driver options for those specified opts.drivers = mapValues(opts.drivers, function(config, name) { return merge({}, defaultConfig.drivers[name], config); }); } else { opts.drivers = defaultConfig.drivers; } if (process.platform !== 'win32') { delete opts.drivers.ie; } expectedRequests = Object.keys(opts.drivers).length + 1; if (opts.proxy) { requestOpts.proxy = opts.proxy; } opts.progressCb = opts.progressCb || noop; logger('----------'); logger('selenium-standalone installation starting'); logger('----------'); logger(''); var fsPaths = computeFsPaths({ seleniumVersion: opts.version, drivers: opts.drivers, basePath: opts.basePath }); var urls = computeDownloadUrls({ seleniumVersion: opts.version, seleniumBaseURL: opts.baseURL, drivers: opts.drivers }); logInstallSummary(logger, fsPaths, urls); var tasks = [ createDirs.bind(null, fsPaths), download.bind(null, { urls: urls, fsPaths: fsPaths }), asyncLogEnd.bind(null, logger) ]; if (fsPaths.chrome) { tasks.push(chmodChromeDr.bind(null, fsPaths.chrome.installPath)); } if (fsPaths.firefox) { tasks.push(chmodChromeDr.bind(null, fsPaths.firefox.installPath)); } async.series(tasks, function(err) { cb(err, fsPaths); }); function onlyInstallMissingFiles(opts, cb) { async.waterfall([ checksum.bind(null, opts.to), isUpToDate.bind(null, opts.from, requestOpts) ], function (error, isLatest) { if (error) { return cb(error); } // File already exists. Prevent download/installation. if (isLatest) { logger('---'); logger('File from ' + opts.from + ' has already been downloaded'); expectedRequests -= 1; return cb(); } opts.installer.call(null, { to: opts.to, from: opts.from }, cb); }); } function download(opts, cb) { var installers = [{ installer: installSelenium, from: opts.urls.selenium, to: opts.fsPaths.selenium.downloadPath }]; if (opts.fsPaths.chrome) { installers.push({ installer: installChromeDr, from: opts.urls.chrome, to: opts.fsPaths.chrome.downloadPath }); } if (process.platform === 'win32' && opts.fsPaths.ie) { installers.push({ installer: installIeDr, from: opts.urls.ie, to: opts.fsPaths.ie.downloadPath }); } if (opts.fsPaths.firefox) { installers.push({ installer: installFirefoxDr, from: opts.urls.firefox, to: opts.fsPaths.firefox.downloadPath }) } var steps = installers.map(function (opts) { return onlyInstallMissingFiles.bind(null, opts); }); async.parallel(steps, cb); } function installSelenium(opts, cb) { getDownloadStream(opts.from, function(err, stream) { if (err) { return cb(err); } stream .pipe(fs.createWriteStream(opts.to)) .once('error', cb.bind(null, new Error('Could not write to ' + opts.to))) .once('finish', cb); }); } function installChromeDr(opts, cb) { installZippedFile(opts.from, opts.to, cb); } function installIeDr(opts, cb) { installZippedFile(opts.from, opts.to, cb); } function installFirefoxDr(opts, cb) { // only windows build is a zip if (path.extname(opts.from) === '.zip') { installZippedFile(opts.from, opts.to, cb); } else { installGzippedFile(opts.from, opts.to, cb); } } function installGzippedFile(from, to, cb) { getDownloadStream(from, function(err, stream) { if (err) { return cb(err); } // Store downloaded compressed file var gzipWriteStream = fs.createWriteStream(to) .once('error', cb.bind(null, new Error('Could not write to ' + to))); stream.pipe(gzipWriteStream); gzipWriteStream.once('finish', uncompressGzippedFile.bind(null, from, to, cb)); }); } function uncompressGzippedFile(from, gzipFilePath, cb) { var gunzip = require('zlib').createGunzip(); var extractPath = path.join(path.dirname(gzipFilePath), path.basename(gzipFilePath, '.gz')); var writeStream = fs.createWriteStream(extractPath).once('error', function(error) { cb.bind(null, new Error('Could not write to ' + extractPath)); } ); var gunzippedContent = fs.createReadStream(gzipFilePath).pipe(gunzip) .once('error', cb.bind(null, new Error('Could not read ' + gzipFilePath))); if (from.substr(-7) === '.tar.gz') { var extractor = tarStream.extract(); var fileAlreadyUnarchived = false; var cbCalled = false; extractor .on('entry', function(header, stream, callback) { if (fileAlreadyUnarchived) { if (!cbCalled) { cb(new Error('Tar archive contains more than one file')); cbCalled = true; } fileAlreadyUnarchived = true; } stream.pipe(writeStream); stream.on('end', function() { callback(); }) stream.resume(); }) .on('finish', function() { if (!cbCalled) { cb(); cbCalled = true; } }); gunzippedContent.pipe(extractor); } else { gunzippedContent.pipe(writeStream).on('finish', function() { cb(); }); } } function installZippedFile(from, to, cb) { getDownloadStream(from, function(err, stream) { if (err) { return cb(err); } // Store downloaded compressed file var zipWriteStream = fs.createWriteStream(to) .once('error', cb.bind(null, new Error('Could not write to ' + to))); stream.pipe(zipWriteStream); // Uncompress downloaded file zipWriteStream.once('finish', uncompressDownloadedFile.bind(null, to, cb) ); }); } function getDownloadStream(downloadUrl, cb) { var r = request(downloadUrl, requestOpts) .on('response', function(res) { startedRequests += 1; if (res.statusCode !== 200) { return cb(new Error('Could not download ' + downloadUrl)); } res.on('data', function(chunk) { progress += chunk.length; updateProgressPercentage(chunk.length); }); total += parseInt(res.headers['content-length'], 10); cb(null, res); }) .once('error', function(error) { cb(new Error('Could not download ' + downloadUrl + ': ' + error)); }); // initiate request r.end(); } function uncompressDownloadedFile(zipFilePath, cb) { var yauzl = require('yauzl'); var extractPath = path.join(path.dirname(zipFilePath), path.basename(zipFilePath, '.zip')); yauzl.open(zipFilePath, {lazyEntries: true}, function onOpenZipFile(err, zipFile) { if (err) { cb(err); return; } zipFile.readEntry(); zipFile.once('entry', function (entry) { zipFile.openReadStream(entry, function onOpenZipFileEntryReadStream(err, readStream) { if (err) { cb(err); return; } var extractWriteStream = fs.createWriteStream(extractPath) .once('error', cb.bind(null, new Error('Could not write to ' + extractPath))); readStream .pipe(extractWriteStream) .once('error', cb.bind(null, new Error('Could not read ' + zipFilePath))) .once('finish', function onExtracted() { zipFile.close(); cb(); }); }); }); }) } function updateProgressPercentage(chunk) { if (expectedRequests === startedRequests) { opts.progressCb(total, progress, chunk); } } } function asyncLogEnd(logger, cb) { setImmediate(function() { logger(''); logger(''); logger('-----'); logger('selenium-standalone installation finished'); logger('-----'); cb(); }); } function createDirs(paths, cb) { var installDirectories = Object .keys(paths) .map(function(name) { return paths[name].installPath; }); async.eachSeries( installDirectories.map(basePath), mkdirp, cb ); } function basePath(fullPath) { return path.dirname(fullPath); } function chmodChromeDr(where, cb) { var chmod = function () { fs.chmod(where, '0755', cb); }; // node.js 0.10.x does not support fs.access if (fs.access) { fs.access(where, fs.R_OK | fs.X_OK, function(err) { if (err) { chmod(); } else { return cb(); } }.bind(this)); } else { chmod(); } } function logInstallSummary(logger, paths, urls) { ['selenium', 'chrome', 'ie', 'firefox'].forEach(function log(name) { if (!paths[name]) { return; } logger('---'); logger(name + ' install:'); logger('from: ' + urls[name]); logger('to: ' + paths[name].installPath); }); } function checksum (filepath, cb) { if (!fs.existsSync(filepath)) { return cb(null, null); } var hash = crypto.createHash('md5'); var stream = fs.createReadStream(filepath); stream.on('data', function (data) { hash.update(data, 'utf8'); }).on('end', function () { cb(null, hash.digest('hex')); }).once('error', cb); } function unquote (str, quoteChar) { quoteChar = quoteChar || '"'; if (str[0] === quoteChar && str[str.length - 1] === quoteChar) { return str.slice(1, str.length - 1); } return str; } function isUpToDate (url, requestOpts, hash, cb) { if (!hash) { return cb(null, false); } var query = Object.assign({}, requestOpts, { url: url, headers: { 'If-None-Match': '"' + hash + '"' } }); var req = request.get(query); req.on('response', function (res) { req.abort(); if (res.statusCode === 304) { return cb(null, true); } if (res.statusCode !== 200) { return cb(new Error('Could not request headers from ' + url + ': ', res.statusCodestatusCode)); } cb(null, false); }).once('error', function (err) { cb(new Error('Could not request headers from ' + url + ': ' + err)); }); }