1 | var SocketIO = require('socket.io')
|
2 | var di = require('di')
|
3 | var util = require('util')
|
4 | var Promise = require('bluebird')
|
5 | var spawn = require('child_process').spawn
|
6 | var tmp = require('tmp')
|
7 | var fs = require('fs')
|
8 | var path = require('path')
|
9 | var root = global || window || this
|
10 |
|
11 | var cfg = require('./config')
|
12 | var logger = require('./logger')
|
13 | var constant = require('./constants')
|
14 | var watcher = require('./watcher')
|
15 | var plugin = require('./plugin')
|
16 |
|
17 | var ws = require('./web-server')
|
18 | var preprocessor = require('./preprocessor')
|
19 | var Launcher = require('./launcher').Launcher
|
20 | var FileList = require('./file-list')
|
21 | var reporter = require('./reporter')
|
22 | var helper = require('./helper')
|
23 | var events = require('./events')
|
24 | var EventEmitter = events.EventEmitter
|
25 | var Executor = require('./executor')
|
26 | var Browser = require('./browser')
|
27 | var BrowserCollection = require('./browser_collection')
|
28 | var EmitterWrapper = require('./emitter_wrapper')
|
29 | var processWrapper = new EmitterWrapper(process)
|
30 | var browserify = require('browserify')
|
31 |
|
32 | const karmaJsPath = path.join(__dirname, '/../static/karma.js')
|
33 | const contextJsPath = path.join(__dirname, '/../static/context.js')
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | function bundleResource (inPath, outPath) {
|
42 | return new Promise((resolve, reject) => {
|
43 | var bundler = browserify(inPath)
|
44 | bundler.bundle().pipe(fs.createWriteStream(outPath))
|
45 | .once('finish', () => {
|
46 | resolve()
|
47 | })
|
48 | .once('error', (e) => {
|
49 | reject(e)
|
50 | })
|
51 | })
|
52 | }
|
53 |
|
54 | function createSocketIoServer (webServer, executor, config) {
|
55 | var server = new SocketIO(webServer, {
|
56 |
|
57 | destroyUpgrade: false,
|
58 | path: config.urlRoot + 'socket.io/',
|
59 | transports: config.transports,
|
60 | forceJSONP: config.forceJSONP
|
61 | })
|
62 |
|
63 |
|
64 | executor.socketIoSockets = server.sockets
|
65 |
|
66 | return server
|
67 | }
|
68 |
|
69 |
|
70 | var Server = function (cliOptions, done) {
|
71 | EventEmitter.call(this)
|
72 |
|
73 | logger.setupFromConfig(cliOptions)
|
74 |
|
75 | this.log = logger.create()
|
76 |
|
77 | this.loadErrors = []
|
78 |
|
79 | var config = cfg.parseConfig(cliOptions.configFile, cliOptions)
|
80 |
|
81 | var modules = [{
|
82 | helper: ['value', helper],
|
83 | logger: ['value', logger],
|
84 | done: ['value', done || process.exit],
|
85 | emitter: ['value', this],
|
86 | server: ['value', this],
|
87 | launcher: ['type', Launcher],
|
88 | config: ['value', config],
|
89 | preprocess: ['factory', preprocessor.createPreprocessor],
|
90 | fileList: ['type', FileList],
|
91 | webServer: ['factory', ws.create],
|
92 | socketServer: ['factory', createSocketIoServer],
|
93 | executor: ['type', Executor],
|
94 |
|
95 | customFileHandlers: ['value', []],
|
96 |
|
97 | customScriptTypes: ['value', []],
|
98 | reporter: ['factory', reporter.createReporters],
|
99 | capturedBrowsers: ['type', BrowserCollection],
|
100 | args: ['value', {}],
|
101 | timer: ['value', {
|
102 | setTimeout: function () {
|
103 | return setTimeout.apply(root, arguments)
|
104 | },
|
105 | clearTimeout: function (timeoutId) {
|
106 | clearTimeout(timeoutId)
|
107 | }
|
108 | }]
|
109 | }]
|
110 |
|
111 | this._setUpLoadErrorListener()
|
112 |
|
113 | modules = modules.concat(plugin.resolve(config.plugins, this))
|
114 |
|
115 | this._injector = new di.Injector(modules)
|
116 | }
|
117 |
|
118 |
|
119 | util.inherits(Server, EventEmitter)
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 | Server.prototype.start = function () {
|
126 | this._injector.invoke(this._start, this)
|
127 | }
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 | Server.start = function (cliOptions, done) {
|
134 | var server = new Server(cliOptions, done)
|
135 | server.start()
|
136 | }
|
137 |
|
138 |
|
139 |
|
140 |
|
141 | Server.prototype.get = function (token) {
|
142 | return this._injector.get(token)
|
143 | }
|
144 |
|
145 |
|
146 | Server.prototype.refreshFiles = function () {
|
147 | if (!this._fileList) return Promise.resolve()
|
148 |
|
149 | return this._fileList.refresh()
|
150 | }
|
151 |
|
152 |
|
153 |
|
154 |
|
155 | Server.prototype._start = function (config, launcher, preprocess, fileList,
|
156 | capturedBrowsers, executor, done) {
|
157 | var self = this
|
158 | if (config.detached) {
|
159 | this._detach(config, done)
|
160 | return
|
161 | }
|
162 |
|
163 | self._fileList = fileList
|
164 |
|
165 | config.frameworks.forEach(function (framework) {
|
166 | self._injector.get('framework:' + framework)
|
167 | })
|
168 |
|
169 | var webServer = self._injector.get('webServer')
|
170 | var socketServer = self._injector.get('socketServer')
|
171 |
|
172 |
|
173 | var singleRunDoneBrowsers = Object.create(null)
|
174 |
|
175 |
|
176 |
|
177 | var singleRunBrowsers = new BrowserCollection(new EventEmitter())
|
178 |
|
179 |
|
180 | var singleRunBrowserNotCaptured = false
|
181 |
|
182 | webServer.on('error', function (e) {
|
183 | if (e.code === 'EADDRINUSE') {
|
184 | self.log.warn('Port %d in use', config.port)
|
185 | config.port++
|
186 | webServer.listen(config.port, config.listenAddress)
|
187 | } else {
|
188 | throw e
|
189 | }
|
190 | })
|
191 |
|
192 | var afterPreprocess = function () {
|
193 | if (config.autoWatch) {
|
194 | self._injector.invoke(watcher.watch)
|
195 | }
|
196 |
|
197 | var startWebServer = function () {
|
198 | webServer.listen(config.port, config.listenAddress, function () {
|
199 | self.log.info('Karma v%s server started at %s//%s:%s%s', constant.VERSION,
|
200 | config.protocol, config.listenAddress, config.port, config.urlRoot)
|
201 |
|
202 | self.emit('listening', config.port)
|
203 | if (config.browsers && config.browsers.length) {
|
204 | self._injector.invoke(launcher.launch, launcher).forEach(function (browserLauncher) {
|
205 | singleRunDoneBrowsers[browserLauncher.id] = false
|
206 | })
|
207 | }
|
208 | var noLoadErrors = self.loadErrors.length
|
209 | if (noLoadErrors > 0) {
|
210 | self.log.error('Found %d load error%s', noLoadErrors, noLoadErrors === 1 ? '' : 's')
|
211 | process.exitCode = 1
|
212 | process.kill(process.pid, 'SIGINT')
|
213 | }
|
214 | })
|
215 | }
|
216 |
|
217 |
|
218 | if (!(fs.existsSync(karmaJsPath) && fs.existsSync(contextJsPath))) {
|
219 | self.log.info('Front-end scripts not present. Compiling...')
|
220 | var mainPromise = bundleResource(path.join(__dirname, '/../client/main.js'), karmaJsPath)
|
221 | var contextPromise = bundleResource(path.join(__dirname, '/../context/main.js'), contextJsPath)
|
222 | Promise.all([mainPromise, contextPromise]).then(() => {
|
223 | startWebServer()
|
224 | }).catch((error) => {
|
225 | self.log.error('Front-end script compile failed with error: ' + error)
|
226 | process.exitCode = 1
|
227 | process.kill(process.pid, 'SIGINT')
|
228 | })
|
229 | } else {
|
230 | startWebServer()
|
231 | }
|
232 | }
|
233 |
|
234 | fileList.refresh().then(afterPreprocess, afterPreprocess)
|
235 |
|
236 | self.on('browsers_change', function () {
|
237 |
|
238 | socketServer.sockets.emit('info', capturedBrowsers.serialize())
|
239 | })
|
240 |
|
241 | self.on('browser_register', function (browser) {
|
242 | launcher.markCaptured(browser.id)
|
243 |
|
244 |
|
245 |
|
246 | if (launcher.areAllCaptured()) {
|
247 | self.emit('browsers_ready')
|
248 |
|
249 | if (config.autoWatch) {
|
250 | executor.schedule()
|
251 | }
|
252 | }
|
253 | })
|
254 |
|
255 | if (config.browserConsoleLogOptions && config.browserConsoleLogOptions.path) {
|
256 | var configLevel = config.browserConsoleLogOptions.level || 'debug'
|
257 | var configFormat = config.browserConsoleLogOptions.format || '%b %T: %m'
|
258 | var configPath = config.browserConsoleLogOptions.path
|
259 | self.log.info('Writing browser console to file: %s', configPath)
|
260 | var browserLogFile = fs.openSync(configPath, 'w+')
|
261 | var levels = ['log', 'error', 'warn', 'info', 'debug']
|
262 | self.on('browser_log', function (browser, message, level) {
|
263 | if (levels.indexOf(level.toLowerCase()) > levels.indexOf(configLevel)) return
|
264 | if (!helper.isString(message)) {
|
265 | message = util.inspect(message, {showHidden: false, colors: false})
|
266 | }
|
267 | var logMap = {'%m': message, '%t': level.toLowerCase(), '%T': level.toUpperCase(), '%b': browser}
|
268 | var logString = configFormat.replace(/%[mtTb]/g, function (m) {
|
269 | return logMap[m]
|
270 | })
|
271 | self.log.debug('Writing browser console line: %s', logString)
|
272 | fs.write(browserLogFile, logString + '\n')
|
273 | })
|
274 | }
|
275 |
|
276 | var EVENTS_TO_REPLY = ['start', 'info', 'karma_error', 'result', 'complete']
|
277 | socketServer.sockets.on('connection', function (socket) {
|
278 | self.log.debug('A browser has connected on socket ' + socket.id)
|
279 |
|
280 | var replySocketEvents = events.bufferEvents(socket, EVENTS_TO_REPLY)
|
281 |
|
282 | socket.on('complete', function (data, ack) {
|
283 | ack()
|
284 | })
|
285 |
|
286 | socket.on('register', function (info) {
|
287 | var newBrowser
|
288 | var isRestart
|
289 |
|
290 | if (info.id) {
|
291 | newBrowser = capturedBrowsers.getById(info.id) || singleRunBrowsers.getById(info.id)
|
292 | }
|
293 |
|
294 | if (newBrowser) {
|
295 | isRestart = newBrowser.state === Browser.STATE_DISCONNECTED
|
296 | newBrowser.reconnect(socket)
|
297 |
|
298 |
|
299 | if (isRestart && config.singleRun) {
|
300 | newBrowser.execute(config.client)
|
301 | }
|
302 | } else {
|
303 | newBrowser = self._injector.createChild([{
|
304 | id: ['value', info.id || null],
|
305 | fullName: ['value', (helper.isDefined(info.displayName) ? info.displayName : info.name)],
|
306 | socket: ['value', socket]
|
307 | }]).instantiate(Browser)
|
308 |
|
309 | newBrowser.init()
|
310 |
|
311 |
|
312 | if (config.singleRun) {
|
313 | newBrowser.execute(config.client)
|
314 | singleRunBrowsers.add(newBrowser)
|
315 | }
|
316 | }
|
317 |
|
318 | replySocketEvents()
|
319 | })
|
320 | })
|
321 |
|
322 | var emitRunCompleteIfAllBrowsersDone = function () {
|
323 |
|
324 | var isDone = Object.keys(singleRunDoneBrowsers).reduce(function (isDone, id) {
|
325 | return isDone && singleRunDoneBrowsers[id]
|
326 | }, true)
|
327 |
|
328 | if (isDone) {
|
329 | var results = singleRunBrowsers.getResults()
|
330 | if (singleRunBrowserNotCaptured) {
|
331 | results.exitCode = 1
|
332 | } else if (results.success + results.failed === 0 && !config.failOnEmptyTestSuite) {
|
333 | results.exitCode = 0
|
334 | self.log.warn('Test suite was empty.')
|
335 | }
|
336 | self.emit('run_complete', singleRunBrowsers, results)
|
337 | }
|
338 | }
|
339 |
|
340 | self.on('browser_complete', function (completedBrowser) {
|
341 | if (completedBrowser.lastResult.disconnected &&
|
342 | completedBrowser.disconnectsCount <= config.browserDisconnectTolerance) {
|
343 | self.log.info('Restarting %s (%d of %d attempts)', completedBrowser.name,
|
344 | completedBrowser.disconnectsCount, config.browserDisconnectTolerance)
|
345 |
|
346 | if (!launcher.restart(completedBrowser.id)) {
|
347 | self.emit('browser_restart_failure', completedBrowser)
|
348 | }
|
349 | } else {
|
350 | self.emit('browser_complete_with_no_more_retries', completedBrowser)
|
351 | }
|
352 | })
|
353 |
|
354 | if (config.singleRun) {
|
355 | self.on('browser_restart_failure', function (completedBrowser) {
|
356 | singleRunDoneBrowsers[completedBrowser.id] = true
|
357 | emitRunCompleteIfAllBrowsersDone()
|
358 | })
|
359 | self.on('browser_complete_with_no_more_retries', function (completedBrowser) {
|
360 | singleRunDoneBrowsers[completedBrowser.id] = true
|
361 |
|
362 | if (launcher.kill(completedBrowser.id)) {
|
363 |
|
364 | completedBrowser.state = Browser.STATE_DISCONNECTED
|
365 | }
|
366 |
|
367 | emitRunCompleteIfAllBrowsersDone()
|
368 | })
|
369 |
|
370 | self.on('browser_process_failure', function (browserLauncher) {
|
371 | singleRunDoneBrowsers[browserLauncher.id] = true
|
372 | singleRunBrowserNotCaptured = true
|
373 |
|
374 | emitRunCompleteIfAllBrowsersDone()
|
375 | })
|
376 |
|
377 | self.on('run_complete', function (browsers, results) {
|
378 | self.log.debug('Run complete, exiting.')
|
379 | disconnectBrowsers(results.exitCode)
|
380 | })
|
381 |
|
382 | self.emit('run_start', singleRunBrowsers)
|
383 | }
|
384 |
|
385 | if (config.autoWatch) {
|
386 | self.on('file_list_modified', function () {
|
387 | self.log.debug('List of files has changed, trying to execute')
|
388 | if (config.restartOnFileChange) {
|
389 | socketServer.sockets.emit('stop')
|
390 | }
|
391 | executor.schedule()
|
392 | })
|
393 | }
|
394 |
|
395 | var webServerCloseTimeout = 3000
|
396 | var disconnectBrowsers = function (code) {
|
397 |
|
398 |
|
399 |
|
400 | var sockets = socketServer.sockets.sockets
|
401 |
|
402 | Object.keys(sockets).forEach(function (id) {
|
403 | var socket = sockets[id]
|
404 | socket.removeAllListeners('disconnect')
|
405 | if (!socket.disconnected) {
|
406 |
|
407 |
|
408 | process.nextTick(socket.disconnect.bind(socket))
|
409 | }
|
410 | })
|
411 |
|
412 | var removeAllListenersDone = false
|
413 | var removeAllListeners = function () {
|
414 |
|
415 | if (removeAllListenersDone) {
|
416 | return
|
417 | }
|
418 | removeAllListenersDone = true
|
419 | webServer.removeAllListeners()
|
420 | processWrapper.removeAllListeners()
|
421 | done(code || 0)
|
422 | }
|
423 |
|
424 | self.emitAsync('exit').then(function () {
|
425 |
|
426 |
|
427 | var closeTimeout = setTimeout(removeAllListeners, webServerCloseTimeout)
|
428 |
|
429 |
|
430 | webServer.close(function () {
|
431 | clearTimeout(closeTimeout)
|
432 | removeAllListeners()
|
433 | })
|
434 | })
|
435 | }
|
436 |
|
437 | processWrapper.on('SIGINT', function () {
|
438 | disconnectBrowsers(process.exitCode)
|
439 | })
|
440 | processWrapper.on('SIGTERM', disconnectBrowsers)
|
441 |
|
442 |
|
443 |
|
444 | processWrapper.on('uncaughtException', function (error) {
|
445 | self.log.error(error)
|
446 | disconnectBrowsers(1)
|
447 | })
|
448 | }
|
449 |
|
450 | Server.prototype._setUpLoadErrorListener = function () {
|
451 | var self = this
|
452 | self.on('load_error', function (type, name) {
|
453 | self.log.debug('Registered a load error of type %s with name %s', type, name)
|
454 | self.loadErrors.push([type, name])
|
455 | })
|
456 | }
|
457 |
|
458 | Server.prototype._detach = function (config, done) {
|
459 | var log = this.log
|
460 | var tmpFile = tmp.fileSync({keep: true})
|
461 | log.info('Starting karma detached')
|
462 | log.info('Run "karma stop" to stop the server.')
|
463 | log.debug('Writing config to tmp-file %s', tmpFile.name)
|
464 | config.detached = false
|
465 | try {
|
466 | fs.writeFileSync(tmpFile.name, JSON.stringify(config), 'utf8')
|
467 | } catch (e) {
|
468 | log.error("Couldn't write temporary configuration file")
|
469 | done(1)
|
470 | return
|
471 | }
|
472 | var child = spawn(process.argv[0], [path.resolve(__dirname, '../lib/detached.js'), tmpFile.name], {
|
473 | detached: true,
|
474 | stdio: 'ignore'
|
475 | })
|
476 | child.unref()
|
477 | }
|
478 |
|
479 |
|
480 |
|
481 |
|
482 | module.exports = Server
|