123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397 |
- 'use strict'
- const SocketIO = require('socket.io')
- const di = require('di')
- const util = require('util')
- const Promise = require('bluebird')
- const spawn = require('child_process').spawn
- const tmp = require('tmp')
- const fs = require('fs')
- const path = require('path')
- const root = global || window || this
- const cfg = require('./config')
- const logger = require('./logger')
- const constant = require('./constants')
- const watcher = require('./watcher')
- const plugin = require('./plugin')
- const ws = require('./web-server')
- const preprocessor = require('./preprocessor')
- const Launcher = require('./launcher').Launcher
- const FileList = require('./file-list')
- const reporter = require('./reporter')
- const helper = require('./helper')
- const events = require('./events')
- const KarmaEventEmitter = events.EventEmitter
- const EventEmitter = require('events').EventEmitter
- const Executor = require('./executor')
- const Browser = require('./browser')
- const BrowserCollection = require('./browser_collection')
- const EmitterWrapper = require('./emitter_wrapper')
- const processWrapper = new EmitterWrapper(process)
- const karmaJsPath = path.join(__dirname, '/../static/karma.js')
- const contextJsPath = path.join(__dirname, '/../static/context.js')
- function bundleResource (inPath, outPath) {
- const browserify = require('browserify')
- return new Promise((resolve, reject) => {
- browserify(inPath)
- .bundle()
- .pipe(fs.createWriteStream(outPath))
- .once('finish', () => resolve())
- .once('error', (e) => reject(e))
- })
- }
- function createSocketIoServer (webServer, executor, config) {
- const server = new SocketIO(webServer, {
- // avoid destroying http upgrades from socket.io to get proxied websockets working
- destroyUpgrade: false,
- path: config.urlRoot + 'socket.io/',
- transports: config.transports,
- forceJSONP: config.forceJSONP
- })
- // hack to overcome circular dependency
- executor.socketIoSockets = server.sockets
- return server
- }
- class Server extends KarmaEventEmitter {
- constructor (cliOptions, done) {
- super()
- logger.setupFromConfig(cliOptions)
- this.log = logger.create()
- this.loadErrors = []
- const config = cfg.parseConfig(cliOptions.configFile, cliOptions)
- let modules = [{
- helper: ['value', helper],
- logger: ['value', logger],
- done: ['value', done || process.exit],
- emitter: ['value', this],
- server: ['value', this],
- launcher: ['type', Launcher],
- config: ['value', config],
- preprocess: ['factory', preprocessor.createPreprocessor],
- fileList: ['factory', FileList.factory],
- webServer: ['factory', ws.create],
- socketServer: ['factory', createSocketIoServer],
- executor: ['factory', Executor.factory],
- // TODO(vojta): remove
- customFileHandlers: ['value', []],
- // TODO(vojta): remove, once karma-dart does not rely on it
- customScriptTypes: ['value', []],
- reporter: ['factory', reporter.createReporters],
- capturedBrowsers: ['factory', BrowserCollection.factory],
- args: ['value', {}],
- timer: ['value', {
- setTimeout () {
- return setTimeout.apply(root, arguments)
- },
- clearTimeout
- }]
- }]
- this.on('load_error', (type, name) => {
- this.log.debug(`Registered a load error of type ${type} with name ${name}`)
- this.loadErrors.push([type, name])
- })
- modules = modules.concat(plugin.resolve(config.plugins, this))
- this._injector = new di.Injector(modules)
- }
- start () {
- this._injector.invoke(this._start, this)
- }
- get (token) {
- return this._injector.get(token)
- }
- refreshFiles () {
- return this._fileList ? this._fileList.refresh() : Promise.resolve()
- }
- _start (config, launcher, preprocess, fileList, capturedBrowsers, executor, done) {
- if (config.detached) {
- this._detach(config, done)
- return
- }
- this._fileList = fileList
- config.frameworks.forEach((framework) => this._injector.get('framework:' + framework))
- const webServer = this._injector.get('webServer')
- const socketServer = this._injector.get('socketServer')
- const singleRunDoneBrowsers = Object.create(null)
- const singleRunBrowsers = new BrowserCollection(new EventEmitter())
- let singleRunBrowserNotCaptured = false
- webServer.on('error', (e) => {
- if (e.code === 'EADDRINUSE') {
- this.log.warn('Port %d in use', config.port)
- config.port++
- webServer.listen(config.port, config.listenAddress)
- } else {
- throw e
- }
- })
- const afterPreprocess = () => {
- if (config.autoWatch) {
- this._injector.invoke(watcher.watch)
- }
- const startWebServer = () => {
- webServer.listen(config.port, config.listenAddress, () => {
- this.log.info(`Karma v${constant.VERSION} server started at ${config.protocol}//${config.listenAddress}:${config.port}${config.urlRoot}`)
- this.emit('listening', config.port)
- if (config.browsers && config.browsers.length) {
- this._injector.invoke(launcher.launch, launcher).forEach((browserLauncher) => {
- singleRunDoneBrowsers[browserLauncher.id] = false
- })
- }
- if (this.loadErrors.length > 0) {
- this.log.error('Found %d load error%s', this.loadErrors.length, this.loadErrors.length === 1 ? '' : 's')
- process.exitCode = 1
- process.kill(process.pid, 'SIGINT')
- }
- })
- }
- if (fs.existsSync(karmaJsPath) && fs.existsSync(contextJsPath)) {
- startWebServer()
- } else {
- this.log.info('Front-end scripts not present. Compiling...')
- Promise.all([
- bundleResource(path.join(__dirname, '/../client/main.js'), karmaJsPath),
- bundleResource(path.join(__dirname, '/../context/main.js'), contextJsPath)
- ])
- .then(startWebServer)
- .catch((error) => {
- this.log.error('Front-end script compile failed with error: ' + error)
- process.exitCode = 1
- process.kill(process.pid, 'SIGINT')
- })
- }
- }
- fileList.refresh().then(afterPreprocess, afterPreprocess)
- this.on('browsers_change', () => socketServer.sockets.emit('info', capturedBrowsers.serialize()))
- this.on('browser_register', (browser) => {
- launcher.markCaptured(browser.id)
- if (launcher.areAllCaptured()) {
- this.emit('browsers_ready')
- if (config.autoWatch) {
- executor.schedule()
- }
- }
- })
- if (config.browserConsoleLogOptions && config.browserConsoleLogOptions.path) {
- const configLevel = config.browserConsoleLogOptions.level || 'debug'
- const configFormat = config.browserConsoleLogOptions.format || '%b %T: %m'
- const configPath = config.browserConsoleLogOptions.path
- this.log.info(`Writing browser console to file: ${configPath}`)
- const browserLogFile = fs.openSync(configPath, 'w+')
- const levels = ['log', 'error', 'warn', 'info', 'debug']
- this.on('browser_log', function (browser, message, level) {
- if (levels.indexOf(level.toLowerCase()) > levels.indexOf(configLevel)) {
- return
- }
- if (!helper.isString(message)) {
- message = util.inspect(message, { showHidden: false, colors: false })
- }
- const logMap = {'%m': message, '%t': level.toLowerCase(), '%T': level.toUpperCase(), '%b': browser}
- const logString = configFormat.replace(/%[mtTb]/g, (m) => logMap[m])
- this.log.debug(`Writing browser console line: ${logString}`)
- fs.writeSync(browserLogFile, logString + '\n')
- })
- }
- socketServer.sockets.on('connection', (socket) => {
- this.log.debug('A browser has connected on socket ' + socket.id)
- const replySocketEvents = events.bufferEvents(socket, ['start', 'info', 'karma_error', 'result', 'complete'])
- socket.on('complete', (data, ack) => ack())
- socket.on('register', (info) => {
- let newBrowser = info.id ? (capturedBrowsers.getById(info.id) || singleRunBrowsers.getById(info.id)) : null
- if (newBrowser) {
- newBrowser.reconnect(socket)
- // We are restarting a previously disconnected browser.
- if (newBrowser.state === Browser.STATE_DISCONNECTED && config.singleRun) {
- newBrowser.execute(config.client)
- }
- } else {
- newBrowser = this._injector.createChild([{
- id: ['value', info.id || null],
- fullName: ['value', (helper.isDefined(info.displayName) ? info.displayName : info.name)],
- socket: ['value', socket]
- }]).invoke(Browser.factory)
- newBrowser.init()
- if (config.singleRun) {
- newBrowser.execute(config.client)
- singleRunBrowsers.add(newBrowser)
- }
- }
- replySocketEvents()
- })
- })
- const emitRunCompleteIfAllBrowsersDone = () => {
- if (Object.keys(singleRunDoneBrowsers).every((key) => singleRunDoneBrowsers[key])) {
- const results = singleRunBrowsers.getResults()
- if (singleRunBrowserNotCaptured) {
- results.exitCode = 1
- } else if (results.success + results.failed === 0 && !config.failOnEmptyTestSuite) {
- results.exitCode = 0
- this.log.warn('Test suite was empty.')
- }
- this.emit('run_complete', singleRunBrowsers, results)
- }
- }
- this.on('browser_complete', (completedBrowser) => {
- if (completedBrowser.lastResult.disconnected && completedBrowser.disconnectsCount <= config.browserDisconnectTolerance) {
- this.log.info(`Restarting ${completedBrowser.name} (${completedBrowser.disconnectsCount} of ${config.browserDisconnectTolerance} attempts)`)
- if (!launcher.restart(completedBrowser.id)) {
- this.emit('browser_restart_failure', completedBrowser)
- }
- } else {
- this.emit('browser_complete_with_no_more_retries', completedBrowser)
- }
- })
- if (config.singleRun) {
- this.on('browser_restart_failure', (completedBrowser) => {
- singleRunDoneBrowsers[completedBrowser.id] = true
- emitRunCompleteIfAllBrowsersDone()
- })
- this.on('browser_complete_with_no_more_retries', function (completedBrowser) {
- singleRunDoneBrowsers[completedBrowser.id] = true
- if (launcher.kill(completedBrowser.id)) {
- // workaround to supress "disconnect" warning
- completedBrowser.state = Browser.STATE_DISCONNECTED
- }
- emitRunCompleteIfAllBrowsersDone()
- })
- this.on('browser_process_failure', (browserLauncher) => {
- singleRunDoneBrowsers[browserLauncher.id] = true
- singleRunBrowserNotCaptured = true
- emitRunCompleteIfAllBrowsersDone()
- })
- this.on('run_complete', function (browsers, results) {
- this.log.debug('Run complete, exiting.')
- disconnectBrowsers(results.exitCode)
- })
- this.emit('run_start', singleRunBrowsers)
- }
- if (config.autoWatch) {
- this.on('file_list_modified', () => {
- this.log.debug('List of files has changed, trying to execute')
- if (config.restartOnFileChange) {
- socketServer.sockets.emit('stop')
- }
- executor.schedule()
- })
- }
- const webServerCloseTimeout = 3000
- const disconnectBrowsers = (code) => {
- const sockets = socketServer.sockets.sockets
- Object.keys(sockets).forEach((id) => {
- const socket = sockets[id]
- socket.removeAllListeners('disconnect')
- if (!socket.disconnected) {
- process.nextTick(socket.disconnect.bind(socket))
- }
- })
- let removeAllListenersDone = false
- const removeAllListeners = () => {
- if (removeAllListenersDone) {
- return
- }
- removeAllListenersDone = true
- webServer.removeAllListeners()
- processWrapper.removeAllListeners()
- done(code || 0)
- }
- this.emitAsync('exit').then(() => {
- socketServer.sockets.removeAllListeners()
- socketServer.close()
- const closeTimeout = setTimeout(removeAllListeners, webServerCloseTimeout)
- webServer.close(() => {
- clearTimeout(closeTimeout)
- removeAllListeners()
- })
- })
- }
- processWrapper.on('SIGINT', () => disconnectBrowsers(process.exitCode))
- processWrapper.on('SIGTERM', disconnectBrowsers)
- processWrapper.on('uncaughtException', (error) => {
- this.log.error(error)
- disconnectBrowsers(1)
- })
- }
- _detach (config, done) {
- const tmpFile = tmp.fileSync({ keep: true })
- this.log.info('Starting karma detached')
- this.log.info('Run "karma stop" to stop the server.')
- this.log.debug(`Writing config to tmp-file ${tmpFile.name}`)
- config.detached = false
- try {
- fs.writeFileSync(tmpFile.name, JSON.stringify(config), 'utf8')
- } catch (e) {
- this.log.error("Couldn't write temporary configuration file")
- done(1)
- return
- }
- const child = spawn(process.argv[0], [path.resolve(__dirname, '../lib/detached.js'), tmpFile.name], {
- detached: true,
- stdio: 'ignore'
- })
- child.unref()
- }
- }
- Server.prototype._start.$inject = ['config', 'launcher', 'preprocess', 'fileList', 'capturedBrowsers', 'executor', 'done']
- module.exports = Server
|