[6a3a178] | 1 | 'use strict'
|
---|
| 2 |
|
---|
| 3 | const SocketIO = require('socket.io')
|
---|
| 4 | const di = require('di')
|
---|
| 5 | const util = require('util')
|
---|
| 6 | const spawn = require('child_process').spawn
|
---|
| 7 | const tmp = require('tmp')
|
---|
| 8 | const fs = require('fs')
|
---|
| 9 | const path = require('path')
|
---|
| 10 |
|
---|
| 11 | const NetUtils = require('./utils/net-utils')
|
---|
| 12 | const root = global || window || this
|
---|
| 13 |
|
---|
| 14 | const cfg = require('./config')
|
---|
| 15 | const logger = require('./logger')
|
---|
| 16 | const constant = require('./constants')
|
---|
| 17 | const watcher = require('./watcher')
|
---|
| 18 | const plugin = require('./plugin')
|
---|
| 19 |
|
---|
| 20 | const createServeFile = require('./web-server').createServeFile
|
---|
| 21 | const createServeStaticFile = require('./web-server').createServeStaticFile
|
---|
| 22 | const createFilesPromise = require('./web-server').createFilesPromise
|
---|
| 23 | const createWebServer = require('./web-server').createWebServer
|
---|
| 24 | const preprocessor = require('./preprocessor')
|
---|
| 25 | const Launcher = require('./launcher').Launcher
|
---|
| 26 | const FileList = require('./file-list')
|
---|
| 27 | const reporter = require('./reporter')
|
---|
| 28 | const helper = require('./helper')
|
---|
| 29 | const events = require('./events')
|
---|
| 30 | const KarmaEventEmitter = events.EventEmitter
|
---|
| 31 | const EventEmitter = require('events').EventEmitter
|
---|
| 32 | const Executor = require('./executor')
|
---|
| 33 | const Browser = require('./browser')
|
---|
| 34 | const BrowserCollection = require('./browser_collection')
|
---|
| 35 | const EmitterWrapper = require('./emitter_wrapper')
|
---|
| 36 | const processWrapper = new EmitterWrapper(process)
|
---|
| 37 |
|
---|
| 38 | function 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 |
|
---|
| 57 | class 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 |
|
---|
| 479 | Server.prototype._start.$inject = ['config', 'launcher', 'preprocess', 'fileList', 'capturedBrowsers', 'executor', 'done']
|
---|
| 480 |
|
---|
| 481 | module.exports = Server
|
---|