launcher.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. 'use strict'
  2. const Promise = require('bluebird')
  3. const Jobs = require('qjobs')
  4. const log = require('./logger').create('launcher')
  5. const baseDecorator = require('./launchers/base').decoratorFactory
  6. const captureTimeoutDecorator = require('./launchers/capture_timeout').decoratorFactory
  7. const retryDecorator = require('./launchers/retry').decoratorFactory
  8. const processDecorator = require('./launchers/process').decoratorFactory
  9. // TODO(vojta): remove once nobody uses it
  10. const baseBrowserDecoratorFactory = function (
  11. baseLauncherDecorator,
  12. captureTimeoutLauncherDecorator,
  13. retryLauncherDecorator,
  14. processLauncherDecorator,
  15. processKillTimeout
  16. ) {
  17. return function (launcher) {
  18. baseLauncherDecorator(launcher)
  19. captureTimeoutLauncherDecorator(launcher)
  20. retryLauncherDecorator(launcher)
  21. processLauncherDecorator(launcher, processKillTimeout)
  22. }
  23. }
  24. function Launcher (server, emitter, injector) {
  25. this._browsers = []
  26. let lastStartTime
  27. const getBrowserById = (id) => this._browsers.find((browser) => browser.id === id)
  28. this.launchSingle = (protocol, hostname, port, urlRoot, upstreamProxy, processKillTimeout) => {
  29. if (upstreamProxy) {
  30. protocol = upstreamProxy.protocol
  31. hostname = upstreamProxy.hostname
  32. port = upstreamProxy.port
  33. urlRoot = upstreamProxy.path + urlRoot.substr(1)
  34. }
  35. return (name) => {
  36. let browser
  37. const locals = {
  38. id: ['value', Launcher.generateId()],
  39. name: ['value', name],
  40. processKillTimeout: ['value', processKillTimeout],
  41. baseLauncherDecorator: ['factory', baseDecorator],
  42. captureTimeoutLauncherDecorator: ['factory', captureTimeoutDecorator],
  43. retryLauncherDecorator: ['factory', retryDecorator],
  44. processLauncherDecorator: ['factory', processDecorator],
  45. baseBrowserDecorator: ['factory', baseBrowserDecoratorFactory]
  46. }
  47. // TODO(vojta): determine script from name
  48. if (name.includes('/')) {
  49. name = 'Script'
  50. }
  51. try {
  52. browser = injector.createChild([locals], ['launcher:' + name]).get('launcher:' + name)
  53. } catch (e) {
  54. if (e.message.includes(`No provider for "launcher:${name}"`)) {
  55. log.error(`Cannot load browser "${name}": it is not registered! Perhaps you are missing some plugin?`)
  56. } else {
  57. log.error(`Cannot load browser "${name}"!\n ` + e.stack)
  58. }
  59. emitter.emit('load_error', 'launcher', name)
  60. return
  61. }
  62. this.jobs.add((args, done) => {
  63. log.info(`Starting browser ${browser.displayName || browser.name}`)
  64. browser.on('browser_process_failure', () => done(browser.error))
  65. browser.on('done', () => {
  66. if (!browser.error && browser.state !== browser.STATE_RESTARTING) {
  67. done(null, browser)
  68. }
  69. })
  70. browser.start(`${protocol}//${hostname}:${port}${urlRoot}`)
  71. }, [])
  72. this.jobs.run()
  73. this._browsers.push(browser)
  74. }
  75. }
  76. this.launch = (names, concurrency) => {
  77. log.info(`Launching browsers ${names.join(', ')} with concurrency ${concurrency === Infinity ? 'unlimited' : concurrency}`)
  78. this.jobs = new Jobs({ maxConcurrency: concurrency })
  79. lastStartTime = Date.now()
  80. if (server.loadErrors.length) {
  81. this.jobs.add((args, done) => done(), [])
  82. } else {
  83. names.forEach((name) => injector.invoke(this.launchSingle, this)(name))
  84. }
  85. this.jobs.on('end', (err) => {
  86. log.debug('Finished all browsers')
  87. if (err) {
  88. log.error(err)
  89. }
  90. })
  91. this.jobs.run()
  92. return this._browsers
  93. }
  94. this.launch.$inject = [
  95. 'config.browsers',
  96. 'config.concurrency',
  97. 'config.processKillTimeout'
  98. ]
  99. this.launchSingle.$inject = [
  100. 'config.protocol',
  101. 'config.hostname',
  102. 'config.port',
  103. 'config.urlRoot',
  104. 'config.upstreamProxy',
  105. 'config.processKillTimeout'
  106. ]
  107. this.kill = (id, callback) => {
  108. callback = callback || function () {}
  109. const browser = getBrowserById(id)
  110. if (browser) {
  111. browser.forceKill().then(callback)
  112. return true
  113. }
  114. process.nextTick(callback)
  115. return false
  116. }
  117. this.restart = (id) => {
  118. const browser = getBrowserById(id)
  119. if (browser) {
  120. browser.restart()
  121. return true
  122. }
  123. return false
  124. }
  125. this.killAll = (callback) => {
  126. callback = callback || function () {}
  127. log.debug('Disconnecting all browsers')
  128. if (!this._browsers.length) {
  129. return process.nextTick(callback)
  130. }
  131. Promise.all(
  132. this._browsers
  133. .map((browser) => browser.forceKill())
  134. ).then(callback)
  135. }
  136. this.areAllCaptured = () => this._browsers.every((browser) => browser.isCaptured())
  137. this.markCaptured = (id) => {
  138. const browser = getBrowserById(id)
  139. if (browser) {
  140. browser.markCaptured()
  141. log.debug(`${browser.name} (id ${browser.id}) captured in ${(Date.now() - lastStartTime) / 1000} secs`)
  142. }
  143. }
  144. emitter.on('exit', this.killAll)
  145. }
  146. Launcher.$inject = ['server', 'emitter', 'injector']
  147. Launcher.generateId = () => Math.floor(Math.random() * 100000000).toString()
  148. exports.Launcher = Launcher