process.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. var path = require('path')
  2. var log = require('../logger').create('launcher')
  3. var env = process.env
  4. var ProcessLauncher = function (spawn, tempDir, timer, processKillTimeout) {
  5. var self = this
  6. var onExitCallback
  7. var killTimeout = processKillTimeout || 2000
  8. // Will hold output from the spawned child process
  9. var streamedOutputs = {
  10. stdout: '',
  11. stderr: ''
  12. }
  13. this._tempDir = tempDir.getPath('/karma-' + this.id.toString())
  14. this.on('start', function (url) {
  15. tempDir.create(self._tempDir)
  16. self._start(url)
  17. })
  18. this.on('kill', function (done) {
  19. if (!self._process) {
  20. return process.nextTick(done)
  21. }
  22. onExitCallback = done
  23. self._process.kill()
  24. self._killTimer = timer.setTimeout(self._onKillTimeout, killTimeout)
  25. })
  26. this._start = function (url) {
  27. self._execCommand(self._getCommand(), self._getOptions(url))
  28. }
  29. this._getCommand = function () {
  30. return env[self.ENV_CMD] || self.DEFAULT_CMD[process.platform]
  31. }
  32. this._getOptions = function (url) {
  33. return [url]
  34. }
  35. // Normalize the command, remove quotes (spawn does not like them).
  36. this._normalizeCommand = function (cmd) {
  37. if (cmd.charAt(0) === cmd.charAt(cmd.length - 1) && '\'`"'.indexOf(cmd.charAt(0)) !== -1) {
  38. cmd = cmd.substring(1, cmd.length - 1)
  39. log.warn('The path should not be quoted.\n Normalized the path to %s', cmd)
  40. }
  41. return path.normalize(cmd)
  42. }
  43. this._onStdout = function (data) {
  44. streamedOutputs.stdout += data
  45. }
  46. this._onStderr = function (data) {
  47. streamedOutputs.stderr += data
  48. }
  49. this._execCommand = function (cmd, args) {
  50. if (!cmd) {
  51. log.error('No binary for %s browser on your platform.\n ' +
  52. 'Please, set "%s" env variable.', self.name, self.ENV_CMD)
  53. // disable restarting
  54. self._retryLimit = -1
  55. return self._clearTempDirAndReportDone('no binary')
  56. }
  57. cmd = this._normalizeCommand(cmd)
  58. log.debug(cmd + ' ' + args.join(' '))
  59. self._process = spawn(cmd, args)
  60. var errorOutput = ''
  61. self._process.stdout.on('data', self._onStdout)
  62. self._process.stderr.on('data', self._onStderr)
  63. self._process.on('exit', function (code) {
  64. self._onProcessExit(code, errorOutput)
  65. })
  66. self._process.on('error', function (err) {
  67. if (err.code === 'ENOENT') {
  68. self._retryLimit = -1
  69. errorOutput = 'Can not find the binary ' + cmd + '\n\t' +
  70. 'Please set env variable ' + self.ENV_CMD
  71. } else {
  72. errorOutput += err.toString()
  73. }
  74. })
  75. self._process.stderr.on('data', function (errBuff) {
  76. errorOutput += errBuff.toString()
  77. })
  78. }
  79. this._onProcessExit = function (code, errorOutput) {
  80. log.debug('Process %s exited with code %d', self.name, code)
  81. var error = null
  82. if (self.state === self.STATE_BEING_CAPTURED) {
  83. log.error('Cannot start %s\n\t%s', self.name, errorOutput)
  84. error = 'cannot start'
  85. }
  86. if (self.state === self.STATE_CAPTURED) {
  87. log.error('%s crashed.\n\t%s', self.name, errorOutput)
  88. error = 'crashed'
  89. }
  90. if (error) {
  91. log.error('%s stdout: %s', self.name, streamedOutputs.stdout)
  92. log.error('%s stderr: %s', self.name, streamedOutputs.stderr)
  93. }
  94. self._process = null
  95. streamedOutputs.stdout = ''
  96. streamedOutputs.stderr = ''
  97. if (self._killTimer) {
  98. timer.clearTimeout(self._killTimer)
  99. self._killTimer = null
  100. }
  101. self._clearTempDirAndReportDone(error)
  102. }
  103. this._clearTempDirAndReportDone = function (error) {
  104. tempDir.remove(self._tempDir, function () {
  105. self._done(error)
  106. if (onExitCallback) {
  107. onExitCallback()
  108. onExitCallback = null
  109. }
  110. })
  111. }
  112. this._onKillTimeout = function () {
  113. if (self.state !== self.STATE_BEING_KILLED && self.state !== self.STATE_BEING_FORCE_KILLED) {
  114. return
  115. }
  116. log.warn('%s was not killed in %d ms, sending SIGKILL.', self.name, killTimeout)
  117. self._process.kill('SIGKILL')
  118. // NOTE: https://github.com/karma-runner/karma/pull/1184
  119. // NOTE: SIGKILL is just a signal. Processes should never ignore it, but they can.
  120. // If a process gets into a state where it doesn't respond in a reasonable amount of time
  121. // Karma should warn, and continue as though the kill succeeded.
  122. // This a certainly suboptimal, but it is better than having the test harness hang waiting
  123. // for a zombie child process to exit.
  124. self._killTimer = timer.setTimeout(function () {
  125. log.warn('%s was not killed by SIGKILL in %d ms, continuing.', self.name, killTimeout)
  126. self._onProcessExit(-1, '')
  127. }, killTimeout)
  128. }
  129. }
  130. ProcessLauncher.decoratorFactory = function (timer) {
  131. return function (launcher, processKillTimeout) {
  132. var spawn = require('child_process').spawn
  133. var spawnWithoutOutput = function () {
  134. var proc = spawn.apply(null, arguments)
  135. proc.stdout.resume()
  136. proc.stderr.resume()
  137. return proc
  138. }
  139. ProcessLauncher.call(launcher, spawnWithoutOutput, require('../temp_dir'), timer, processKillTimeout)
  140. }
  141. }
  142. module.exports = ProcessLauncher