331 lines
8.9 KiB
JavaScript
331 lines
8.9 KiB
JavaScript
var q = require('q')
|
|
var api = require('browserstack')
|
|
var BrowserStackTunnel = require('browserstacktunnel-wrapper')
|
|
var os = require('os')
|
|
var workerManager = require('./worker-manager')
|
|
var BrowserStackReporter = require('./browserstack-reporter')
|
|
|
|
var createBrowserStackTunnel = function (logger, config, emitter) {
|
|
var log = logger.create('launcher.browserstack')
|
|
var bsConfig = config.browserStack || {}
|
|
var bsBinaryBasePath = process.env.BROWSER_STACK_BINARY_BASE_PATH || bsConfig.binaryBasePath || null
|
|
var bsRunConfig = {
|
|
key: process.env.BROWSER_STACK_ACCESS_KEY || bsConfig.accessKey,
|
|
localIdentifier: bsConfig.localIdentifier || bsConfig.tunnelIdentifier,
|
|
jarFile: process.env.BROWSER_STACK_TUNNEL_JAR || bsConfig.jarFile,
|
|
hosts: [{
|
|
name: config.hostname,
|
|
port: config.port,
|
|
sslFlag: 0
|
|
}],
|
|
proxyHost: bsConfig.proxyHost || null,
|
|
proxyPort: bsConfig.proxyPort || null,
|
|
proxyUser: bsConfig.proxyUser || null,
|
|
proxyPass: bsConfig.proxyPass || null,
|
|
forcelocal: bsConfig.forcelocal || null,
|
|
enableLoggingForApi: bsConfig.enableLoggingForApi || null
|
|
}
|
|
|
|
if (bsConfig.startTunnel === false) {
|
|
bsConfig.tunnelIdentifier = bsRunConfig.localIdentifier
|
|
return q()
|
|
}
|
|
|
|
bsRunConfig.localIdentifier = bsRunConfig.localIdentifier || 'karma' + Math.random()
|
|
bsConfig.tunnelIdentifier = bsRunConfig.localIdentifier
|
|
|
|
if (bsBinaryBasePath) {
|
|
switch (os.platform()) {
|
|
case 'linux':
|
|
switch (os.arch()) {
|
|
case 'x64':
|
|
bsRunConfig.linux64Bin = bsBinaryBasePath
|
|
break
|
|
case 'ia32':
|
|
bsRunConfig.linux32Bin = bsBinaryBasePath
|
|
break
|
|
}
|
|
break
|
|
case 'darwin':
|
|
bsRunConfig.osxBin = bsBinaryBasePath
|
|
break
|
|
default:
|
|
bsRunConfig.win32Bin = bsBinaryBasePath
|
|
break
|
|
}
|
|
}
|
|
|
|
log.debug('Establishing the tunnel on %s:%s', config.hostname, config.port)
|
|
|
|
var deferred = q.defer()
|
|
var tunnel = new BrowserStackTunnel(bsRunConfig)
|
|
|
|
tunnel.start(function (error) {
|
|
if (error) {
|
|
log.error('Can not establish the tunnel.\n%s', error.toString())
|
|
deferred.reject(error)
|
|
} else {
|
|
log.debug('Tunnel established.')
|
|
deferred.resolve()
|
|
}
|
|
})
|
|
|
|
emitter.on('exit', function (done) {
|
|
log.debug('Shutting down the tunnel.')
|
|
tunnel.stop(function (error) {
|
|
if (error) {
|
|
log.error(error)
|
|
}
|
|
|
|
if (workerManager.isPolling) {
|
|
workerManager.stopPolling()
|
|
}
|
|
|
|
done()
|
|
})
|
|
})
|
|
|
|
return deferred.promise
|
|
}
|
|
|
|
var createBrowserStackClient = function (/* config.browserStack */config, /* BrowserStack:sessionMapping */ sessionMapping) {
|
|
var env = process.env
|
|
|
|
config = config || {}
|
|
|
|
var options = {
|
|
username: env.BROWSER_STACK_USERNAME || config.username,
|
|
password: env.BROWSER_STACK_ACCESS_KEY || config.accessKey
|
|
}
|
|
|
|
if (config.proxyHost && config.proxyPort) {
|
|
config.proxyProtocol = config.proxyProtocol || 'http'
|
|
var proxyAuth = (config.proxyUser && config.proxyPass)
|
|
? (encodeURIComponent(config.proxyUser) + ':' + encodeURIComponent(config.proxyPass) + '@') : ''
|
|
options.proxy = config.proxyProtocol + '://' + proxyAuth + config.proxyHost + ':' + config.proxyPort
|
|
}
|
|
|
|
sessionMapping.credentials = {
|
|
username: options.username,
|
|
password: options.password,
|
|
proxy: options.proxy
|
|
}
|
|
|
|
// TODO(vojta): handle no username/pwd
|
|
var client = api.createClient(options)
|
|
|
|
var pollingTimeout = config.pollingTimeout || 1000
|
|
|
|
if (!workerManager.isPolling) {
|
|
workerManager.startPolling(client, pollingTimeout, function (err) {
|
|
if (err) {
|
|
console.error(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
return client
|
|
}
|
|
|
|
var formatError = function (error) {
|
|
if (error.message === 'Validation Failed') {
|
|
return ' Validation Failed: you probably misconfigured the browser ' +
|
|
'or given browser is not available.'
|
|
}
|
|
|
|
return error.toString()
|
|
}
|
|
|
|
var BrowserStackBrowser = function (
|
|
id, emitter, args, logger,
|
|
/* config */ config,
|
|
/* browserStackTunnel */ tunnel,
|
|
/* browserStackClient */ client,
|
|
baseLauncherDecorator,
|
|
captureTimeoutLauncherDecorator,
|
|
retryLauncherDecorator,
|
|
/* BrowserStack:sessionMapping */ sessionMapping
|
|
) {
|
|
var self = this
|
|
|
|
baseLauncherDecorator(self)
|
|
captureTimeoutLauncherDecorator(self)
|
|
retryLauncherDecorator(self)
|
|
|
|
var workerId = null
|
|
var captured = false
|
|
var alreadyKilling = null
|
|
var log = logger.create('launcher.browserstack')
|
|
var browserName = (args.browser || args.device) + (args.browser_version ? ' ' + args.browser_version : '') +
|
|
' (' + args.os + ' ' + args.os_version + ')'
|
|
|
|
this.id = id
|
|
this.name = browserName + ' on BrowserStack'
|
|
|
|
var bsConfig = config.browserStack || {}
|
|
var captureTimeout = config.captureTimeout || 0
|
|
var captureTimeoutId
|
|
var retryLimit = bsConfig.retryLimit || 3
|
|
var previousUrl = null
|
|
|
|
this.start = function (url) {
|
|
url = url || previousUrl
|
|
previousUrl = url
|
|
|
|
var globalSettings = Object.assign(
|
|
{
|
|
timeout: 300,
|
|
name: 'Karma test',
|
|
build: process.env.BUILD_NUMBER ||
|
|
process.env.BUILD_TAG ||
|
|
process.env.CI_BUILD_NUMBER ||
|
|
process.env.CI_BUILD_TAG ||
|
|
process.env.TRAVIS_BUILD_NUMBER ||
|
|
process.env.CIRCLE_BUILD_NUM ||
|
|
process.env.DRONE_BUILD_NUMBER || null,
|
|
// TODO(vojta): remove "version" (only for B-C)
|
|
browser_version: args.version || 'latest',
|
|
video: true
|
|
},
|
|
bsConfig
|
|
)
|
|
|
|
// TODO(vojta): handle non os/browser/version
|
|
var settings = Object.assign(
|
|
{
|
|
url: url + '?id=' + id,
|
|
'browserstack.tunnel': true
|
|
},
|
|
globalSettings,
|
|
args
|
|
)
|
|
|
|
tunnel.then(function () {
|
|
client.createWorker(settings, function (error, worker) {
|
|
var sessionUrlShowed = false
|
|
|
|
if (error) {
|
|
log.error('Can not start %s\n %s', browserName, formatError(error))
|
|
return emitter.emit('browser_process_failure', self)
|
|
}
|
|
|
|
workerId = worker.id
|
|
alreadyKilling = null
|
|
|
|
worker = workerManager.registerWorker(worker)
|
|
worker.on('status', function (status) {
|
|
// TODO(vojta): show immediately in createClient callback once this gets fixed:
|
|
// https://github.com/browserstack/api/issues/10
|
|
if (!sessionUrlShowed) {
|
|
log.info('%s session at %s', browserName, worker.browser_url)
|
|
sessionMapping[self.id] = worker.browser_url.split('/').slice(-1)[0]
|
|
sessionUrlShowed = true
|
|
}
|
|
|
|
switch (status) {
|
|
case 'running':
|
|
log.debug('%s job started with id %s', browserName, workerId)
|
|
|
|
if (captureTimeout && !captured) {
|
|
captureTimeoutId = setTimeout(self._onTimeout, captureTimeout)
|
|
}
|
|
break
|
|
|
|
case 'queue':
|
|
log.debug('%s job with id %s in queue.', browserName, workerId)
|
|
break
|
|
|
|
case 'delete':
|
|
log.debug('%s job with id %s has been deleted.', browserName, workerId)
|
|
break
|
|
}
|
|
})
|
|
})
|
|
}).catch(function () {
|
|
emitter.emit('browser_process_failure', self)
|
|
})
|
|
}
|
|
|
|
this.kill = function (done) {
|
|
var allDone = function () {
|
|
self._done()
|
|
if (done) {
|
|
done()
|
|
}
|
|
}
|
|
|
|
if (!alreadyKilling) {
|
|
alreadyKilling = q.defer()
|
|
|
|
if (workerId) {
|
|
log.debug('Killing %s (worker %s).', browserName, workerId)
|
|
client.terminateWorker(workerId, function () {
|
|
log.debug('%s (worker %s) successfully killed.', browserName, workerId)
|
|
|
|
if (captureTimeoutId) {
|
|
clearTimeout(captureTimeoutId)
|
|
captureTimeoutId = null
|
|
}
|
|
|
|
workerId = null
|
|
captured = false
|
|
alreadyKilling.resolve()
|
|
})
|
|
} else {
|
|
alreadyKilling.resolve()
|
|
}
|
|
}
|
|
|
|
return alreadyKilling.promise.then(allDone)
|
|
}
|
|
|
|
this.forceKill = function () {
|
|
var self = this
|
|
|
|
return q.promise(function (resolve) {
|
|
self.kill(resolve)
|
|
})
|
|
}
|
|
|
|
this.markCaptured = function () {
|
|
captured = true
|
|
|
|
if (captureTimeoutId) {
|
|
clearTimeout(captureTimeoutId)
|
|
captureTimeoutId = null
|
|
}
|
|
}
|
|
|
|
this.isCaptured = function () {
|
|
return captured
|
|
}
|
|
|
|
this.toString = function () {
|
|
return this.name
|
|
}
|
|
|
|
this._onTimeout = function () {
|
|
if (captured) {
|
|
return
|
|
}
|
|
|
|
log.warn('%s has not captured in %d ms, killing.', browserName, captureTimeout)
|
|
self.kill(function () {
|
|
if (retryLimit--) {
|
|
self.start(previousUrl)
|
|
} else {
|
|
emitter.emit('browser_process_failure', self)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// PUBLISH DI MODULE
|
|
module.exports = {
|
|
'browserStackTunnel': ['factory', createBrowserStackTunnel],
|
|
'browserStackClient': ['factory', createBrowserStackClient],
|
|
'launcher:BrowserStack': ['type', BrowserStackBrowser],
|
|
'reporter:BrowserStack': ['type', BrowserStackReporter],
|
|
'BrowserStack:sessionMapping': ['value', {}]
|
|
}
|