addon-sdk/source/lib/sdk/system/child_process.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:2a6e1ac91eea
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 }

mercurial