browserstacktunnel-wrapper
Version:
A Node.js wrapper for the BrowserStack java tunnel client
200 lines (169 loc) • 5.4 kB
JavaScript
var util = require('util'),
fs = require('fs'),
EventEmitter = require('events').EventEmitter,
spawn = require('child_process').spawn,
os = require('os'),
ZipBinary = require('./ZipBinary');
function BrowserStackTunnel(options) {
'use strict';
var params = [];
var binary;
switch (os.platform()) {
case 'linux':
switch (os.arch()) {
case 'x64':
binary = new ZipBinary('linux', 'x64', options.linux64Bin);
break;
case 'ia32':
binary = new ZipBinary('linux', 'ia32', options.linux32Bin);
break;
}
break;
case 'darwin':
binary = new ZipBinary('darwin', 'x64', options.osxBin);
break;
default:
binary = new ZipBinary('win32', null, options.win32Bin, 'exe');
break;
}
this.stdoutData = '';
this.tunnel = null;
if (options.hosts) {
var hosts = '';
options.hosts.forEach(function (host) {
if (hosts.length > 0) {
hosts += ',';
}
hosts += host.name + ',' + host.port + ',' + host.sslFlag;
});
params.push('--only', hosts);
}
if (options.localIdentifier) {
params.push('--local-identifier', options.localIdentifier);
}
if (options.v) {
params.push('--verbose');
}
if (options.force) {
params.push('--force');
}
if (options.forcelocal) {
params.push('--force-local');
}
if (options.onlyAutomate) {
params.push('--only-automate');
}
if (options.enableLoggingForApi) {
params.push('--enable-logging-for-api');
}
if (options.proxyHost) {
params.push('--proxy-host', options.proxyHost);
}
if (options.proxyPort) {
params.push('--proxy-port', options.proxyPort);
}
if (options.proxyUser) {
params.push('--proxy-user', options.proxyUser);
}
if (options.proxyPass) {
params.push('--proxy-pass', options.proxyPass);
}
this.state = 'stop';
this.stateMatchers = {
'already_running': new RegExp('\\*\\*Error: There is another JAR already running'),
'invalid_key': new RegExp('\\*\\*Error: You provided an invalid key'),
'connection_failure': new RegExp('\\*\\*Error: Could not connect to server'),
'newer_available': new RegExp('There is a new version of BrowserStackTunnel.jar available on server'),
'started': new RegExp('Press Ctrl-C to exit')
};
this.on('newer_available', function () {
console.log('BrowserStackTunnel: binary out of date');
this.killTunnel();
var self = this;
binary.update(options, function () {
self.startTunnel();
});
});
this.on('invalid_key', function () {
this.emit('started', new Error('Invalid key'));
});
this.on('connection_failure', function () {
this.emit('started', new Error('Could not connect to server'));
});
this.on('already_running', function () {
this.emit('started', new Error('child already started'));
});
this.updateState = function (data) {
var state;
this.stdoutData += data.toString();
for (state in this.stateMatchers) {
if (this.stateMatchers.hasOwnProperty(state) && this.stateMatchers[state].test(this.stdoutData) && this.state !== state) {
this.state = state;
this.emit(state, null);
break;
}
}
};
this.killTunnel = function () {
if (this.tunnel) {
this.tunnel.stdout.removeAllListeners('data');
this.tunnel.stderr.removeAllListeners('data');
this.tunnel.removeAllListeners('error');
this.tunnel.kill();
this.tunnel = null;
}
};
this.exit = function () {
if (this.state !== 'started' && this.state !== 'newer_available') {
this.emit('started', new Error('child failed to start:\n' + this.stdoutData));
} else if (this.state !== 'newer_available') {
this.state = 'stop';
this.emit('stop');
}
};
this.cleanUp = function () {
this.stdoutData = '';
process.removeListener('uncaughtException', this.exit.bind(this));
};
this._startTunnel = function () {
this.cleanUp();
this.tunnel = spawn(binary.command, binary.args.concat([options.key]).concat(params));
this.tunnel.stdout.on('data', this.updateState.bind(this));
this.tunnel.stderr.on('data', this.updateState.bind(this));
this.tunnel.on('error', this.killTunnel.bind(this));
this.tunnel.on('exit', this.exit.bind(this));
process.on('uncaughtException', this.killTunnel.bind(this));
};
this.startTunnel = function () {
if (!fs.existsSync(binary.path)) {
console.log('BrowserStackTunnel: binary not present');
var self = this;
binary.update(options, function () {
self._startTunnel();
});
} else {
this._startTunnel();
}
};
this.start = function (callback) {
this.once('started', callback);
if (this.state === 'started') {
this.emit('already_running');
} else {
this.startTunnel();
}
};
this.stop = function (callback) {
var listenerCount = (typeof this.listenerCount !== 'undefined') ? this.listenerCount('stop') : EventEmitter.listenerCount(this, 'stop');
if (this.state !== 'stop' && listenerCount === 0) {
this.once('stop', callback);
} else if (this.state !== 'started') {
var err = new Error('child not started');
this.emit('stop', err);
callback(err);
}
this.killTunnel();
};
}
util.inherits(BrowserStackTunnel, EventEmitter);
module.exports = BrowserStackTunnel;