1 | var SocketIO = require('socket.io')
|
2 | var di = require('di')
|
3 | var util = require('util')
|
4 | var Promise = require('bluebird')
|
5 |
|
6 | var root = global || window || this
|
7 |
|
8 | var cfg = require('./config')
|
9 | var logger = require('./logger')
|
10 | var constant = require('./constants')
|
11 | var watcher = require('./watcher')
|
12 | var plugin = require('./plugin')
|
13 |
|
14 | var ws = require('./web-server')
|
15 | var preprocessor = require('./preprocessor')
|
16 | var Launcher = require('./launcher').Launcher
|
17 | var FileList = require('./file-list')
|
18 | var reporter = require('./reporter')
|
19 | var helper = require('./helper')
|
20 | var events = require('./events')
|
21 | var EventEmitter = events.EventEmitter
|
22 | var Executor = require('./executor')
|
23 | var Browser = require('./browser')
|
24 | var BrowserCollection = require('./browser_collection')
|
25 | var EmitterWrapper = require('./emitter_wrapper')
|
26 | var processWrapper = new EmitterWrapper(process)
|
27 |
|
28 | function createSocketIoServer (webServer, executor, config) {
|
29 | var server = new SocketIO(webServer, {
|
30 |
|
31 | destroyUpgrade: false,
|
32 | path: config.urlRoot + 'socket.io/',
|
33 | transports: config.transports,
|
34 | forceJSONP: config.forceJSONP
|
35 | })
|
36 |
|
37 |
|
38 | executor.socketIoSockets = server.sockets
|
39 |
|
40 | return server
|
41 | }
|
42 |
|
43 | function setupLogger (level, colors) {
|
44 | var logLevel = logLevel || constant.LOG_INFO
|
45 | var logColors = helper.isDefined(colors) ? colors : true
|
46 | logger.setup(logLevel, logColors, [constant.CONSOLE_APPENDER])
|
47 | }
|
48 |
|
49 |
|
50 | var Server = function (cliOptions, done) {
|
51 | EventEmitter.call(this)
|
52 |
|
53 | setupLogger(cliOptions.logLevel, cliOptions.colors)
|
54 |
|
55 | this.log = logger.create()
|
56 |
|
57 | var config = cfg.parseConfig(cliOptions.configFile, cliOptions)
|
58 |
|
59 | var modules = [{
|
60 | helper: ['value', helper],
|
61 | logger: ['value', logger],
|
62 | done: ['value', done || process.exit],
|
63 | emitter: ['value', this],
|
64 | launcher: ['type', Launcher],
|
65 | config: ['value', config],
|
66 | preprocess: ['factory', preprocessor.createPreprocessor],
|
67 | fileList: ['type', FileList],
|
68 | webServer: ['factory', ws.create],
|
69 | socketServer: ['factory', createSocketIoServer],
|
70 | executor: ['type', Executor],
|
71 |
|
72 | customFileHandlers: ['value', []],
|
73 |
|
74 | customScriptTypes: ['value', []],
|
75 | reporter: ['factory', reporter.createReporters],
|
76 | capturedBrowsers: ['type', BrowserCollection],
|
77 | args: ['value', {}],
|
78 | timer: ['value', {
|
79 | setTimeout: function () {
|
80 | return setTimeout.apply(root, arguments)
|
81 | },
|
82 | clearTimeout: function (timeoutId) {
|
83 | clearTimeout(timeoutId)
|
84 | }
|
85 | }]
|
86 | }]
|
87 |
|
88 |
|
89 | modules = modules.concat(plugin.resolve(config.plugins))
|
90 |
|
91 | this._injector = new di.Injector(modules)
|
92 | }
|
93 |
|
94 |
|
95 | util.inherits(Server, EventEmitter)
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 | Server.prototype.start = function () {
|
102 | this._injector.invoke(this._start, this)
|
103 | }
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 | Server.start = function (cliOptions, done) {
|
110 | var server = new Server(cliOptions, done)
|
111 | server.start()
|
112 | }
|
113 |
|
114 |
|
115 |
|
116 |
|
117 | Server.prototype.get = function (token) {
|
118 | return this._injector.get(token)
|
119 | }
|
120 |
|
121 |
|
122 | Server.prototype.refreshFiles = function () {
|
123 | if (!this._fileList) return Promise.resolve()
|
124 |
|
125 | return this._fileList.refresh()
|
126 | }
|
127 |
|
128 |
|
129 |
|
130 |
|
131 | Server.prototype._start = function (config, launcher, preprocess, fileList, webServer,
|
132 | capturedBrowsers, socketServer, executor, done) {
|
133 | var self = this
|
134 |
|
135 | self._fileList = fileList
|
136 |
|
137 | config.frameworks.forEach(function (framework) {
|
138 | self._injector.get('framework:' + framework)
|
139 | })
|
140 |
|
141 |
|
142 | var singleRunDoneBrowsers = Object.create(null)
|
143 |
|
144 |
|
145 |
|
146 | var singleRunBrowsers = new BrowserCollection(new EventEmitter())
|
147 |
|
148 |
|
149 | var singleRunBrowserNotCaptured = false
|
150 |
|
151 | webServer.on('error', function (e) {
|
152 | if (e.code === 'EADDRINUSE') {
|
153 | self.log.warn('Port %d in use', config.port)
|
154 | config.port++
|
155 | webServer.listen(config.port)
|
156 | } else {
|
157 | throw e
|
158 | }
|
159 | })
|
160 |
|
161 | var afterPreprocess = function () {
|
162 | if (config.autoWatch) {
|
163 | self._injector.invoke(watcher.watch)
|
164 | }
|
165 |
|
166 | webServer.listen(config.port, function () {
|
167 | self.log.info('Karma v%s server started at %s//%s:%s%s', constant.VERSION,
|
168 | config.protocol, config.hostname, config.port, config.urlRoot)
|
169 |
|
170 | if (config.browsers && config.browsers.length) {
|
171 | self._injector.invoke(launcher.launch, launcher).forEach(function (browserLauncher) {
|
172 | singleRunDoneBrowsers[browserLauncher.id] = false
|
173 | })
|
174 | }
|
175 | })
|
176 | }
|
177 |
|
178 | fileList.refresh().then(afterPreprocess, afterPreprocess)
|
179 |
|
180 | self.on('browsers_change', function () {
|
181 |
|
182 | socketServer.sockets.emit('info', capturedBrowsers.serialize())
|
183 | })
|
184 |
|
185 | self.on('browser_register', function (browser) {
|
186 | launcher.markCaptured(browser.id)
|
187 |
|
188 |
|
189 |
|
190 | if (launcher.areAllCaptured()) {
|
191 | self.emit('browsers_ready')
|
192 |
|
193 | if (config.autoWatch) {
|
194 | executor.schedule()
|
195 | }
|
196 | }
|
197 | })
|
198 |
|
199 | var EVENTS_TO_REPLY = ['start', 'info', 'karma_error', 'result', 'complete']
|
200 | socketServer.sockets.on('connection', function (socket) {
|
201 | self.log.debug('A browser has connected on socket ' + socket.id)
|
202 |
|
203 | var replySocketEvents = events.bufferEvents(socket, EVENTS_TO_REPLY)
|
204 |
|
205 | socket.on('complete', function (data, ack) {
|
206 | ack()
|
207 | })
|
208 |
|
209 | socket.on('register', function (info) {
|
210 | var newBrowser
|
211 | var isRestart
|
212 |
|
213 | if (info.id) {
|
214 | newBrowser = capturedBrowsers.getById(info.id) || singleRunBrowsers.getById(info.id)
|
215 | }
|
216 |
|
217 | if (newBrowser) {
|
218 | isRestart = newBrowser.state === Browser.STATE_DISCONNECTED
|
219 | newBrowser.reconnect(socket)
|
220 |
|
221 |
|
222 | if (isRestart && config.singleRun) {
|
223 | newBrowser.execute(config.client)
|
224 | }
|
225 | } else {
|
226 | newBrowser = self._injector.createChild([{
|
227 | id: ['value', info.id || null],
|
228 | fullName: ['value', (helper.isDefined(info.displayName) ? info.displayName : info.name)],
|
229 | socket: ['value', socket]
|
230 | }]).instantiate(Browser)
|
231 |
|
232 | newBrowser.init()
|
233 |
|
234 |
|
235 | if (config.singleRun) {
|
236 | newBrowser.execute(config.client)
|
237 | singleRunBrowsers.add(newBrowser)
|
238 | }
|
239 | }
|
240 |
|
241 | replySocketEvents()
|
242 | })
|
243 | })
|
244 |
|
245 | var emitRunCompleteIfAllBrowsersDone = function () {
|
246 |
|
247 | var isDone = Object.keys(singleRunDoneBrowsers).reduce(function (isDone, id) {
|
248 | return isDone && singleRunDoneBrowsers[id]
|
249 | }, true)
|
250 |
|
251 | if (isDone) {
|
252 | var results = singleRunBrowsers.getResults()
|
253 | if (singleRunBrowserNotCaptured) {
|
254 | results.exitCode = 1
|
255 | } else if (results.success + results.failed === 0 && !config.failOnEmptyTestSuite) {
|
256 | results.exitCode = 0
|
257 | self.log.warn('Test suite was empty.')
|
258 | }
|
259 | self.emit('run_complete', singleRunBrowsers, results)
|
260 | }
|
261 | }
|
262 |
|
263 | if (config.singleRun) {
|
264 | self.on('browser_complete', function (completedBrowser) {
|
265 | if (completedBrowser.lastResult.disconnected &&
|
266 | completedBrowser.disconnectsCount <= config.browserDisconnectTolerance) {
|
267 | self.log.info('Restarting %s (%d of %d attempts)', completedBrowser.name,
|
268 | completedBrowser.disconnectsCount, config.browserDisconnectTolerance)
|
269 | if (!launcher.restart(completedBrowser.id)) {
|
270 | singleRunDoneBrowsers[completedBrowser.id] = true
|
271 | emitRunCompleteIfAllBrowsersDone()
|
272 | }
|
273 | } else {
|
274 | singleRunDoneBrowsers[completedBrowser.id] = true
|
275 |
|
276 | if (launcher.kill(completedBrowser.id)) {
|
277 |
|
278 | completedBrowser.state = Browser.STATE_DISCONNECTED
|
279 | }
|
280 |
|
281 | emitRunCompleteIfAllBrowsersDone()
|
282 | }
|
283 | })
|
284 |
|
285 | self.on('browser_process_failure', function (browserLauncher) {
|
286 | singleRunDoneBrowsers[browserLauncher.id] = true
|
287 | singleRunBrowserNotCaptured = true
|
288 |
|
289 | emitRunCompleteIfAllBrowsersDone()
|
290 | })
|
291 |
|
292 | self.on('run_complete', function (browsers, results) {
|
293 | self.log.debug('Run complete, exiting.')
|
294 | disconnectBrowsers(results.exitCode)
|
295 | })
|
296 |
|
297 | self.emit('run_start', singleRunBrowsers)
|
298 | }
|
299 |
|
300 | if (config.autoWatch) {
|
301 | self.on('file_list_modified', function () {
|
302 | self.log.debug('List of files has changed, trying to execute')
|
303 | if (config.restartOnFileChange) {
|
304 | socketServer.sockets.emit('stop')
|
305 | }
|
306 | executor.schedule()
|
307 | })
|
308 | }
|
309 |
|
310 | var webServerCloseTimeout = 3000
|
311 | var disconnectBrowsers = function (code) {
|
312 |
|
313 |
|
314 |
|
315 | var sockets = socketServer.sockets.sockets
|
316 |
|
317 | Object.keys(sockets).forEach(function (id) {
|
318 | var socket = sockets[id]
|
319 | socket.removeAllListeners('disconnect')
|
320 | if (!socket.disconnected) {
|
321 |
|
322 |
|
323 | process.nextTick(socket.disconnect.bind(socket))
|
324 | }
|
325 | })
|
326 |
|
327 | var removeAllListenersDone = false
|
328 | var removeAllListeners = function () {
|
329 |
|
330 | if (removeAllListenersDone) {
|
331 | return
|
332 | }
|
333 | removeAllListenersDone = true
|
334 | webServer.removeAllListeners()
|
335 | processWrapper.removeAllListeners()
|
336 | done(code || 0)
|
337 | }
|
338 |
|
339 | self.emitAsync('exit').then(function () {
|
340 |
|
341 |
|
342 | var closeTimeout = setTimeout(removeAllListeners, webServerCloseTimeout)
|
343 |
|
344 |
|
345 | webServer.close(function () {
|
346 | clearTimeout(closeTimeout)
|
347 | removeAllListeners()
|
348 | })
|
349 | })
|
350 | }
|
351 |
|
352 | processWrapper.on('SIGINT', disconnectBrowsers)
|
353 | processWrapper.on('SIGTERM', disconnectBrowsers)
|
354 |
|
355 |
|
356 |
|
357 | processWrapper.on('uncaughtException', function (error) {
|
358 | self.log.error(error)
|
359 | disconnectBrowsers(1)
|
360 | })
|
361 | }
|
362 |
|
363 |
|
364 |
|
365 |
|
366 | module.exports = Server
|