|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 'use strict'; |
|
5 |
|
6 module.metadata = { |
|
7 'stability': 'experimental' |
|
8 }; |
|
9 |
|
10 let { Ci } = require('chrome'); |
|
11 let subprocess = require('./child_process/subprocess'); |
|
12 let { EventTarget } = require('../event/target'); |
|
13 let { Stream } = require('../io/stream'); |
|
14 let { on, emit, off } = require('../event/core'); |
|
15 let { Class } = require('../core/heritage'); |
|
16 let { platform } = require('../system'); |
|
17 let { isFunction, isArray } = require('../lang/type'); |
|
18 let { delay } = require('../lang/functional'); |
|
19 let { merge } = require('../util/object'); |
|
20 let { setTimeout, clearTimeout } = require('../timers'); |
|
21 let isWindows = platform.indexOf('win') === 0; |
|
22 |
|
23 let processes = WeakMap(); |
|
24 |
|
25 |
|
26 /** |
|
27 * The `Child` class wraps a subprocess command, exposes |
|
28 * the stdio streams, and methods to manipulate the subprocess |
|
29 */ |
|
30 let Child = Class({ |
|
31 implements: [EventTarget], |
|
32 initialize: function initialize (options) { |
|
33 let child = this; |
|
34 let proc; |
|
35 |
|
36 this.killed = false; |
|
37 this.exitCode = undefined; |
|
38 this.signalCode = undefined; |
|
39 |
|
40 this.stdin = Stream(); |
|
41 this.stdout = Stream(); |
|
42 this.stderr = Stream(); |
|
43 |
|
44 try { |
|
45 proc = subprocess.call({ |
|
46 command: options.file, |
|
47 arguments: options.cmdArgs, |
|
48 environment: serializeEnv(options.env), |
|
49 workdir: options.cwd, |
|
50 charset: options.encoding, |
|
51 stdout: data => emit(child.stdout, 'data', data), |
|
52 stderr: data => emit(child.stderr, 'data', data), |
|
53 stdin: stream => { |
|
54 child.stdin.on('data', pumpStdin); |
|
55 child.stdin.on('end', function closeStdin () { |
|
56 child.stdin.off('data', pumpStdin); |
|
57 child.stdin.off('end', closeStdin); |
|
58 stream.close(); |
|
59 }); |
|
60 function pumpStdin (data) { |
|
61 stream.write(data); |
|
62 } |
|
63 }, |
|
64 done: function (result) { |
|
65 // Only emit if child is not killed; otherwise, |
|
66 // the `kill` method will handle this |
|
67 if (!child.killed) { |
|
68 child.exitCode = result.exitCode; |
|
69 child.signalCode = null; |
|
70 |
|
71 // If process exits with < 0, there was an error |
|
72 if (child.exitCode < 0) { |
|
73 handleError(new Error('Process exited with exit code ' + child.exitCode)); |
|
74 } |
|
75 else { |
|
76 // Also do 'exit' event as there's not much of |
|
77 // a difference in our implementation as we're not using |
|
78 // node streams |
|
79 emit(child, 'exit', child.exitCode, child.signalCode); |
|
80 } |
|
81 |
|
82 // Emit 'close' event with exit code and signal, |
|
83 // which is `null`, as it was not a killed process |
|
84 emit(child, 'close', child.exitCode, child.signalCode); |
|
85 } |
|
86 } |
|
87 }); |
|
88 processes.set(child, proc); |
|
89 } catch (e) { |
|
90 // Delay the error handling so an error handler can be set |
|
91 // during the same tick that the Child was created |
|
92 delay(() => handleError(e)); |
|
93 } |
|
94 |
|
95 // `handleError` is called when process could not even |
|
96 // be spawned |
|
97 function handleError (e) { |
|
98 // If error is an nsIObject, make a fresh error object |
|
99 // so we're not exposing nsIObjects, and we can modify it |
|
100 // with additional process information, like node |
|
101 let error = e; |
|
102 if (e instanceof Ci.nsISupports) { |
|
103 error = new Error(e.message, e.filename, e.lineNumber); |
|
104 } |
|
105 emit(child, 'error', error); |
|
106 child.exitCode = -1; |
|
107 child.signalCode = null; |
|
108 emit(child, 'close', child.exitCode, child.signalCode); |
|
109 } |
|
110 }, |
|
111 kill: function kill (signal) { |
|
112 let proc = processes.get(this); |
|
113 proc.kill(signal); |
|
114 this.killed = true; |
|
115 this.exitCode = null; |
|
116 this.signalCode = signal; |
|
117 emit(this, 'exit', this.exitCode, this.signalCode); |
|
118 emit(this, 'close', this.exitCode, this.signalCode); |
|
119 }, |
|
120 get pid() { return processes.get(this, {}).pid || -1; } |
|
121 }); |
|
122 |
|
123 function spawn (file, ...args) { |
|
124 let cmdArgs = []; |
|
125 // Default options |
|
126 let options = { |
|
127 cwd: null, |
|
128 env: null, |
|
129 encoding: 'UTF-8' |
|
130 }; |
|
131 |
|
132 if (args[1]) { |
|
133 merge(options, args[1]); |
|
134 cmdArgs = args[0]; |
|
135 } |
|
136 else { |
|
137 if (isArray(args[0])) |
|
138 cmdArgs = args[0]; |
|
139 else |
|
140 merge(options, args[0]); |
|
141 } |
|
142 |
|
143 if ('gid' in options) |
|
144 console.warn('`gid` option is not yet supported for `child_process`'); |
|
145 if ('uid' in options) |
|
146 console.warn('`uid` option is not yet supported for `child_process`'); |
|
147 if ('detached' in options) |
|
148 console.warn('`detached` option is not yet supported for `child_process`'); |
|
149 |
|
150 options.file = file; |
|
151 options.cmdArgs = cmdArgs; |
|
152 |
|
153 return Child(options); |
|
154 } |
|
155 |
|
156 exports.spawn = spawn; |
|
157 |
|
158 /** |
|
159 * exec(command, options, callback) |
|
160 */ |
|
161 function exec (cmd, ...args) { |
|
162 let file, cmdArgs, callback, options = {}; |
|
163 |
|
164 if (isFunction(args[0])) |
|
165 callback = args[0]; |
|
166 else { |
|
167 merge(options, args[0]); |
|
168 callback = args[1]; |
|
169 } |
|
170 |
|
171 if (isWindows) { |
|
172 file = 'C:\\Windows\\System32\\cmd.exe'; |
|
173 cmdArgs = ['/s', '/c', (cmd || '').split(' ')]; |
|
174 } |
|
175 else { |
|
176 file = '/bin/sh'; |
|
177 cmdArgs = ['-c', cmd]; |
|
178 } |
|
179 |
|
180 // Undocumented option from node being able to specify shell |
|
181 if (options && options.shell) |
|
182 file = options.shell; |
|
183 |
|
184 return execFile(file, cmdArgs, options, callback); |
|
185 } |
|
186 exports.exec = exec; |
|
187 /** |
|
188 * execFile (file, args, options, callback) |
|
189 */ |
|
190 function execFile (file, ...args) { |
|
191 let cmdArgs = [], callback; |
|
192 // Default options |
|
193 let options = { |
|
194 cwd: null, |
|
195 env: null, |
|
196 encoding: 'utf8', |
|
197 timeout: 0, |
|
198 maxBuffer: 200 * 1024, |
|
199 killSignal: 'SIGTERM' |
|
200 }; |
|
201 |
|
202 if (isFunction(args[args.length - 1])) |
|
203 callback = args[args.length - 1]; |
|
204 |
|
205 if (isArray(args[0])) { |
|
206 cmdArgs = args[0]; |
|
207 merge(options, args[1]); |
|
208 } else if (!isFunction(args[0])) |
|
209 merge(options, args[0]); |
|
210 |
|
211 let child = spawn(file, cmdArgs, options); |
|
212 let exited = false; |
|
213 let stdout = ''; |
|
214 let stderr = ''; |
|
215 let error = null; |
|
216 let timeoutId = null; |
|
217 |
|
218 child.stdout.setEncoding(options.encoding); |
|
219 child.stderr.setEncoding(options.encoding); |
|
220 |
|
221 on(child.stdout, 'data', pumpStdout); |
|
222 on(child.stderr, 'data', pumpStderr); |
|
223 on(child, 'close', exitHandler); |
|
224 on(child, 'error', errorHandler); |
|
225 |
|
226 if (options.timeout > 0) { |
|
227 setTimeout(() => { |
|
228 kill(); |
|
229 timeoutId = null; |
|
230 }, options.timeout); |
|
231 } |
|
232 |
|
233 function exitHandler (code, signal) { |
|
234 |
|
235 // Return if exitHandler called previously, occurs |
|
236 // when multiple maxBuffer errors thrown and attempt to kill multiple |
|
237 // times |
|
238 if (exited) return; |
|
239 exited = true; |
|
240 |
|
241 if (!isFunction(callback)) return; |
|
242 |
|
243 if (timeoutId) { |
|
244 clearTimeout(timeoutId); |
|
245 timeoutId = null; |
|
246 } |
|
247 |
|
248 if (!error && (code !== 0 || signal !== null)) |
|
249 error = createProcessError(new Error('Command failed: ' + stderr), { |
|
250 code: code, |
|
251 signal: signal, |
|
252 killed: !!child.killed |
|
253 }); |
|
254 |
|
255 callback(error, stdout, stderr); |
|
256 |
|
257 off(child.stdout, 'data', pumpStdout); |
|
258 off(child.stderr, 'data', pumpStderr); |
|
259 off(child, 'close', exitHandler); |
|
260 off(child, 'error', errorHandler); |
|
261 } |
|
262 |
|
263 function errorHandler (e) { |
|
264 error = e; |
|
265 exitHandler(); |
|
266 } |
|
267 |
|
268 function kill () { |
|
269 try { |
|
270 child.kill(options.killSignal); |
|
271 } catch (e) { |
|
272 // In the scenario where the kill signal happens when |
|
273 // the process is already closing, just abort the kill fail |
|
274 if (/library is not open/.test(e)) |
|
275 return; |
|
276 error = e; |
|
277 exitHandler(-1, options.killSignal); |
|
278 } |
|
279 } |
|
280 |
|
281 function pumpStdout (data) { |
|
282 stdout += data; |
|
283 if (stdout.length > options.maxBuffer) { |
|
284 error = new Error('stdout maxBuffer exceeded'); |
|
285 kill(); |
|
286 } |
|
287 } |
|
288 |
|
289 function pumpStderr (data) { |
|
290 stderr += data; |
|
291 if (stderr.length > options.maxBuffer) { |
|
292 error = new Error('stderr maxBuffer exceeded'); |
|
293 kill(); |
|
294 } |
|
295 } |
|
296 |
|
297 return child; |
|
298 } |
|
299 exports.execFile = execFile; |
|
300 |
|
301 exports.fork = function fork () { |
|
302 throw new Error("child_process#fork is not currently supported"); |
|
303 }; |
|
304 |
|
305 function serializeEnv (obj) { |
|
306 return Object.keys(obj || {}).map(prop => prop + '=' + obj[prop]); |
|
307 } |
|
308 |
|
309 function createProcessError (err, options = {}) { |
|
310 // If code and signal look OK, this was probably a failure |
|
311 // attempting to spawn the process (like ENOENT in node) -- use |
|
312 // the code from the error message |
|
313 if (!options.code && !options.signal) { |
|
314 let match = err.message.match(/(NS_ERROR_\w*)/); |
|
315 if (match && match.length > 1) |
|
316 err.code = match[1]; |
|
317 else { |
|
318 // If no good error message found, use the passed in exit code; |
|
319 // this occurs when killing a process that's already closing, |
|
320 // where we want both a valid exit code (0) and the error |
|
321 err.code = options.code != null ? options.code : null; |
|
322 } |
|
323 } |
|
324 else |
|
325 err.code = options.code != null ? options.code : null; |
|
326 err.signal = options.signal || null; |
|
327 err.killed = options.killed || false; |
|
328 return err; |
|
329 } |