/**
* @module digdug/SeleniumTunnel
*/
var format = require('util').format;
var fs = require('fs');
var pathUtil = require('path');
var Promise = require('dojo/Promise');
var request = require('dojo/request');
var Tunnel = require('./Tunnel');
var util = require('./util');
/*
* For the three included driver configs, the `platform` and `arch` properties are assumed to use values from the same
* sets as Node's `os.platform` and `os.arch` properties.
*/
function ChromeConfig(options) {
util.mixin(this, options);
}
ChromeConfig.prototype = {
constructor: ChromeConfig,
version: '2.25',
baseUrl: 'https://chromedriver.storage.googleapis.com',
platform: process.platform,
arch: process.arch,
get artifact() {
var platform = this.platform;
if (platform === 'linux') {
platform = 'linux' + (this.arch === 'x64' ? '64' : '32');
}
else if (platform === 'darwin') {
var parts = String(this.version).split('.').map(Number);
var isGreater = [ 2, 22 ].some(function (part, i) {
return parts[i] > part;
});
platform = isGreater ? 'mac64' : 'mac32';
}
return format('chromedriver_%s.zip', platform);
},
get url() {
return format('%s/%s/%s', this.baseUrl, this.version, this.artifact);
},
get executable() {
return this.platform === 'win32' ? 'chromedriver.exe' : 'chromedriver';
},
get seleniumProperty() {
return 'webdriver.chrome.driver';
}
};
function IeConfig(options) {
util.mixin(this, options);
}
IeConfig.prototype = {
constructor: IeConfig,
version: '3.0.0',
baseUrl: 'https://selenium-release.storage.googleapis.com',
arch: process.arch,
get artifact() {
var architecture = this.arch === 'x64' ? 'x64' : 'Win32';
return format('IEDriverServer_%s_%s.zip', architecture, this.version);
},
get url() {
var majorMinorVersion = this.version.slice(0, this.version.lastIndexOf('.'));
return format('%s/%s/%s', this.baseUrl, majorMinorVersion, this.artifact);
},
get executable() {
return 'IEDriverServer.exe';
},
get seleniumProperty() {
return 'webdriver.ie.driver';
}
};
function FirefoxConfig(options) {
util.mixin(this, options);
}
FirefoxConfig.prototype = {
constructor: FirefoxConfig,
version: '0.11.1',
baseUrl: 'https://github.com/mozilla/geckodriver/releases/download',
arch: process.arch,
platform: process.platform,
get artifact() {
var platform = this.platform;
if (platform === 'linux') {
platform = 'linux' + (this.arch === 'x64' ? '64' : '32');
}
else if (platform === 'win32') {
platform = 'win' + (this.arch === 'x64' ? '64' : '32');
}
else if (platform === 'darwin') {
platform = 'macos';
}
var extension = /^win/.test(platform) ? '.zip' : '.tar.gz';
return format('geckodriver-v%s-%s%s', this.version, platform, extension);
},
get url() {
return format('%s/v%s/%s', this.baseUrl, this.version, this.artifact);
},
get executable() {
return this.platform === 'win32' ? 'geckodriver.exe' : 'geckodriver';
},
get seleniumProperty() {
return 'webdriver.gecko.driver';
}
};
var driverNameMap = {
chrome: ChromeConfig,
ie: IeConfig,
firefox: FirefoxConfig
};
/**
* A Selenium tunnel. This tunnel downloads the {@link http://www.seleniumhq.org/download/ Selenium-standalone server}
* and any necessary WebDriver executables, and handles starting and stopping Selenium.
*
* The primary configuration option is {@linkcode module:digdug/SeleniumTunnel#drivers drivers}, which determines which
* browsers the Selenium tunnel will support.
*
* Note that Java must be installed and in the system path to use this tunnel.
*
* @constructor module:digdug/SeleniumTunnel
* @extends module:digdug/Tunnel
*/
function SeleniumTunnel() {
Tunnel.apply(this, arguments);
if (this.drivers === null) {
this.drivers = [ 'chrome' ];
}
}
var _super = Tunnel.prototype;
SeleniumTunnel.prototype = util.mixin(Object.create(_super), /** @lends module:digdug/SeleniumTunnel# */ {
constructor: SeleniumTunnel,
/**
* Additional arguments to send to the Selenium server at startup
*
* @type {Array.<string>}
*/
seleniumArgs: null,
/**
* The desired Selenium drivers to install. Each entry may be a string or an object. Strings must be the names of
* existing drivers in SeleniumTunnel. An object with a 'name' property is a configuration object -- the name must
* be the name of an existing driver in SeleniumTunnel, and the remaining properties will be used to configure that
* driver. An object without a 'name' property is a driver definition. It must contain three properties:
*
* - executable - the name of the driver executable
* - url - the URL where the driver can be downloaded from
* - seleniumProperty - the name of the Java property used to tell Selenium where the driver is
*
* @example
* [
* 'chrome',
* {
* name: 'firefox',
* version: '0.8.0'
* },
* {
* url: 'https://github.com/operasoftware/operachromiumdriver/releases/.../operadriver_mac64.zip',
* executable: 'operadriver',
* seleniumProperty: 'webdriver.opera.driver'
* }
* ]
*
* @type {Array.<string|Object>}
* @default [ 'chrome' ]
*/
drivers: null,
/**
* The base address where Selenium artifacts may be found.
*
* @type {string}
* @default https://selenium-release.storage.googleapis.com
*/
baseUrl: 'https://selenium-release.storage.googleapis.com',
/**
* The desired version of selenium to install.
*
* @type {string}
* @default 3.0.1
*/
version: '3.0.1',
/**
* Timeout in milliseconds for communicating with the Selenium server
*
* @type {number}
* @default 5000
*/
seleniumTimeout: 5000,
get artifact() {
return 'selenium-server-standalone-' + this.version + '.jar';
},
get directory() {
return pathUtil.join(__dirname, 'selenium-standalone');
},
get executable() {
return 'java';
},
get isDownloaded() {
var directory = this.directory;
return this._getDriverConfigs().every(function (config) {
return util.fileExists(pathUtil.join(directory, config.executable));
}, this) && util.fileExists(pathUtil.join(directory, this.artifact));
},
get url() {
var majorMinorVersion = this.version.slice(0, this.version.lastIndexOf('.'));
return format('%s/%s/%s', this.baseUrl, majorMinorVersion, this.artifact);
},
download: function (forceDownload) {
if (!forceDownload && this.isDownloaded) {
return Promise.resolve();
}
var self = this;
return new Promise(function (resolve, reject, progress, setCanceler) {
setCanceler(function (reason) {
tasks && tasks.forEach(function (task) {
task.cancel(reason);
});
});
var configs = [ { url: self.url, executable: self.artifact } ];
configs = configs.concat(self._getDriverConfigs());
var tasks = configs.map(function (config) {
var executable = config.executable;
if (fs.existsSync(pathUtil.join(self.directory, executable))) {
return Promise.resolve();
}
return self._downloadFile(config.url, self.proxy, {
executable: executable
}).then(null, null, progress);
});
resolve(Promise.all(tasks));
});
},
sendJobState: function () {
// This is a noop for Selenium
return Promise.resolve();
},
_getDriverConfigs: function () {
function getDriverConfig(name, options) {
var Constructor = driverNameMap[name];
if (!Constructor) {
throw new Error('Invalid driver name "' + name + '"');
}
return new Constructor(options);
}
return this.drivers.map(function (data) {
if (typeof data === 'string') {
return getDriverConfig(data);
}
if (typeof data === 'object' && data.name) {
return getDriverConfig(data.name, data);
}
// data is a driver definition
return data;
});
},
_makeArgs: function () {
var directory = this.directory;
var driverConfigs = this._getDriverConfigs();
var args = [];
driverConfigs.reduce(function (args, config) {
var file = pathUtil.join(directory, config.executable);
args.push('-D' + config.seleniumProperty + '=' + file);
return args;
}, args);
if (this.seleniumArgs) {
args = args.concat(this.seleniumArgs);
}
args = args.concat([
'-jar',
pathUtil.join(this.directory, this.artifact),
'-port',
this.port
]);
if (this.verbose) {
args.push('-debug');
console.log('starting with arguments: ', args.join(' '));
}
return args;
},
_postDownloadFile: function (response, options) {
if (pathUtil.extname(options.executable) === '.jar') {
return util.writeFile(response.data, pathUtil.join(this.directory, options.executable));
}
return util.decompress(response.data, this.directory);
},
_start: function () {
var self = this;
var childHandle = this._makeChild();
var child = childHandle.process;
var dfd = childHandle.deferred;
var handle = util.on(child.stderr, 'data', function (data) {
// Selenium recommends that we poll the hub looking for a status response
// https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/7957
// We're going against the recommendation here for a few reasons
// 1. There's no default pid or log to look for errors to provide a specific failure
// 2. Polling on a failed server start could leave us with an unpleasant wait
// 3. Just polling a selenium server doesn't guarantee it's the server we started
// 4. This works pretty well
if (data.indexOf('Selenium Server is up and running') > -1) {
dfd.resolve();
}
if (self.verbose) {
process.stderr.write(data);
}
});
var removeHandle = handle.remove.bind(handle);
dfd.promise.then(removeHandle, removeHandle);
return childHandle;
},
_stop: function () {
var dfd = new Promise.Deferred();
var childProcess = this._process;
var timeout;
childProcess.once('exit', function (code) {
dfd.resolve(code);
clearTimeout(timeout);
});
// Nicely ask the Selenium server to shutdown
request('http://' + this.hostname + ':' + this.port +
'/selenium-server/driver/?cmd=shutDownSeleniumServer', {
timeout: this.seleniumTimeout,
handleAs: 'text'
});
// Give Selenium a few seconds, then forcefully tell it to shutdown
timeout = setTimeout(function () {
childProcess.kill('SIGTERM');
}, 5000);
return dfd.promise;
}
});
module.exports = SeleniumTunnel;