UNPKG

11 kBJavaScriptView Raw
1var SocketIO = require('socket.io')
2var di = require('di')
3var util = require('util')
4var Promise = require('bluebird')
5
6var root = global || window || this
7
8var cfg = require('./config')
9var logger = require('./logger')
10var constant = require('./constants')
11var watcher = require('./watcher')
12var plugin = require('./plugin')
13
14var ws = require('./web-server')
15var preprocessor = require('./preprocessor')
16var Launcher = require('./launcher').Launcher
17var FileList = require('./file-list')
18var reporter = require('./reporter')
19var helper = require('./helper')
20var events = require('./events')
21var EventEmitter = events.EventEmitter
22var Executor = require('./executor')
23var Browser = require('./browser')
24var BrowserCollection = require('./browser_collection')
25var EmitterWrapper = require('./emitter_wrapper')
26var processWrapper = new EmitterWrapper(process)
27
28function createSocketIoServer (webServer, executor, config) {
29 var server = new SocketIO(webServer, {
30 // avoid destroying http upgrades from socket.io to get proxied websockets working
31 destroyUpgrade: false,
32 path: config.urlRoot + 'socket.io/',
33 transports: config.transports,
34 forceJSONP: config.forceJSONP
35 })
36
37 // hack to overcome circular dependency
38 executor.socketIoSockets = server.sockets
39
40 return server
41}
42
43function 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// Constructor
50var 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 // TODO(vojta): remove
72 customFileHandlers: ['value', []],
73 // TODO(vojta): remove, once karma-dart does not rely on it
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 // Load the plugins
89 modules = modules.concat(plugin.resolve(config.plugins))
90
91 this._injector = new di.Injector(modules)
92}
93
94// Inherit from events.EventEmitter
95util.inherits(Server, EventEmitter)
96
97// Public Methods
98// --------------
99
100// Start the server
101Server.prototype.start = function () {
102 this._injector.invoke(this._start, this)
103}
104
105/**
106 * Backward-compatibility with karma-intellij bundled with WebStorm.
107 * Deprecated since version 0.13, to be removed in 0.14
108 */
109Server.start = function (cliOptions, done) {
110 var server = new Server(cliOptions, done)
111 server.start()
112}
113
114// Get properties from the injector
115//
116// token - String
117Server.prototype.get = function (token) {
118 return this._injector.get(token)
119}
120
121// Force a refresh of the file list
122Server.prototype.refreshFiles = function () {
123 if (!this._fileList) return Promise.resolve()
124
125 return this._fileList.refresh()
126}
127
128// Private Methods
129// ---------------
130
131Server.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 // A map of launched browsers.
142 var singleRunDoneBrowsers = Object.create(null)
143
144 // Passing fake event emitter, so that it does not emit on the global,
145 // we don't care about these changes.
146 var singleRunBrowsers = new BrowserCollection(new EventEmitter())
147
148 // Some browsers did not get captured.
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 // TODO(vojta): send only to interested browsers
182 socketServer.sockets.emit('info', capturedBrowsers.serialize())
183 })
184
185 self.on('browser_register', function (browser) {
186 launcher.markCaptured(browser.id)
187
188 // TODO(vojta): This is lame, browser can get captured and then
189 // crash (before other browsers get captured).
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 // We are restarting a previously disconnected browser.
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 // execute in this browser immediately
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 // all browsers done
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 // workaround to supress "disconnect" warning
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 // Slightly hacky way of removing disconnect listeners
313 // to suppress "browser disconnect" warnings
314 // TODO(vojta): change the client to not send the event (if disconnected by purpose)
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 // Disconnect asynchronously. Socket.io mutates the `sockets.sockets` array
322 // underneath us so this would skip every other browser/socket.
323 process.nextTick(socket.disconnect.bind(socket))
324 }
325 })
326
327 var removeAllListenersDone = false
328 var removeAllListeners = function () {
329 // make sure we don't execute cleanup twice
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 // don't wait forever on webServer.close() because
341 // pending client connections prevent it from closing.
342 var closeTimeout = setTimeout(removeAllListeners, webServerCloseTimeout)
343
344 // shutdown the server...
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 // Handle all unhandled exceptions, so we don't just exit but
356 // disconnect the browsers before exiting.
357 processWrapper.on('uncaughtException', function (error) {
358 self.log.error(error)
359 disconnectBrowsers(1)
360 })
361}
362
363// Export
364// ------
365
366module.exports = Server