launcher.js 5.3 KB

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