|
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 |
|
5 'use strict'; |
|
6 |
|
7 const { spawn, exec, execFile, fork } = require('sdk/system/child_process'); |
|
8 const { env, platform, pathFor } = require('sdk/system'); |
|
9 const { isNumber } = require('sdk/lang/type'); |
|
10 const { after } = require('sdk/test/utils'); |
|
11 const { emit } = require('sdk/event/core'); |
|
12 const PROFILE_DIR= pathFor('ProfD'); |
|
13 const isWindows = platform.toLowerCase().indexOf('win') === 0; |
|
14 const { getScript, cleanUp } = require('./fixtures/child-process-scripts'); |
|
15 |
|
16 // We use direct paths to these utilities as we currently cannot |
|
17 // call non-absolute paths to utilities in subprocess.jsm |
|
18 const CAT_PATH = isWindows ? 'C:\\Windows\\System32\\more.com' : '/bin/cat'; |
|
19 |
|
20 exports.testExecCallbackSuccess = function (assert, done) { |
|
21 exec(isWindows ? 'DIR /A-D' : 'ls -al', { |
|
22 cwd: PROFILE_DIR |
|
23 }, function (err, stdout, stderr) { |
|
24 assert.ok(!err, 'no errors found'); |
|
25 assert.equal(stderr, '', 'stderr is empty'); |
|
26 assert.ok(/extensions\.ini/.test(stdout), 'stdout output of `ls -al` finds files'); |
|
27 |
|
28 if (isWindows) { |
|
29 // `DIR /A-D` does not display directories on WIN |
|
30 assert.ok(!/<DIR>/.test(stdout), |
|
31 'passing arguments in `exec` works'); |
|
32 } |
|
33 else { |
|
34 // `ls -al` should list all the priviledge information on Unix |
|
35 assert.ok(/d(r[-|w][-|x]){3}/.test(stdout), |
|
36 'passing arguments in `exec` works'); |
|
37 } |
|
38 done(); |
|
39 }); |
|
40 }; |
|
41 |
|
42 exports.testExecCallbackError = function (assert, done) { |
|
43 exec('not-real-command', { cwd: PROFILE_DIR }, function (err, stdout, stderr) { |
|
44 assert.ok(/not-real-command/.test(err.toString()), |
|
45 'error contains error message'); |
|
46 assert.ok(err.lineNumber >= 0, 'error contains lineNumber'); |
|
47 assert.ok(/resource:\/\//.test(err.fileName), 'error contains fileName'); |
|
48 assert.ok(err.code && isNumber(err.code), 'non-zero error code property on error'); |
|
49 assert.equal(err.signal, null, |
|
50 'null signal property when not manually terminated'); |
|
51 assert.equal(stdout, '', 'stdout is empty'); |
|
52 assert.ok(/not-real-command/.test(stderr), 'stderr contains error message'); |
|
53 done(); |
|
54 }); |
|
55 }; |
|
56 |
|
57 exports.testExecOptionsEnvironment = function (assert, done) { |
|
58 getScript('check-env').then(envScript => { |
|
59 exec(envScript, { |
|
60 cwd: PROFILE_DIR, |
|
61 env: { CHILD_PROCESS_ENV_TEST: 'my-value-test' } |
|
62 }, function (err, stdout, stderr) { |
|
63 assert.equal(stderr, '', 'stderr is empty'); |
|
64 assert.ok(!err, 'received `cwd` option'); |
|
65 assert.ok(/my-value-test/.test(stdout), |
|
66 'receives environment option'); |
|
67 done(); |
|
68 }); |
|
69 }); |
|
70 }; |
|
71 |
|
72 exports.testExecOptionsTimeout = function (assert, done) { |
|
73 let count = 0; |
|
74 getScript('wait').then(script => { |
|
75 let child = exec(script, { timeout: 100 }, (err, stdout, stderr) => { |
|
76 assert.equal(err.killed, true, 'error has `killed` property as true'); |
|
77 assert.equal(err.code, null, 'error has `code` as null'); |
|
78 assert.equal(err.signal, 'SIGTERM', |
|
79 'error has `signal` as SIGTERM by default'); |
|
80 assert.equal(stdout, '', 'stdout is empty'); |
|
81 assert.equal(stderr, '', 'stderr is empty'); |
|
82 if (++count === 3) complete(); |
|
83 }); |
|
84 |
|
85 function exitHandler (code, signal) { |
|
86 assert.equal(code, null, 'error has `code` as null'); |
|
87 assert.equal(signal, 'SIGTERM', |
|
88 'error has `signal` as SIGTERM by default'); |
|
89 if (++count === 3) complete(); |
|
90 } |
|
91 |
|
92 function closeHandler (code, signal) { |
|
93 assert.equal(code, null, 'error has `code` as null'); |
|
94 assert.equal(signal, 'SIGTERM', |
|
95 'error has `signal` as SIGTERM by default'); |
|
96 if (++count === 3) complete(); |
|
97 } |
|
98 |
|
99 child.on('exit', exitHandler); |
|
100 child.on('close', closeHandler); |
|
101 |
|
102 function complete () { |
|
103 child.off('exit', exitHandler); |
|
104 child.off('close', closeHandler); |
|
105 done(); |
|
106 } |
|
107 }); |
|
108 }; |
|
109 |
|
110 exports.testExecFileCallbackSuccess = function (assert, done) { |
|
111 getScript('args').then(script => { |
|
112 execFile(script, ['--myargs', '-j', '-s'], { cwd: PROFILE_DIR }, function (err, stdout, stderr) { |
|
113 assert.ok(!err, 'no errors found'); |
|
114 assert.equal(stderr, '', 'stderr is empty'); |
|
115 // Trim output since different systems have different new line output |
|
116 assert.equal(stdout.trim(), '--myargs -j -s'.trim(), 'passes in correct arguments'); |
|
117 done(); |
|
118 }); |
|
119 }); |
|
120 }; |
|
121 |
|
122 exports.testExecFileCallbackError = function (assert, done) { |
|
123 execFile('not-real-command', { cwd: PROFILE_DIR }, function (err, stdout, stderr) { |
|
124 assert.ok(/NS_ERROR_FILE_UNRECOGNIZED_PATH/.test(err.message), |
|
125 'error contains error message'); |
|
126 assert.ok(err.lineNumber >= 0, 'error contains lineNumber'); |
|
127 assert.ok(/resource:\/\//.test(err.fileName), 'error contains fileName'); |
|
128 assert.equal(stdout, '', 'stdout is empty'); |
|
129 assert.equal(stderr, '', 'stdout is empty'); |
|
130 done(); |
|
131 }); |
|
132 }; |
|
133 |
|
134 exports.testExecFileOptionsEnvironment = function (assert, done) { |
|
135 getScript('check-env').then(script => { |
|
136 execFile(script, { |
|
137 cwd: PROFILE_DIR, |
|
138 env: { CHILD_PROCESS_ENV_TEST: 'my-value-test' } |
|
139 }, function (err, stdout, stderr) { |
|
140 assert.equal(stderr, '', 'stderr is empty'); |
|
141 assert.ok(!err, 'received `cwd` option'); |
|
142 assert.ok(/my-value-test/.test(stdout), |
|
143 'receives environment option'); |
|
144 done(); |
|
145 }); |
|
146 }); |
|
147 }; |
|
148 |
|
149 exports.testExecFileOptionsTimeout = function (assert, done) { |
|
150 let count = 0; |
|
151 getScript('wait').then(script => { |
|
152 let child = execFile(script, { timeout: 100 }, (err, stdout, stderr) => { |
|
153 assert.equal(err.killed, true, 'error has `killed` property as true'); |
|
154 assert.equal(err.code, null, 'error has `code` as null'); |
|
155 assert.equal(err.signal, 'SIGTERM', |
|
156 'error has `signal` as SIGTERM by default'); |
|
157 assert.equal(stdout, '', 'stdout is empty'); |
|
158 assert.equal(stderr, '', 'stderr is empty'); |
|
159 if (++count === 3) complete(); |
|
160 }); |
|
161 |
|
162 function exitHandler (code, signal) { |
|
163 assert.equal(code, null, 'error has `code` as null'); |
|
164 assert.equal(signal, 'SIGTERM', |
|
165 'error has `signal` as SIGTERM by default'); |
|
166 if (++count === 3) complete(); |
|
167 } |
|
168 |
|
169 function closeHandler (code, signal) { |
|
170 assert.equal(code, null, 'error has `code` as null'); |
|
171 assert.equal(signal, 'SIGTERM', |
|
172 'error has `signal` as SIGTERM by default'); |
|
173 if (++count === 3) complete(); |
|
174 } |
|
175 |
|
176 child.on('exit', exitHandler); |
|
177 child.on('close', closeHandler); |
|
178 |
|
179 function complete () { |
|
180 child.off('exit', exitHandler); |
|
181 child.off('close', closeHandler); |
|
182 done(); |
|
183 } |
|
184 }); |
|
185 }; |
|
186 |
|
187 /** |
|
188 * Not necessary to test for both `exec` and `execFile`, but |
|
189 * it is necessary to test both when the buffer is larger |
|
190 * and smaller than buffer size used by the subprocess library (1024) |
|
191 */ |
|
192 exports.testExecFileOptionsMaxBufferLargeStdOut = function (assert, done) { |
|
193 let count = 0; |
|
194 let stdoutChild; |
|
195 |
|
196 // Creates a buffer of 2000 to stdout, greater than 1024 |
|
197 getScript('large-out').then(script => { |
|
198 stdoutChild = execFile(script, ['10000'], { maxBuffer: 50 }, (err, stdout, stderr) => { |
|
199 assert.ok(/stdout maxBuffer exceeded/.test(err.toString()), |
|
200 'error contains stdout maxBuffer exceeded message'); |
|
201 assert.ok(stdout.length >= 50, 'stdout has full buffer'); |
|
202 assert.equal(stderr, '', 'stderr is empty'); |
|
203 if (++count === 3) complete(); |
|
204 }); |
|
205 stdoutChild.on('exit', exitHandler); |
|
206 stdoutChild.on('close', closeHandler); |
|
207 }); |
|
208 |
|
209 function exitHandler (code, signal) { |
|
210 assert.equal(code, null, 'Exit code is null in exit handler'); |
|
211 assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in exit handler'); |
|
212 if (++count === 3) complete(); |
|
213 } |
|
214 |
|
215 function closeHandler (code, signal) { |
|
216 assert.equal(code, null, 'Exit code is null in close handler'); |
|
217 assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in close handler'); |
|
218 if (++count === 3) complete(); |
|
219 } |
|
220 |
|
221 function complete () { |
|
222 stdoutChild.off('exit', exitHandler); |
|
223 stdoutChild.off('close', closeHandler); |
|
224 done(); |
|
225 } |
|
226 }; |
|
227 |
|
228 exports.testExecFileOptionsMaxBufferLargeStdOErr = function (assert, done) { |
|
229 let count = 0; |
|
230 let stderrChild; |
|
231 // Creates a buffer of 2000 to stderr, greater than 1024 |
|
232 getScript('large-err').then(script => { |
|
233 stderrChild = execFile(script, ['10000'], { maxBuffer: 50 }, (err, stdout, stderr) => { |
|
234 assert.ok(/stderr maxBuffer exceeded/.test(err.toString()), |
|
235 'error contains stderr maxBuffer exceeded message'); |
|
236 assert.ok(stderr.length >= 50, 'stderr has full buffer'); |
|
237 assert.equal(stdout, '', 'stdout is empty'); |
|
238 if (++count === 3) complete(); |
|
239 }); |
|
240 stderrChild.on('exit', exitHandler); |
|
241 stderrChild.on('close', closeHandler); |
|
242 }); |
|
243 |
|
244 function exitHandler (code, signal) { |
|
245 assert.equal(code, null, 'Exit code is null in exit handler'); |
|
246 assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in exit handler'); |
|
247 if (++count === 3) complete(); |
|
248 } |
|
249 |
|
250 function closeHandler (code, signal) { |
|
251 assert.equal(code, null, 'Exit code is null in close handler'); |
|
252 assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in close handler'); |
|
253 if (++count === 3) complete(); |
|
254 } |
|
255 |
|
256 function complete () { |
|
257 stderrChild.off('exit', exitHandler); |
|
258 stderrChild.off('close', closeHandler); |
|
259 done(); |
|
260 } |
|
261 }; |
|
262 |
|
263 /** |
|
264 * When total buffer is < process buffer (1024), the process will exit |
|
265 * and not get a chance to be killed for violating the maxBuffer, |
|
266 * although the error will still be sent through (node behaviour) |
|
267 */ |
|
268 exports.testExecFileOptionsMaxBufferSmallStdOut = function (assert, done) { |
|
269 let count = 0; |
|
270 let stdoutChild; |
|
271 |
|
272 // Creates a buffer of 60 to stdout, less than 1024 |
|
273 getScript('large-out').then(script => { |
|
274 stdoutChild = execFile(script, ['60'], { maxBuffer: 50 }, (err, stdout, stderr) => { |
|
275 assert.ok(/stdout maxBuffer exceeded/.test(err.toString()), |
|
276 'error contains stdout maxBuffer exceeded message'); |
|
277 assert.ok(stdout.length >= 50, 'stdout has full buffer'); |
|
278 assert.equal(stderr, '', 'stderr is empty'); |
|
279 if (++count === 3) complete(); |
|
280 }); |
|
281 stdoutChild.on('exit', exitHandler); |
|
282 stdoutChild.on('close', closeHandler); |
|
283 }); |
|
284 |
|
285 function exitHandler (code, signal) { |
|
286 // Sometimes the buffer limit is hit before the process closes successfully |
|
287 // on both OSX/Windows |
|
288 if (code === null) { |
|
289 assert.equal(code, null, 'Exit code is null in exit handler'); |
|
290 assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in exit handler'); |
|
291 } |
|
292 else { |
|
293 assert.equal(code, 0, 'Exit code is 0 in exit handler'); |
|
294 assert.equal(signal, null, 'Signal is null in exit handler'); |
|
295 } |
|
296 if (++count === 3) complete(); |
|
297 } |
|
298 |
|
299 function closeHandler (code, signal) { |
|
300 // Sometimes the buffer limit is hit before the process closes successfully |
|
301 // on both OSX/Windows |
|
302 if (code === null) { |
|
303 assert.equal(code, null, 'Exit code is null in close handler'); |
|
304 assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in close handler'); |
|
305 } |
|
306 else { |
|
307 assert.equal(code, 0, 'Exit code is 0 in close handler'); |
|
308 assert.equal(signal, null, 'Signal is null in close handler'); |
|
309 } |
|
310 if (++count === 3) complete(); |
|
311 } |
|
312 |
|
313 function complete () { |
|
314 stdoutChild.off('exit', exitHandler); |
|
315 stdoutChild.off('close', closeHandler); |
|
316 done(); |
|
317 } |
|
318 }; |
|
319 |
|
320 exports.testExecFileOptionsMaxBufferSmallStdErr = function (assert, done) { |
|
321 let count = 0; |
|
322 let stderrChild; |
|
323 // Creates a buffer of 60 to stderr, less than 1024 |
|
324 getScript('large-err').then(script => { |
|
325 stderrChild = execFile(script, ['60'], { maxBuffer: 50 }, (err, stdout, stderr) => { |
|
326 assert.ok(/stderr maxBuffer exceeded/.test(err.toString()), |
|
327 'error contains stderr maxBuffer exceeded message'); |
|
328 assert.ok(stderr.length >= 50, 'stderr has full buffer'); |
|
329 assert.equal(stdout, '', 'stdout is empty'); |
|
330 if (++count === 3) complete(); |
|
331 }); |
|
332 stderrChild.on('exit', exitHandler); |
|
333 stderrChild.on('close', closeHandler); |
|
334 }); |
|
335 |
|
336 function exitHandler (code, signal) { |
|
337 // Sometimes the buffer limit is hit before the process closes successfully |
|
338 // on both OSX/Windows |
|
339 if (code === null) { |
|
340 assert.equal(code, null, 'Exit code is null in exit handler'); |
|
341 assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in exit handler'); |
|
342 } |
|
343 else { |
|
344 assert.equal(code, 0, 'Exit code is 0 in exit handler'); |
|
345 assert.equal(signal, null, 'Signal is null in exit handler'); |
|
346 } |
|
347 if (++count === 3) complete(); |
|
348 } |
|
349 |
|
350 function closeHandler (code, signal) { |
|
351 // Sometimes the buffer limit is hit before the process closes successfully |
|
352 // on both OSX/Windows |
|
353 if (code === null) { |
|
354 assert.equal(code, null, 'Exit code is null in close handler'); |
|
355 assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in close handler'); |
|
356 } |
|
357 else { |
|
358 assert.equal(code, 0, 'Exit code is 0 in close handler'); |
|
359 assert.equal(signal, null, 'Signal is null in close handler'); |
|
360 } |
|
361 if (++count === 3) complete(); |
|
362 } |
|
363 |
|
364 function complete () { |
|
365 stderrChild.off('exit', exitHandler); |
|
366 stderrChild.off('close', closeHandler); |
|
367 done(); |
|
368 } |
|
369 }; |
|
370 |
|
371 exports.testChildExecFileKillSignal = function (assert, done) { |
|
372 getScript('wait').then(script => { |
|
373 execFile(script, { |
|
374 killSignal: 'beepbeep', |
|
375 timeout: 10 |
|
376 }, function (err, stdout, stderr) { |
|
377 assert.equal(err.signal, 'beepbeep', 'correctly used custom killSignal'); |
|
378 done(); |
|
379 }); |
|
380 }); |
|
381 }; |
|
382 |
|
383 exports.testChildProperties = function (assert, done) { |
|
384 getScript('check-env').then(script => { |
|
385 let child = spawn(script, { |
|
386 env: { CHILD_PROCESS_ENV_TEST: 'my-value-test' } |
|
387 }); |
|
388 |
|
389 if (isWindows) |
|
390 assert.ok(true, 'Windows environment does not have `pid`'); |
|
391 else |
|
392 assert.ok(child.pid > 0, 'Child has a pid'); |
|
393 done(); |
|
394 }); |
|
395 }; |
|
396 |
|
397 exports.testChildStdinStreamLarge = function (assert, done) { |
|
398 let REPEAT = 2000; |
|
399 let allData = ''; |
|
400 // Use direct paths to more/cat, as we do not currently support calling non-files |
|
401 // from subprocess.jsm |
|
402 let child = spawn(CAT_PATH); |
|
403 |
|
404 child.stdout.on('data', onData); |
|
405 child.on('close', onClose); |
|
406 |
|
407 for (let i = 0; i < REPEAT; i++) |
|
408 emit(child.stdin, 'data', '12345\n'); |
|
409 |
|
410 emit(child.stdin, 'end'); |
|
411 |
|
412 function onData (data) { |
|
413 allData += data; |
|
414 } |
|
415 |
|
416 function onClose (code, signal) { |
|
417 child.stdout.off('data', onData); |
|
418 child.off('close', onClose); |
|
419 assert.equal(code, 0, 'exited succesfully'); |
|
420 assert.equal(signal, null, 'no kill signal given'); |
|
421 assert.equal(allData.replace(/\W/g, '').length, '12345'.length * REPEAT, |
|
422 'all data processed from stdin'); |
|
423 done(); |
|
424 } |
|
425 }; |
|
426 |
|
427 exports.testChildStdinStreamSmall = function (assert, done) { |
|
428 let allData = ''; |
|
429 let child = spawn(CAT_PATH); |
|
430 child.stdout.on('data', onData); |
|
431 child.on('close', onClose); |
|
432 |
|
433 emit(child.stdin, 'data', '12345'); |
|
434 emit(child.stdin, 'end'); |
|
435 |
|
436 function onData (data) { |
|
437 allData += data; |
|
438 } |
|
439 |
|
440 function onClose (code, signal) { |
|
441 child.stdout.off('data', onData); |
|
442 child.off('close', onClose); |
|
443 assert.equal(code, 0, 'exited succesfully'); |
|
444 assert.equal(signal, null, 'no kill signal given'); |
|
445 assert.equal(allData.trim(), '12345', 'all data processed from stdin'); |
|
446 done(); |
|
447 } |
|
448 }; |
|
449 /* |
|
450 * This tests failures when an error is thrown attempting to |
|
451 * spawn the process, like an invalid command |
|
452 */ |
|
453 exports.testChildEventsSpawningError= function (assert, done) { |
|
454 let handlersCalled = 0; |
|
455 let child = execFile('i-do-not-exist', (err, stdout, stderr) => { |
|
456 assert.ok(err, 'error was passed into callback'); |
|
457 assert.equal(stdout, '', 'stdout is empty') |
|
458 assert.equal(stderr, '', 'stderr is empty'); |
|
459 if (++handlersCalled === 3) complete(); |
|
460 }); |
|
461 |
|
462 child.on('error', handleError); |
|
463 child.on('exit', handleExit); |
|
464 child.on('close', handleClose); |
|
465 |
|
466 function handleError (e) { |
|
467 assert.ok(e, 'error passed into error handler'); |
|
468 if (++handlersCalled === 3) complete(); |
|
469 } |
|
470 |
|
471 function handleClose (code, signal) { |
|
472 assert.equal(code, -1, |
|
473 'process was never spawned, therefore exit code is -1'); |
|
474 assert.equal(signal, null, 'signal should be null'); |
|
475 if (++handlersCalled === 3) complete(); |
|
476 } |
|
477 |
|
478 function handleExit (code, signal) { |
|
479 assert.fail('Close event should not be called on init failure'); |
|
480 } |
|
481 |
|
482 function complete () { |
|
483 child.off('error', handleError); |
|
484 child.off('exit', handleExit); |
|
485 child.off('close', handleClose); |
|
486 done(); |
|
487 } |
|
488 }; |
|
489 |
|
490 exports.testSpawnOptions = function (assert, done) { |
|
491 let count = 0; |
|
492 let envStdout = ''; |
|
493 let cwdStdout = ''; |
|
494 let checkEnv, checkPwd, envChild, cwdChild; |
|
495 getScript('check-env').then(script => { |
|
496 checkEnv = script; |
|
497 return getScript('check-pwd'); |
|
498 }).then(script => { |
|
499 checkPwd = script; |
|
500 |
|
501 envChild = spawn(checkEnv, { |
|
502 env: { CHILD_PROCESS_ENV_TEST: 'my-value-test' } |
|
503 }); |
|
504 cwdChild = spawn(checkPwd, { cwd: PROFILE_DIR }); |
|
505 |
|
506 // Do these need to be unbound? |
|
507 envChild.stdout.on('data', data => envStdout += data); |
|
508 cwdChild.stdout.on('data', data => cwdStdout += data); |
|
509 |
|
510 envChild.on('close', envClose); |
|
511 cwdChild.on('close', cwdClose); |
|
512 }); |
|
513 |
|
514 function envClose () { |
|
515 assert.equal(envStdout.trim(), 'my-value-test', 'spawn correctly passed in ENV'); |
|
516 if (++count === 2) complete(); |
|
517 } |
|
518 |
|
519 function cwdClose () { |
|
520 // Check for PROFILE_DIR in the output because |
|
521 // some systems resolve symbolic links, and on OSX |
|
522 // /var -> /private/var |
|
523 let isCorrectPath = ~cwdStdout.trim().indexOf(PROFILE_DIR); |
|
524 assert.ok(isCorrectPath, 'spawn correctly passed in cwd'); |
|
525 if (++count === 2) complete(); |
|
526 } |
|
527 |
|
528 function complete () { |
|
529 envChild.off('close', envClose); |
|
530 cwdChild.off('close', cwdClose); |
|
531 done(); |
|
532 } |
|
533 }; |
|
534 |
|
535 exports.testFork = function (assert) { |
|
536 assert.throws(function () { |
|
537 fork(); |
|
538 }, /not currently supported/, 'fork() correctly throws an unsupported error'); |
|
539 }; |
|
540 |
|
541 after(exports, cleanUp); |
|
542 |
|
543 require("test").run(exports); |
|
544 |
|
545 // Test disabled because of bug 979675 |
|
546 module.exports = {}; |