source: trip-planner-front/node_modules/karma/lib/server.js@ ceaed42

Last change on this file since ceaed42 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 15.6 KB
RevLine 
[6a3a178]1'use strict'
2
3const SocketIO = require('socket.io')
4const di = require('di')
5const util = require('util')
6const spawn = require('child_process').spawn
7const tmp = require('tmp')
8const fs = require('fs')
9const path = require('path')
10
11const NetUtils = require('./utils/net-utils')
12const root = global || window || this
13
14const cfg = require('./config')
15const logger = require('./logger')
16const constant = require('./constants')
17const watcher = require('./watcher')
18const plugin = require('./plugin')
19
20const createServeFile = require('./web-server').createServeFile
21const createServeStaticFile = require('./web-server').createServeStaticFile
22const createFilesPromise = require('./web-server').createFilesPromise
23const createWebServer = require('./web-server').createWebServer
24const preprocessor = require('./preprocessor')
25const Launcher = require('./launcher').Launcher
26const FileList = require('./file-list')
27const reporter = require('./reporter')
28const helper = require('./helper')
29const events = require('./events')
30const KarmaEventEmitter = events.EventEmitter
31const EventEmitter = require('events').EventEmitter
32const Executor = require('./executor')
33const Browser = require('./browser')
34const BrowserCollection = require('./browser_collection')
35const EmitterWrapper = require('./emitter_wrapper')
36const processWrapper = new EmitterWrapper(process)
37
38function createSocketIoServer (webServer, executor, config) {
39 const server = new SocketIO.Server(webServer, {
40 // avoid destroying http upgrades from socket.io to get proxied websockets working
41 destroyUpgrade: false,
42 path: config.urlRoot + 'socket.io/',
43 transports: config.transports,
44 forceJSONP: config.forceJSONP,
45 // Default is 5000 in socket.io v2.x and v3.x.
46 pingTimeout: config.pingTimeout || 5000,
47 // Default in v2 is 1e8 and coverage results can fail at 1e6
48 maxHttpBufferSize: 1e8
49 })
50
51 // hack to overcome circular dependency
52 executor.socketIoSockets = server.sockets
53
54 return server
55}
56
57class Server extends KarmaEventEmitter {
58 constructor (cliOptionsOrConfig, done) {
59 super()
60 cliOptionsOrConfig = cliOptionsOrConfig || {}
61 this.log = logger.create('karma-server')
62 done = helper.isFunction(done) ? done : process.exit
63 this.loadErrors = []
64
65 let config
66 if (cliOptionsOrConfig instanceof cfg.Config) {
67 config = cliOptionsOrConfig
68 } else {
69 logger.setupFromConfig({
70 colors: cliOptionsOrConfig.colors,
71 logLevel: cliOptionsOrConfig.logLevel
72 })
73 const deprecatedCliOptionsMessage =
74 'Passing raw CLI options to `new Server(config, done)` is ' +
75 'deprecated. Use ' +
76 '`parseConfig(configFilePath, cliOptions, {promiseConfig: true, throwErrors: true})` ' +
77 'to prepare a processed `Config` instance and pass that as the ' +
78 '`config` argument instead.'
79 this.log.warn(deprecatedCliOptionsMessage)
80 try {
81 config = cfg.parseConfig(
82 cliOptionsOrConfig.configFile,
83 cliOptionsOrConfig,
84 {
85 promiseConfig: false,
86 throwErrors: true
87 }
88 )
89 } catch (parseConfigError) {
90 // TODO: change how `done` falls back to exit in next major version
91 // SEE: https://github.com/karma-runner/karma/pull/3635#discussion_r565399378
92 done(1)
93 return
94 }
95 }
96
97 this.log.debug('Final config', util.inspect(config, false, /** depth **/ null))
98
99 let modules = [{
100 helper: ['value', helper],
101 logger: ['value', logger],
102 done: ['value', done || process.exit],
103 emitter: ['value', this],
104 server: ['value', this],
105 watcher: ['value', watcher],
106 launcher: ['factory', Launcher.factory],
107 config: ['value', config],
108 instantiatePlugin: ['factory', plugin.createInstantiatePlugin],
109 preprocess: ['factory', preprocessor.createPriorityPreprocessor],
110 fileList: ['factory', FileList.factory],
111 webServer: ['factory', createWebServer],
112 serveFile: ['factory', createServeFile],
113 serveStaticFile: ['factory', createServeStaticFile],
114 filesPromise: ['factory', createFilesPromise],
115 socketServer: ['factory', createSocketIoServer],
116 executor: ['factory', Executor.factory],
117 // TODO: Deprecated. Remove in the next major
118 customFileHandlers: ['value', []],
119 reporter: ['factory', reporter.createReporters],
120 capturedBrowsers: ['factory', BrowserCollection.factory],
121 args: ['value', {}],
122 timer: ['value', {
123 setTimeout () {
124 return setTimeout.apply(root, arguments)
125 },
126 clearTimeout
127 }]
128 }]
129
130 this.on('load_error', (type, name) => {
131 this.log.debug(`Registered a load error of type ${type} with name ${name}`)
132 this.loadErrors.push([type, name])
133 })
134
135 modules = modules.concat(plugin.resolve(config.plugins, this))
136 this._injector = new di.Injector(modules)
137 }
138
139 async start () {
140 const config = this.get('config')
141 try {
142 this._boundServer = await NetUtils.bindAvailablePort(config.port, config.listenAddress)
143 this._boundServer.on('connection', (socket) => {
144 // Attach an error handler to avoid UncaughtException errors.
145 socket.on('error', (err) => {
146 // Errors on this socket are retried, ignore them
147 this.log.debug('Ignoring error on webserver connection: ' + err)
148 })
149 })
150 config.port = this._boundServer.address().port
151 await this._injector.invoke(this._start, this)
152 } catch (err) {
153 this.log.error(`Server start failed on port ${config.port}: ${err}`)
154 this._close(1)
155 }
156 }
157
158 get (token) {
159 return this._injector.get(token)
160 }
161
162 refreshFiles () {
163 return this._fileList ? this._fileList.refresh() : Promise.resolve()
164 }
165
166 refreshFile (path) {
167 return this._fileList ? this._fileList.changeFile(path) : Promise.resolve()
168 }
169
170 emitExitAsync (code) {
171 const name = 'exit'
172 let pending = this.listeners(name).length
173 const deferred = helper.defer()
174
175 function resolve () {
176 deferred.resolve(code)
177 }
178
179 try {
180 this.emit(name, (newCode) => {
181 if (newCode && typeof newCode === 'number') {
182 // Only update code if it is given and not zero
183 code = newCode
184 }
185 if (!--pending) {
186 resolve()
187 }
188 })
189
190 if (!pending) {
191 resolve()
192 }
193 } catch (err) {
194 deferred.reject(err)
195 }
196 return deferred.promise
197 }
198
199 async _start (config, launcher, preprocess, fileList, capturedBrowsers, executor, done) {
200 if (config.detached) {
201 this._detach(config, done)
202 return
203 }
204
205 this._fileList = fileList
206
207 await Promise.all(
208 config.frameworks.map((framework) => this._injector.get('framework:' + framework))
209 )
210
211 const webServer = this._injector.get('webServer')
212 const socketServer = this._injector.get('socketServer')
213
214 const singleRunDoneBrowsers = Object.create(null)
215 const singleRunBrowsers = new BrowserCollection(new EventEmitter())
216 let singleRunBrowserNotCaptured = false
217
218 webServer.on('error', (err) => {
219 this.log.error(`Webserver fail ${err}`)
220 this._close(1)
221 })
222
223 const afterPreprocess = () => {
224 if (config.autoWatch) {
225 const watcher = this.get('watcher')
226 this._injector.invoke(watcher)
227 }
228
229 webServer.listen(this._boundServer, () => {
230 this.log.info(`Karma v${constant.VERSION} server started at ${config.protocol}//${config.hostname}:${config.port}${config.urlRoot}`)
231
232 this.emit('listening', config.port)
233 if (config.browsers && config.browsers.length) {
234 this._injector.invoke(launcher.launch, launcher).forEach((browserLauncher) => {
235 singleRunDoneBrowsers[browserLauncher.id] = false
236 })
237 }
238 if (this.loadErrors.length > 0) {
239 this.log.error(new Error(`Found ${this.loadErrors.length} load error${this.loadErrors.length === 1 ? '' : 's'}`))
240 this._close(1)
241 }
242 })
243 }
244
245 fileList.refresh().then(afterPreprocess, (err) => {
246 this.log.error('Error during file loading or preprocessing\n' + err.stack || err)
247 afterPreprocess()
248 })
249
250 this.on('browsers_change', () => socketServer.sockets.emit('info', capturedBrowsers.serialize()))
251
252 this.on('browser_register', (browser) => {
253 launcher.markCaptured(browser.id)
254
255 if (launcher.areAllCaptured()) {
256 this.emit('browsers_ready')
257
258 if (config.autoWatch) {
259 executor.schedule()
260 }
261 }
262 })
263
264 if (config.browserConsoleLogOptions && config.browserConsoleLogOptions.path) {
265 const configLevel = config.browserConsoleLogOptions.level || 'debug'
266 const configFormat = config.browserConsoleLogOptions.format || '%b %T: %m'
267 const configPath = config.browserConsoleLogOptions.path
268 this.log.info(`Writing browser console to file: ${configPath}`)
269 const browserLogFile = fs.openSync(configPath, 'w+')
270 const levels = ['log', 'error', 'warn', 'info', 'debug']
271 this.on('browser_log', function (browser, message, level) {
272 if (levels.indexOf(level.toLowerCase()) > levels.indexOf(configLevel)) {
273 return
274 }
275 if (!helper.isString(message)) {
276 message = util.inspect(message, { showHidden: false, colors: false })
277 }
278 const logMap = { '%m': message, '%t': level.toLowerCase(), '%T': level.toUpperCase(), '%b': browser }
279 const logString = configFormat.replace(/%[mtTb]/g, (m) => logMap[m])
280 this.log.debug(`Writing browser console line: ${logString}`)
281 fs.writeSync(browserLogFile, logString + '\n')
282 })
283 }
284
285 socketServer.sockets.on('connection', (socket) => {
286 this.log.debug(`A browser has connected on socket ${socket.id}`)
287
288 const replySocketEvents = events.bufferEvents(socket, ['start', 'info', 'karma_error', 'result', 'complete'])
289
290 socket.on('error', (err) => {
291 this.log.debug('karma server socket error: ' + err)
292 })
293
294 socket.on('register', (info) => {
295 const knownBrowser = info.id ? (capturedBrowsers.getById(info.id) || singleRunBrowsers.getById(info.id)) : null
296
297 if (knownBrowser) {
298 knownBrowser.reconnect(socket, info.isSocketReconnect)
299 } else {
300 const newBrowser = this._injector.createChild([{
301 id: ['value', info.id || null],
302 fullName: ['value', (helper.isDefined(info.displayName) ? info.displayName : info.name)],
303 socket: ['value', socket]
304 }]).invoke(Browser.factory)
305
306 newBrowser.init()
307
308 if (config.singleRun) {
309 newBrowser.execute()
310 singleRunBrowsers.add(newBrowser)
311 }
312 }
313
314 replySocketEvents()
315 })
316 })
317
318 const emitRunCompleteIfAllBrowsersDone = () => {
319 if (Object.keys(singleRunDoneBrowsers).every((key) => singleRunDoneBrowsers[key])) {
320 this.emit('run_complete', singleRunBrowsers, singleRunBrowsers.getResults(singleRunBrowserNotCaptured, config))
321 }
322 }
323
324 this.on('browser_complete', (completedBrowser) => {
325 if (completedBrowser.lastResult.disconnected && completedBrowser.disconnectsCount <= config.browserDisconnectTolerance) {
326 this.log.info(`Restarting ${completedBrowser.name} (${completedBrowser.disconnectsCount} of ${config.browserDisconnectTolerance} attempts)`)
327
328 if (!launcher.restart(completedBrowser.id)) {
329 this.emit('browser_restart_failure', completedBrowser)
330 }
331 } else {
332 this.emit('browser_complete_with_no_more_retries', completedBrowser)
333 }
334 })
335
336 this.on('stop', (done) => {
337 this.log.debug('Received stop event, exiting.')
338 this._close()
339 done()
340 })
341
342 if (config.singleRun) {
343 this.on('browser_restart_failure', (completedBrowser) => {
344 singleRunDoneBrowsers[completedBrowser.id] = true
345 emitRunCompleteIfAllBrowsersDone()
346 })
347
348 // This is the normal exit trigger.
349 this.on('browser_complete_with_no_more_retries', function (completedBrowser) {
350 singleRunDoneBrowsers[completedBrowser.id] = true
351
352 if (launcher.kill(completedBrowser.id)) {
353 completedBrowser.remove()
354 }
355
356 emitRunCompleteIfAllBrowsersDone()
357 })
358
359 this.on('browser_process_failure', (browserLauncher) => {
360 singleRunDoneBrowsers[browserLauncher.id] = true
361 singleRunBrowserNotCaptured = true
362
363 emitRunCompleteIfAllBrowsersDone()
364 })
365
366 this.on('run_complete', (browsers, results) => {
367 this.log.debug('Run complete, exiting.')
368 this._close(results.exitCode)
369 })
370
371 this.emit('run_start', singleRunBrowsers)
372 }
373
374 if (config.autoWatch) {
375 this.on('file_list_modified', () => {
376 this.log.debug('List of files has changed, trying to execute')
377 if (config.restartOnFileChange) {
378 socketServer.sockets.emit('stop')
379 }
380 executor.schedule()
381 })
382 }
383
384 processWrapper.on('SIGINT', () => this._close())
385 processWrapper.on('SIGTERM', () => this._close())
386
387 const reportError = (error) => {
388 this.log.error(error)
389 process.emit('infrastructure_error', error)
390 this._close(1)
391 }
392
393 processWrapper.on('unhandledRejection', (error) => {
394 this.log.error(`UnhandledRejection: ${error.stack || error.message || String(error)}`)
395 reportError(error)
396 })
397
398 processWrapper.on('uncaughtException', (error) => {
399 this.log.error(`UncaughtException: ${error.stack || error.message || String(error)}`)
400 reportError(error)
401 })
402 }
403
404 _detach (config, done) {
405 const tmpFile = tmp.fileSync({ keep: true })
406 this.log.info('Starting karma detached')
407 this.log.info('Run "karma stop" to stop the server.')
408 this.log.debug(`Writing config to tmp-file ${tmpFile.name}`)
409 config.detached = false
410 try {
411 fs.writeFileSync(tmpFile.name, JSON.stringify(config), 'utf8')
412 } catch (e) {
413 this.log.error("Couldn't write temporary configuration file")
414 done(1)
415 return
416 }
417 const child = spawn(process.argv[0], [path.resolve(__dirname, '../lib/detached.js'), tmpFile.name], {
418 detached: true,
419 stdio: 'ignore'
420 })
421 child.unref()
422 }
423
424 /**
425 * Cleanup all resources allocated by Karma and call the `done` callback
426 * with the result of the tests execution.
427 *
428 * @param [exitCode] - Optional exit code. If omitted will be computed by
429 * 'exit' event listeners.
430 */
431 _close (exitCode) {
432 const webServer = this._injector.get('webServer')
433 const socketServer = this._injector.get('socketServer')
434 const done = this._injector.get('done')
435
436 const webServerCloseTimeout = 3000
437 const sockets = socketServer.sockets.sockets
438
439 Object.keys(sockets).forEach((id) => {
440 const socket = sockets[id]
441 socket.removeAllListeners('disconnect')
442 if (!socket.disconnected) {
443 process.nextTick(socket.disconnect.bind(socket))
444 }
445 })
446
447 this.emitExitAsync(exitCode).catch((err) => {
448 this.log.error('Error while calling exit event listeners\n' + err.stack || err)
449 return 1
450 }).then((code) => {
451 socketServer.sockets.removeAllListeners()
452 socketServer.close()
453
454 let removeAllListenersDone = false
455 const removeAllListeners = () => {
456 if (removeAllListenersDone) {
457 return
458 }
459 removeAllListenersDone = true
460 webServer.removeAllListeners()
461 processWrapper.removeAllListeners()
462 done(code || 0)
463 }
464
465 const closeTimeout = setTimeout(removeAllListeners, webServerCloseTimeout)
466
467 webServer.close(() => {
468 clearTimeout(closeTimeout)
469 removeAllListeners()
470 })
471 })
472 }
473
474 stop () {
475 return this.emitAsync('stop')
476 }
477}
478
479Server.prototype._start.$inject = ['config', 'launcher', 'preprocess', 'fileList', 'capturedBrowsers', 'executor', 'done']
480
481module.exports = Server
Note: See TracBrowser for help on using the repository browser.