-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
process.js
185 lines (148 loc) · 5.33 KB
/
process.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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