launcher.js 5.1 KB

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