123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- const path = require('path')
- const log = require('../logger').create('launcher')
- const env = process.env
- function ProcessLauncher (spawn, tempDir, timer, processKillTimeout) {
- const self = this
- let onExitCallback
- const killTimeout = processKillTimeout || 2000
- // Will hold output from the spawned child process
- const streamedOutputs = {
- stdout: '',
- stderr: ''
- }
- this._tempDir = tempDir.getPath(`/karma-${this.id.toString()}`)
- this.on('start', function (url) {
- tempDir.create(self._tempDir)
- self._start(url)
- })
- this.on('kill', function (done) {
- if (!self._process) {
- return process.nextTick(done)
- }
- onExitCallback = done
- self._process.kill()
- self._killTimer = timer.setTimeout(self._onKillTimeout, killTimeout)
- })
- this._start = function (url) {
- self._execCommand(self._getCommand(), self._getOptions(url))
- }
- this._getCommand = function () {
- return env[self.ENV_CMD] || self.DEFAULT_CMD[process.platform]
- }
- this._getOptions = function (url) {
- return [url]
- }
- // Normalize the command, remove quotes (spawn does not like them).
- this._normalizeCommand = function (cmd) {
- if (cmd.charAt(0) === cmd.charAt(cmd.length - 1) && '\'`"'.includes(cmd.charAt(0))) {
- cmd = cmd.slice(1, -1)
- log.warn(`The path should not be quoted.\n Normalized the path to ${cmd}`)
- }
- return path.normalize(cmd)
- }
- this._onStdout = function (data) {
- streamedOutputs.stdout += data
- }
- this._onStderr = function (data) {
- streamedOutputs.stderr += data
- }
- this._execCommand = function (cmd, args) {
- if (!cmd) {
- log.error(`No binary for ${self.name} browser on your platform.\n Please, set "${self.ENV_CMD}" env variable.`)
- // disable restarting
- self._retryLimit = -1
- return self._clearTempDirAndReportDone('no binary')
- }
- cmd = this._normalizeCommand(cmd)
- log.debug(cmd + ' ' + args.join(' '))
- self._process = spawn(cmd, args)
- let errorOutput = ''
- self._process.stdout.on('data', self._onStdout)
- self._process.stderr.on('data', self._onStderr)
- self._process.on('exit', function (code, signal) {
- self._onProcessExit(code, signal, errorOutput)
- })
- self._process.on('error', function (err) {
- if (err.code === 'ENOENT') {
- self._retryLimit = -1
- errorOutput = `Can not find the binary ${cmd}\n\tPlease set env variable ${self.ENV_CMD}`
- } else if (err.code === 'EACCES') {
- self._retryLimit = -1
- errorOutput = `Permission denied accessing the binary ${cmd}\n\tMaybe it's a directory?`
- } else {
- errorOutput += err.toString()
- }
- self._onProcessExit(-1, null, errorOutput)
- })
- self._process.stderr.on('data', function (errBuff) {
- errorOutput += errBuff.toString()
- })
- }
- this._onProcessExit = function (code, signal, errorOutput) {
- if (!self._process) {
- // Both exit and error events trigger _onProcessExit(), but we only need one cleanup.
- return
- }
- log.debug(`Process ${self.name} exited with code ${code} and signal ${signal}`)
- let error = null
- if (self.state === self.STATE_BEING_CAPTURED) {
- log.error(`Cannot start ${self.name}\n\t${errorOutput}`)
- error = 'cannot start'
- }
- if (self.state === self.STATE_CAPTURED) {
- log.error(`${self.name} crashed.\n\t${errorOutput}`)
- error = 'crashed'
- }
- if (error) {
- log.error(`${self.name} stdout: ${streamedOutputs.stdout}`)
- log.error(`${self.name} stderr: ${streamedOutputs.stderr}`)
- }
- self._process = null
- streamedOutputs.stdout = ''
- streamedOutputs.stderr = ''
- if (self._killTimer) {
- timer.clearTimeout(self._killTimer)
- self._killTimer = null
- }
- self._clearTempDirAndReportDone(error)
- }
- this._clearTempDirAndReportDone = function (error) {
- tempDir.remove(self._tempDir, function () {
- self._done(error)
- if (onExitCallback) {
- onExitCallback()
- onExitCallback = null
- }
- })
- }
- this._onKillTimeout = function () {
- if (self.state !== self.STATE_BEING_KILLED && self.state !== self.STATE_BEING_FORCE_KILLED) {
- return
- }
- log.warn(`${self.name} was not killed in ${killTimeout} ms, sending SIGKILL.`)
- self._process.kill('SIGKILL')
- // NOTE: https://github.com/karma-runner/karma/pull/1184
- // NOTE: SIGKILL is just a signal. Processes should never ignore it, but they can.
- // If a process gets into a state where it doesn't respond in a reasonable amount of time
- // Karma should warn, and continue as though the kill succeeded.
- // This a certainly suboptimal, but it is better than having the test harness hang waiting
- // for a zombie child process to exit.
- self._killTimer = timer.setTimeout(function () {
- log.warn(`${self.name} was not killed by SIGKILL in ${killTimeout} ms, continuing.`)
- self._onProcessExit(-1, null, '')
- }, killTimeout)
- }
- }
- ProcessLauncher.decoratorFactory = function (timer) {
- return function (launcher, processKillTimeout) {
- const spawn = require('child_process').spawn
- function spawnWithoutOutput () {
- const proc = spawn.apply(null, arguments)
- proc.stdout.resume()
- proc.stderr.resume()
- return proc
- }
- ProcessLauncher.call(launcher, spawnWithoutOutput, require('../temp_dir'), timer, processKillTimeout)
- }
- }
- module.exports = ProcessLauncher
|