| 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.substring(1, cmd.length - 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._process.stderr.on('data', function (errBuff) { | |
| errorOutput += errBuff.toString() | |
| }) | |
| } | |
| this._onProcessExit = function (code, signal, errorOutput) { | |
| 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 |