@qunitjs/browserstack-runner
Version:
[Hotfix] run browser tests over BrowserStack
206 lines (167 loc) • 6.12 kB
JavaScript
var Log = require('./logger'),
logger = new Log(global.logLevel || 'info'),
exec = require('child_process').execFile,
fs = require('fs'),
path = require('path'),
https = require('https'),
utils = require('./utils'),
windows = ((process.platform.match(/win32/) || process.platform.match(/win64/)) !== null),
localBinary = __dirname + '/BrowserStackLocal' + (windows ? '.exe' : '');
var Tunnel = function Tunnel(key, port, uniqueIdentifier, config, callback) {
var that = {};
localBinary = process.env.BROWSERSTACK_LOCAL_BINARY_PATH || localBinary;
function tunnelLauncher() {
var tunnelOptions = getTunnelOptions(key, uniqueIdentifier);
if (typeof callback !== 'function') {
callback = function(){};
}
logger.debug('[%s] Launching tunnel', new Date());
var subProcess = exec(localBinary, tunnelOptions, function(error, stdout, stderr) {
logger.debug(stderr);
logger.debug(error);
if (stdout.indexOf('Error') >= 0 || error) {
logger.debug('[%s] Tunnel launching failed', new Date());
logger.debug(stdout);
callback(new Error(new Date() + ': Tunnel launching failed'));
}
});
var data = '';
var running = false;
var runMatchers = [ 'You can now access your local server(s)', 'Press Ctrl-C to exit' ];
setTimeout(function() {
if (!running) {
logger.error('BrowserStackLocal failed to launch within 30 seconds.');
callback(new Error('BrowserStackLocal failed to launch within 30 seconds.'));
}
}, 30 * 1000);
subProcess.stdout.on('data', function(_data) {
if (running) {
return;
}
data += _data;
if (data.indexOf(runMatchers[0]) >= 0 && data.indexOf(runMatchers[1]) >= 0) {
running = true;
logger.debug('[%s] Tunnel launched', new Date());
setTimeout(function(){
callback();
}, 2000);
}
});
if (config.tunnel_pid_file) {
utils.mkdirp(path.dirname(config.tunnel_pid_file));
fs.writeFile(config.tunnel_pid_file, subProcess.pid);
}
that.process = subProcess;
}
function getTunnelOptions(key, uniqueIdentifier) {
var options = [key];
if (config.debug) {
options.push('-v');
}
if (!uniqueIdentifier) {
options.push('-force');
options.push('-onlyAutomate');
} else {
options.push('-localIdentifier');
options.push(uniqueIdentifier);
}
var proxy = config.proxy;
if (proxy) {
options.push('-proxyHost ' + proxy.host);
options.push('-proxyPort ' + proxy.port);
if (proxy.username && proxy.password) {
options.push('-proxyUser ' + proxy.username);
options.push('-proxyPass ' + proxy.password);
}
}
return options;
}
function runTunnelCmd(tunnelOptions, subProcessTimeout, processOutputHook, callback) {
var isRunning, subProcess, timeoutHandle;
var callbackOnce = function (err, result) {
clearTimeout(timeoutHandle);
if (subProcess && isRunning) {
try {
process.kill(subProcess.pid, 'SIGKILL');
subProcess = null;
} catch (e) {
logger.debug('[%s] failed to kill process:', new Date(), e);
} finally {
if (config.tunnel_pid_file) {
fs.unlink(config.tunnel_pid_file, function () {});
}
}
}
callback && callback(err, result);
callback = null;
};
isRunning = true;
try {
subProcess = exec(localBinary, tunnelOptions, function (error, stdout) {
isRunning = false;
if (error) {
callbackOnce(new Error('failed to get process output: ' + error));
} else if (stdout) {
processOutputHook(stdout, callbackOnce);
}
});
subProcess.stdout.on('data', function (data) {
processOutputHook(data, callbackOnce);
});
} catch (e) {
// Handles EACCESS and other errors when binary file exists,
// but doesn't have necessary permissions (among other issues)
callbackOnce(new Error('failed to get process output: ' + e));
}
if (subProcessTimeout > 0) {
timeoutHandle = setTimeout(function () {
callbackOnce(new Error('failed to get process output: command timeout'));
}, subProcessTimeout);
}
}
function getTunnelBinaryVersion(callback) {
var subProcessTimeout = 3000;
runTunnelCmd([ '--version' ], subProcessTimeout, function (data, done) {
var matches = /version\s+(\d+(\.\d+)*)/.exec(data);
var version = (matches && matches.length > 2) && matches[1];
logger.debug('[%s] Tunnel binary: found version', new Date(), version);
done(isFinite(version) ? null : new Error('failed to get binary version'), parseFloat(version));
}, callback);
}
function verifyTunnelBinary(callback) {
logger.debug('[%s] Verifying tunnel binary', new Date());
fs.exists(localBinary, function (exists) {
if (!exists) {
logger.debug('[%s] Verifying tunnel binary: file does not exist', new Date());
callback(false);
} else {
getTunnelBinaryVersion(function (err, version) {
callback(!err && isFinite(version));
});
}
});
}
verifyTunnelBinary(function (exists) {
if (exists) {
tunnelLauncher();
return;
}
logger.debug('Downloading BrowserStack Local to "%s"', localBinary);
var file = fs.createWriteStream(localBinary);
https.get('https://s3.amazonaws.com/browserStack/browserstack-local/BrowserStackLocal' + (windows ? '.exe' : '-' + process.platform + '-' + process.arch),
function(response) {
response.pipe(file);
response.on('end', function() {
fs.chmodSync(localBinary, 0700);
setTimeout(function() {
tunnelLauncher();
}, 100);
}).on('error', function(e) {
logger.info('Got error while downloading binary: ' + e.message);
throw new Error('Got error while downloading binary: ' + e.message);
});
});
});
return that;
};
exports.Tunnel = Tunnel;