'use strict' const Jobs = require('qjobs') const log = require('./logger').create('launcher') const baseDecorator = require('./launchers/base').decoratorFactory const captureTimeoutDecorator = require('./launchers/capture_timeout').decoratorFactory const retryDecorator = require('./launchers/retry').decoratorFactory const processDecorator = require('./launchers/process').decoratorFactory // TODO(vojta): remove once nobody uses it const baseBrowserDecoratorFactory = function ( baseLauncherDecorator, captureTimeoutLauncherDecorator, retryLauncherDecorator, processLauncherDecorator, processKillTimeout ) { return function (launcher) { baseLauncherDecorator(launcher) captureTimeoutLauncherDecorator(launcher) retryLauncherDecorator(launcher) processLauncherDecorator(launcher, processKillTimeout) } } class Launcher { constructor (server, emitter, injector) { this._server = server this._emitter = emitter this._injector = injector this._browsers = [] this._lastStartTime = null // Attach list of dependency injection parameters to methods. this.launch.$inject = [ 'config.browsers', 'config.concurrency' ] this.launchSingle.$inject = [ 'config.protocol', 'config.hostname', 'config.port', 'config.urlRoot', 'config.upstreamProxy', 'config.processKillTimeout' ] this._emitter.on('exit', (callback) => this.killAll(callback)) } getBrowserById (id) { return this._browsers.find((browser) => browser.id === id) } launchSingle (protocol, hostname, port, urlRoot, upstreamProxy, processKillTimeout) { if (upstreamProxy) { protocol = upstreamProxy.protocol hostname = upstreamProxy.hostname port = upstreamProxy.port urlRoot = upstreamProxy.path + urlRoot.slice(1) } return (name) => { let browser const locals = { id: ['value', Launcher.generateId()], name: ['value', name], processKillTimeout: ['value', processKillTimeout], baseLauncherDecorator: ['factory', baseDecorator], captureTimeoutLauncherDecorator: ['factory', captureTimeoutDecorator], retryLauncherDecorator: ['factory', retryDecorator], processLauncherDecorator: ['factory', processDecorator], baseBrowserDecorator: ['factory', baseBrowserDecoratorFactory] } // TODO(vojta): determine script from name if (name.includes('/')) { name = 'Script' } try { browser = this._injector.createChild([locals], ['launcher:' + name]).get('launcher:' + name) } catch (e) { if (e.message.includes(`No provider for "launcher:${name}"`)) { log.error(`Cannot load browser "${name}": it is not registered! Perhaps you are missing some plugin?`) } else { log.error(`Cannot load browser "${name}"!\n ` + e.stack) } this._emitter.emit('load_error', 'launcher', name) return } this.jobs.add((args, done) => { log.info(`Starting browser ${browser.displayName || browser.name}`) browser.on('browser_process_failure', () => done(browser.error)) browser.on('done', () => { if (!browser.error && browser.state !== browser.STATE_RESTARTING) { done(null, browser) } }) browser.start(`${protocol}//${hostname}:${port}${urlRoot}`) }, []) this.jobs.run() this._browsers.push(browser) } } launch (names, concurrency) { log.info(`Launching browsers ${names.join(', ')} with concurrency ${concurrency === Infinity ? 'unlimited' : concurrency}`) this.jobs = new Jobs({ maxConcurrency: concurrency }) this._lastStartTime = Date.now() if (this._server.loadErrors.length) { this.jobs.add((args, done) => done(), []) } else { names.forEach((name) => this._injector.invoke(this.launchSingle, this)(name)) } this.jobs.on('end', (err) => { log.debug('Finished all browsers') if (err) { log.error(err) } }) this.jobs.run() return this._browsers } kill (id, callback) { callback = callback || function () {} const browser = this.getBrowserById(id) if (browser) { browser.forceKill().then(callback) return true } process.nextTick(callback) return false } restart (id) { const browser = this.getBrowserById(id) if (browser) { browser.restart() return true } return false } killAll (callback) { callback = callback || function () {} log.debug('Disconnecting all browsers') if (!this._browsers.length) { return process.nextTick(callback) } Promise.all( this._browsers .map((browser) => browser.forceKill()) ).then(callback) } areAllCaptured () { return this._browsers.every((browser) => browser.isCaptured()) } markCaptured (id) { const browser = this.getBrowserById(id) if (browser) { browser.markCaptured() log.debug(`${browser.name} (id ${browser.id}) captured in ${(Date.now() - this._lastStartTime) / 1000} secs`) } } static generateId () { return Math.floor(Math.random() * 100000000).toString() } } Launcher.factory = function (server, emitter, injector) { return new Launcher(server, emitter, injector) } Launcher.factory.$inject = ['server', 'emitter', 'injector'] exports.Launcher = Launcher