browser.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. 'use strict'
  2. const BrowserResult = require('./browser_result')
  3. const helper = require('./helper')
  4. const logger = require('./logger')
  5. const CONNECTED = 'CONNECTED' // The browser is connected but not yet been commanded to execute tests.
  6. const CONFIGURING = 'CONFIGURING' // The browser has been told to execute tests; it is configuring before tests execution.
  7. const EXECUTING = 'EXECUTING' // The browser is executing the tests.
  8. const EXECUTING_DISCONNECTED = 'EXECUTING_DISCONNECTED' // The browser is executing the tests, but temporarily disconnect (waiting for socket reconnecting).
  9. const DISCONNECTED = 'DISCONNECTED' // The browser got completely disconnected (e.g. browser crash) and can be only restored with a restart of execution.
  10. class Browser {
  11. constructor (id, fullName, collection, emitter, socket, timer, disconnectDelay, noActivityTimeout) {
  12. this.id = id
  13. this.fullName = fullName
  14. this.name = helper.browserFullNameToShort(fullName)
  15. this.lastResult = new BrowserResult()
  16. this.disconnectsCount = 0
  17. this.activeSockets = [socket]
  18. this.noActivityTimeout = noActivityTimeout
  19. this.collection = collection
  20. this.emitter = emitter
  21. this.socket = socket
  22. this.timer = timer
  23. this.disconnectDelay = disconnectDelay
  24. this.log = logger.create(this.name)
  25. this.noActivityTimeoutId = null
  26. this.pendingDisconnect = null
  27. this.setState(CONNECTED)
  28. }
  29. init () {
  30. this.log.info(`Connected on socket ${this.socket.id} with id ${this.id}`)
  31. this.bindSocketEvents(this.socket)
  32. this.collection.add(this)
  33. this.emitter.emit('browser_register', this)
  34. }
  35. setState (toState) {
  36. this.log.debug(`${this.state} -> ${toState}`)
  37. this.state = toState
  38. }
  39. onKarmaError (error) {
  40. if (this.isNotConnected()) {
  41. this.lastResult.error = true
  42. }
  43. this.emitter.emit('browser_error', this, error)
  44. this.refreshNoActivityTimeout()
  45. }
  46. onInfo (info) {
  47. if (helper.isDefined(info.dump)) {
  48. this.emitter.emit('browser_log', this, info.dump, 'dump')
  49. }
  50. if (helper.isDefined(info.log)) {
  51. this.emitter.emit('browser_log', this, info.log, info.type)
  52. } else if (helper.isDefined(info.total)) {
  53. if (this.state === EXECUTING) {
  54. this.lastResult.total = info.total
  55. }
  56. } else if (!helper.isDefined(info.dump)) {
  57. this.emitter.emit('browser_info', this, info)
  58. }
  59. this.refreshNoActivityTimeout()
  60. }
  61. onStart (info) {
  62. if (info.total === null) {
  63. this.log.warn('Adapter did not report total number of specs.')
  64. }
  65. this.lastResult = new BrowserResult(info.total)
  66. this.setState(EXECUTING)
  67. this.emitter.emit('browser_start', this, info)
  68. this.refreshNoActivityTimeout()
  69. }
  70. onComplete (result) {
  71. if (this.isNotConnected()) {
  72. this.setState(CONNECTED)
  73. this.lastResult.totalTimeEnd()
  74. if (!this.lastResult.success) {
  75. this.lastResult.error = true
  76. }
  77. this.emitter.emit('browsers_change', this.collection)
  78. this.emitter.emit('browser_complete', this, result)
  79. this.clearNoActivityTimeout()
  80. }
  81. }
  82. onDisconnect (reason, disconnectedSocket) {
  83. helper.arrayRemove(this.activeSockets, disconnectedSocket)
  84. if (this.activeSockets.length) {
  85. this.log.debug(`Disconnected ${disconnectedSocket.id}, still have ${this.getActiveSocketsIds()}`)
  86. return
  87. }
  88. if (this.isConnected()) {
  89. this.disconnect(`Client disconnected from CONNECTED state (${reason})`)
  90. } else if ([CONFIGURING, EXECUTING].includes(this.state)) {
  91. this.log.debug(`Disconnected during run, waiting ${this.disconnectDelay}ms for reconnecting.`)
  92. this.setState(EXECUTING_DISCONNECTED)
  93. this.pendingDisconnect = this.timer.setTimeout(() => {
  94. this.lastResult.totalTimeEnd()
  95. this.lastResult.disconnected = true
  96. this.disconnect(`reconnect failed before timeout of ${this.disconnectDelay}ms (${reason})`)
  97. this.emitter.emit('browser_complete', this)
  98. }, this.disconnectDelay)
  99. this.clearNoActivityTimeout()
  100. }
  101. }
  102. reconnect (newSocket) {
  103. if (this.state === EXECUTING_DISCONNECTED) {
  104. this.log.debug(`Lost socket connection, but browser continued to execute. Reconnected ` +
  105. `on socket ${newSocket.id}.`)
  106. this.setState(EXECUTING)
  107. } else if ([CONNECTED, CONFIGURING, EXECUTING].includes(this.state)) {
  108. this.log.debug(`Rebinding to new socket ${newSocket.id} (already have ` +
  109. `${this.getActiveSocketsIds()})`)
  110. } else if (this.state === DISCONNECTED) {
  111. this.log.info(`Disconnected browser returned on socket ${newSocket.id} with id ${this.id}.`)
  112. this.setState(CONNECTED)
  113. // Since the disconnected browser is already part of the collection and we want to
  114. // make sure that the server can properly handle the browser like it's the first time
  115. // connecting this browser (as we want a complete new execution), we need to emit the
  116. // following events:
  117. this.emitter.emit('browsers_change', this.collection)
  118. this.emitter.emit('browser_register', this)
  119. }
  120. if (!this.activeSockets.some((s) => s.id === newSocket.id)) {
  121. this.activeSockets.push(newSocket)
  122. this.bindSocketEvents(newSocket)
  123. }
  124. if (this.pendingDisconnect) {
  125. this.timer.clearTimeout(this.pendingDisconnect)
  126. }
  127. this.refreshNoActivityTimeout()
  128. }
  129. onResult (result) {
  130. if (result.length) {
  131. result.forEach(this.onResult, this)
  132. return
  133. } else if (this.isNotConnected()) {
  134. this.lastResult.add(result)
  135. this.emitter.emit('spec_complete', this, result)
  136. }
  137. this.refreshNoActivityTimeout()
  138. }
  139. execute (config) {
  140. this.activeSockets.forEach((socket) => socket.emit('execute', config))
  141. this.setState(CONFIGURING)
  142. this.refreshNoActivityTimeout()
  143. }
  144. getActiveSocketsIds () {
  145. return this.activeSockets.map((s) => s.id).join(', ')
  146. }
  147. disconnect (reason) {
  148. this.log.warn(`Disconnected (${this.disconnectsCount} times)${reason || ''}`)
  149. this.setState(DISCONNECTED)
  150. this.disconnectsCount++
  151. this.emitter.emit('browser_error', this, `Disconnected${reason || ''}`)
  152. this.collection.remove(this)
  153. }
  154. refreshNoActivityTimeout () {
  155. if (this.noActivityTimeout) {
  156. this.clearNoActivityTimeout()
  157. this.noActivityTimeoutId = this.timer.setTimeout(() => {
  158. this.lastResult.totalTimeEnd()
  159. this.lastResult.disconnected = true
  160. this.disconnect(`, because no message in ${this.noActivityTimeout} ms.`)
  161. this.emitter.emit('browser_complete', this)
  162. }, this.noActivityTimeout)
  163. }
  164. }
  165. clearNoActivityTimeout () {
  166. if (this.noActivityTimeout && this.noActivityTimeoutId) {
  167. this.timer.clearTimeout(this.noActivityTimeoutId)
  168. this.noActivityTimeoutId = null
  169. }
  170. }
  171. bindSocketEvents (socket) {
  172. // TODO: check which of these events are actually emitted by socket
  173. socket.on('disconnect', (reason) => this.onDisconnect(reason, socket))
  174. socket.on('start', (info) => this.onStart(info))
  175. socket.on('karma_error', (error) => this.onKarmaError(error))
  176. socket.on('complete', (result) => this.onComplete(result))
  177. socket.on('info', (info) => this.onInfo(info))
  178. socket.on('result', (result) => this.onResult(result))
  179. }
  180. isConnected () {
  181. return this.state === CONNECTED
  182. }
  183. isNotConnected () {
  184. return !this.isConnected()
  185. }
  186. serialize () {
  187. return {
  188. id: this.id,
  189. name: this.name,
  190. isConnected: this.state === CONNECTED
  191. }
  192. }
  193. toString () {
  194. return this.name
  195. }
  196. toJSON () {
  197. return {
  198. id: this.id,
  199. fullName: this.fullName,
  200. name: this.name,
  201. state: this.state,
  202. lastResult: this.lastResult,
  203. disconnectsCount: this.disconnectsCount,
  204. noActivityTimeout: this.noActivityTimeout,
  205. disconnectDelay: this.disconnectDelay
  206. }
  207. }
  208. }
  209. Browser.factory = function (
  210. id, fullName, /* capturedBrowsers */ collection, emitter, socket, timer,
  211. /* config.browserDisconnectTimeout */ disconnectDelay,
  212. /* config.browserNoActivityTimeout */ noActivityTimeout
  213. ) {
  214. return new Browser(id, fullName, collection, emitter, socket, timer, disconnectDelay, noActivityTimeout)
  215. }
  216. Browser.STATE_CONNECTED = CONNECTED
  217. Browser.STATE_CONFIGURING = CONFIGURING
  218. Browser.STATE_EXECUTING = EXECUTING
  219. Browser.STATE_EXECUTING_DISCONNECTED = EXECUTING_DISCONNECTED
  220. Browser.STATE_DISCONNECTED = DISCONNECTED
  221. module.exports = Browser