1 var io = require('socket.io')
4 var cfg = require('./config')
5 var logger = require('./logger')
6 var constant = require('./constants')
7 var watcher = require('./watcher')
8 var plugin = require('./plugin')
10 var ws = require('./web-server')
11 var preprocessor = require('./preprocessor')
12 var Launcher = require('./launcher').Launcher
13 var FileList = require('./file_list').List
14 var reporter = require('./reporter')
15 var helper = require('./helper')
16 var events = require('./events')
17 var EventEmitter = events.EventEmitter
18 var Executor = require('./executor')
19 var Browser = require('./browser')
20 var BrowserCollection = require('./browser_collection')
21 var EmitterWrapper = require('./emitter_wrapper')
22 var processWrapper = new EmitterWrapper(process)
24 var log = logger.create()
26 var start = function (injector, config, launcher, globalEmitter, preprocess, fileList, webServer,
27 capturedBrowsers, socketServer, executor, done) {
28 config.frameworks.forEach(function (framework) {
29 injector.get('framework:' + framework)
32 // A map of launched browsers.
33 var singleRunDoneBrowsers = Object.create(null)
35 // Passing fake event emitter, so that it does not emit on the global,
36 // we don't care about these changes.
37 var singleRunBrowsers = new BrowserCollection(new EventEmitter())
39 // Some browsers did not get captured.
40 var singleRunBrowserNotCaptured = false
42 webServer.on('error', function (e) {
43 if (e.code === 'EADDRINUSE') {
44 log.warn('Port %d in use', config.port)
46 webServer.listen(config.port)
52 var afterPreprocess = function () {
53 if (config.autoWatch) {
54 injector.invoke(watcher.watch)
57 webServer.listen(config.port, function () {
58 log.info('Karma v%s server started at http://%s:%s%s', constant.VERSION, config.hostname,
59 config.port, config.urlRoot)
61 if (config.browsers && config.browsers.length) {
62 injector.invoke(launcher.launch, launcher).forEach(function (browserLauncher) {
63 singleRunDoneBrowsers[browserLauncher.id] = false
69 fileList.refresh().then(afterPreprocess, afterPreprocess)
71 globalEmitter.on('browsers_change', function () {
72 // TODO(vojta): send only to interested browsers
73 socketServer.sockets.emit('info', capturedBrowsers.serialize())
76 globalEmitter.on('browser_register', function (browser) {
77 launcher.markCaptured(browser.id)
79 // TODO(vojta): This is lame, browser can get captured and then crash (before other browsers get
81 if (config.autoWatch && launcher.areAllCaptured()) {
86 var EVENTS_TO_REPLY = ['start', 'info', 'error', 'result', 'complete']
87 socketServer.sockets.on('connection', function (socket) {
88 log.debug('A browser has connected on socket ' + socket.id)
90 var replySocketEvents = events.bufferEvents(socket, EVENTS_TO_REPLY)
92 socket.on('complete', function (data, ack) {
96 socket.on('register', function (info) {
101 newBrowser = capturedBrowsers.getById(info.id) || singleRunBrowsers.getById(info.id)
105 isRestart = newBrowser.state === Browser.STATE_DISCONNECTED
106 newBrowser.reconnect(socket)
108 // We are restarting a previously disconnected browser.
109 if (isRestart && config.singleRun) {
110 newBrowser.execute(config.client)
113 newBrowser = injector.createChild([{
114 id: ['value', info.id || null],
115 fullName: ['value', info.name],
116 socket: ['value', socket]
117 }]).instantiate(Browser)
121 // execute in this browser immediately
122 if (config.singleRun) {
123 newBrowser.execute(config.client)
124 singleRunBrowsers.add(newBrowser)
132 var emitRunCompleteIfAllBrowsersDone = function () {
134 var isDone = Object.keys(singleRunDoneBrowsers).reduce(function (isDone, id) {
135 return isDone && singleRunDoneBrowsers[id]
139 var results = singleRunBrowsers.getResults()
140 if (singleRunBrowserNotCaptured) {
144 globalEmitter.emit('run_complete', singleRunBrowsers, results)
148 if (config.singleRun) {
149 globalEmitter.on('browser_complete', function (completedBrowser) {
150 if (completedBrowser.lastResult.disconnected &&
151 completedBrowser.disconnectsCount <= config.browserDisconnectTolerance) {
152 log.info('Restarting %s (%d of %d attempts)', completedBrowser.name,
153 completedBrowser.disconnectsCount, config.browserDisconnectTolerance)
154 if (!launcher.restart(completedBrowser.id)) {
155 singleRunDoneBrowsers[completedBrowser.id] = true
156 emitRunCompleteIfAllBrowsersDone()
159 singleRunDoneBrowsers[completedBrowser.id] = true
161 if (launcher.kill(completedBrowser.id)) {
162 // workaround to supress "disconnect" warning
163 completedBrowser.state = Browser.STATE_DISCONNECTED
166 emitRunCompleteIfAllBrowsersDone()
170 globalEmitter.on('browser_process_failure', function (browserLauncher) {
171 singleRunDoneBrowsers[browserLauncher.id] = true
172 singleRunBrowserNotCaptured = true
174 emitRunCompleteIfAllBrowsersDone()
177 globalEmitter.on('run_complete', function (browsers, results) {
178 log.debug('Run complete, exiting.')
179 disconnectBrowsers(results.exitCode)
182 globalEmitter.emit('run_start', singleRunBrowsers)
185 if (config.autoWatch) {
186 globalEmitter.on('file_list_modified', function () {
187 log.debug('List of files has changed, trying to execute')
192 var webServerCloseTimeout = 3000
193 var disconnectBrowsers = function (code) {
194 // Slightly hacky way of removing disconnect listeners
195 // to suppress "browser disconnect" warnings
196 // TODO(vojta): change the client to not send the event (if disconnected by purpose)
197 var sockets = socketServer.sockets.sockets
198 Object.getOwnPropertyNames(sockets).forEach(function (key) {
199 var socket = sockets[key]
200 socket.removeAllListeners('disconnect')
201 if (!socket.disconnected) {
206 var removeAllListenersDone = false
207 var removeAllListeners = function () {
208 // make sure we don't execute cleanup twice
209 if (removeAllListenersDone) {
212 removeAllListenersDone = true
213 webServer.removeAllListeners()
214 processWrapper.removeAllListeners()
218 globalEmitter.emitAsync('exit').then(function () {
219 // don't wait forever on webServer.close() because
220 // pending client connections prevent it from closing.
221 var closeTimeout = setTimeout(removeAllListeners, webServerCloseTimeout)
223 // shutdown the server...
224 webServer.close(function () {
225 clearTimeout(closeTimeout)
229 // shutdown socket.io flash transport, if defined
230 if (socketServer.flashPolicyServer) {
231 socketServer.flashPolicyServer.close()
237 processWrapper.on('SIGINT', disconnectBrowsers)
238 processWrapper.on('SIGTERM', disconnectBrowsers)
240 // Windows doesn't support signals yet, so they simply don't get this handling.
241 // https://github.com/joyent/node/issues/1553
244 // Handle all unhandled exceptions, so we don't just exit but
245 // disconnect the browsers before exiting.
246 processWrapper.on('uncaughtException', function (error) {
248 disconnectBrowsers(1)
251 start.$inject = ['injector', 'config', 'launcher', 'emitter', 'preprocess', 'fileList',
252 'webServer', 'capturedBrowsers', 'socketServer', 'executor', 'done']
254 var createSocketIoServer = function (webServer, executor, config) {
255 var server = io.listen(webServer, {
256 // avoid destroying http upgrades from socket.io to get proxied websockets working
257 'destroy upgrade': false,
258 // socket.io has a timeout (15s by default) before destroying a store (a data structure where
259 // data associated with a socket are stored). Unfortunately this timeout is not cleared
260 // properly on socket.io shutdown and this timeout prevents karma from exiting cleanly.
261 // We change this timeout to 0 to make Karma exit just after all tests were executed.
262 'client store expiration': 0,
263 logger: logger.create('socket.io', constant.LOG_ERROR),
264 resource: config.urlRoot + 'socket.io',
265 transports: config.transports
268 // hack to overcome circular dependency
269 executor.socketIoSockets = server.sockets
274 exports.start = function (cliOptions, done) {
275 // apply the default logger config (and config from CLI) as soon as we can
276 logger.setup(cliOptions.logLevel || constant.LOG_INFO,
277 helper.isDefined(cliOptions.colors) ? cliOptions.colors : true, [constant.CONSOLE_APPENDER])
279 var config = cfg.parseConfig(cliOptions.configFile, cliOptions)
281 helper: ['value', helper],
282 logger: ['value', logger],
283 done: ['value', done || process.exit],
284 emitter: ['type', EventEmitter],
285 launcher: ['type', Launcher],
286 config: ['value', config],
287 preprocess: ['factory', preprocessor.createPreprocessor],
288 fileList: ['type', FileList],
289 webServer: ['factory', ws.create],
290 socketServer: ['factory', createSocketIoServer],
291 executor: ['type', Executor],
292 // TODO(vojta): remove
293 customFileHandlers: ['value', []],
294 // TODO(vojta): remove, once karma-dart does not rely on it
295 customScriptTypes: ['value', []],
296 reporter: ['factory', reporter.createReporters],
297 capturedBrowsers: ['type', BrowserCollection],
299 timer: ['value', {setTimeout: setTimeout, clearTimeout: clearTimeout}]
303 modules = modules.concat(plugin.resolve(config.plugins))
305 var injector = new di.Injector(modules)
307 injector.invoke(start)