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