1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/addon-sdk/source/lib/sdk/system/child_process.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,329 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 +'use strict'; 1.8 + 1.9 +module.metadata = { 1.10 + 'stability': 'experimental' 1.11 +}; 1.12 + 1.13 +let { Ci } = require('chrome'); 1.14 +let subprocess = require('./child_process/subprocess'); 1.15 +let { EventTarget } = require('../event/target'); 1.16 +let { Stream } = require('../io/stream'); 1.17 +let { on, emit, off } = require('../event/core'); 1.18 +let { Class } = require('../core/heritage'); 1.19 +let { platform } = require('../system'); 1.20 +let { isFunction, isArray } = require('../lang/type'); 1.21 +let { delay } = require('../lang/functional'); 1.22 +let { merge } = require('../util/object'); 1.23 +let { setTimeout, clearTimeout } = require('../timers'); 1.24 +let isWindows = platform.indexOf('win') === 0; 1.25 + 1.26 +let processes = WeakMap(); 1.27 + 1.28 + 1.29 +/** 1.30 + * The `Child` class wraps a subprocess command, exposes 1.31 + * the stdio streams, and methods to manipulate the subprocess 1.32 + */ 1.33 +let Child = Class({ 1.34 + implements: [EventTarget], 1.35 + initialize: function initialize (options) { 1.36 + let child = this; 1.37 + let proc; 1.38 + 1.39 + this.killed = false; 1.40 + this.exitCode = undefined; 1.41 + this.signalCode = undefined; 1.42 + 1.43 + this.stdin = Stream(); 1.44 + this.stdout = Stream(); 1.45 + this.stderr = Stream(); 1.46 + 1.47 + try { 1.48 + proc = subprocess.call({ 1.49 + command: options.file, 1.50 + arguments: options.cmdArgs, 1.51 + environment: serializeEnv(options.env), 1.52 + workdir: options.cwd, 1.53 + charset: options.encoding, 1.54 + stdout: data => emit(child.stdout, 'data', data), 1.55 + stderr: data => emit(child.stderr, 'data', data), 1.56 + stdin: stream => { 1.57 + child.stdin.on('data', pumpStdin); 1.58 + child.stdin.on('end', function closeStdin () { 1.59 + child.stdin.off('data', pumpStdin); 1.60 + child.stdin.off('end', closeStdin); 1.61 + stream.close(); 1.62 + }); 1.63 + function pumpStdin (data) { 1.64 + stream.write(data); 1.65 + } 1.66 + }, 1.67 + done: function (result) { 1.68 + // Only emit if child is not killed; otherwise, 1.69 + // the `kill` method will handle this 1.70 + if (!child.killed) { 1.71 + child.exitCode = result.exitCode; 1.72 + child.signalCode = null; 1.73 + 1.74 + // If process exits with < 0, there was an error 1.75 + if (child.exitCode < 0) { 1.76 + handleError(new Error('Process exited with exit code ' + child.exitCode)); 1.77 + } 1.78 + else { 1.79 + // Also do 'exit' event as there's not much of 1.80 + // a difference in our implementation as we're not using 1.81 + // node streams 1.82 + emit(child, 'exit', child.exitCode, child.signalCode); 1.83 + } 1.84 + 1.85 + // Emit 'close' event with exit code and signal, 1.86 + // which is `null`, as it was not a killed process 1.87 + emit(child, 'close', child.exitCode, child.signalCode); 1.88 + } 1.89 + } 1.90 + }); 1.91 + processes.set(child, proc); 1.92 + } catch (e) { 1.93 + // Delay the error handling so an error handler can be set 1.94 + // during the same tick that the Child was created 1.95 + delay(() => handleError(e)); 1.96 + } 1.97 + 1.98 + // `handleError` is called when process could not even 1.99 + // be spawned 1.100 + function handleError (e) { 1.101 + // If error is an nsIObject, make a fresh error object 1.102 + // so we're not exposing nsIObjects, and we can modify it 1.103 + // with additional process information, like node 1.104 + let error = e; 1.105 + if (e instanceof Ci.nsISupports) { 1.106 + error = new Error(e.message, e.filename, e.lineNumber); 1.107 + } 1.108 + emit(child, 'error', error); 1.109 + child.exitCode = -1; 1.110 + child.signalCode = null; 1.111 + emit(child, 'close', child.exitCode, child.signalCode); 1.112 + } 1.113 + }, 1.114 + kill: function kill (signal) { 1.115 + let proc = processes.get(this); 1.116 + proc.kill(signal); 1.117 + this.killed = true; 1.118 + this.exitCode = null; 1.119 + this.signalCode = signal; 1.120 + emit(this, 'exit', this.exitCode, this.signalCode); 1.121 + emit(this, 'close', this.exitCode, this.signalCode); 1.122 + }, 1.123 + get pid() { return processes.get(this, {}).pid || -1; } 1.124 +}); 1.125 + 1.126 +function spawn (file, ...args) { 1.127 + let cmdArgs = []; 1.128 + // Default options 1.129 + let options = { 1.130 + cwd: null, 1.131 + env: null, 1.132 + encoding: 'UTF-8' 1.133 + }; 1.134 + 1.135 + if (args[1]) { 1.136 + merge(options, args[1]); 1.137 + cmdArgs = args[0]; 1.138 + } 1.139 + else { 1.140 + if (isArray(args[0])) 1.141 + cmdArgs = args[0]; 1.142 + else 1.143 + merge(options, args[0]); 1.144 + } 1.145 + 1.146 + if ('gid' in options) 1.147 + console.warn('`gid` option is not yet supported for `child_process`'); 1.148 + if ('uid' in options) 1.149 + console.warn('`uid` option is not yet supported for `child_process`'); 1.150 + if ('detached' in options) 1.151 + console.warn('`detached` option is not yet supported for `child_process`'); 1.152 + 1.153 + options.file = file; 1.154 + options.cmdArgs = cmdArgs; 1.155 + 1.156 + return Child(options); 1.157 +} 1.158 + 1.159 +exports.spawn = spawn; 1.160 + 1.161 +/** 1.162 + * exec(command, options, callback) 1.163 + */ 1.164 +function exec (cmd, ...args) { 1.165 + let file, cmdArgs, callback, options = {}; 1.166 + 1.167 + if (isFunction(args[0])) 1.168 + callback = args[0]; 1.169 + else { 1.170 + merge(options, args[0]); 1.171 + callback = args[1]; 1.172 + } 1.173 + 1.174 + if (isWindows) { 1.175 + file = 'C:\\Windows\\System32\\cmd.exe'; 1.176 + cmdArgs = ['/s', '/c', (cmd || '').split(' ')]; 1.177 + } 1.178 + else { 1.179 + file = '/bin/sh'; 1.180 + cmdArgs = ['-c', cmd]; 1.181 + } 1.182 + 1.183 + // Undocumented option from node being able to specify shell 1.184 + if (options && options.shell) 1.185 + file = options.shell; 1.186 + 1.187 + return execFile(file, cmdArgs, options, callback); 1.188 +} 1.189 +exports.exec = exec; 1.190 +/** 1.191 + * execFile (file, args, options, callback) 1.192 + */ 1.193 +function execFile (file, ...args) { 1.194 + let cmdArgs = [], callback; 1.195 + // Default options 1.196 + let options = { 1.197 + cwd: null, 1.198 + env: null, 1.199 + encoding: 'utf8', 1.200 + timeout: 0, 1.201 + maxBuffer: 200 * 1024, 1.202 + killSignal: 'SIGTERM' 1.203 + }; 1.204 + 1.205 + if (isFunction(args[args.length - 1])) 1.206 + callback = args[args.length - 1]; 1.207 + 1.208 + if (isArray(args[0])) { 1.209 + cmdArgs = args[0]; 1.210 + merge(options, args[1]); 1.211 + } else if (!isFunction(args[0])) 1.212 + merge(options, args[0]); 1.213 + 1.214 + let child = spawn(file, cmdArgs, options); 1.215 + let exited = false; 1.216 + let stdout = ''; 1.217 + let stderr = ''; 1.218 + let error = null; 1.219 + let timeoutId = null; 1.220 + 1.221 + child.stdout.setEncoding(options.encoding); 1.222 + child.stderr.setEncoding(options.encoding); 1.223 + 1.224 + on(child.stdout, 'data', pumpStdout); 1.225 + on(child.stderr, 'data', pumpStderr); 1.226 + on(child, 'close', exitHandler); 1.227 + on(child, 'error', errorHandler); 1.228 + 1.229 + if (options.timeout > 0) { 1.230 + setTimeout(() => { 1.231 + kill(); 1.232 + timeoutId = null; 1.233 + }, options.timeout); 1.234 + } 1.235 + 1.236 + function exitHandler (code, signal) { 1.237 + 1.238 + // Return if exitHandler called previously, occurs 1.239 + // when multiple maxBuffer errors thrown and attempt to kill multiple 1.240 + // times 1.241 + if (exited) return; 1.242 + exited = true; 1.243 + 1.244 + if (!isFunction(callback)) return; 1.245 + 1.246 + if (timeoutId) { 1.247 + clearTimeout(timeoutId); 1.248 + timeoutId = null; 1.249 + } 1.250 + 1.251 + if (!error && (code !== 0 || signal !== null)) 1.252 + error = createProcessError(new Error('Command failed: ' + stderr), { 1.253 + code: code, 1.254 + signal: signal, 1.255 + killed: !!child.killed 1.256 + }); 1.257 + 1.258 + callback(error, stdout, stderr); 1.259 + 1.260 + off(child.stdout, 'data', pumpStdout); 1.261 + off(child.stderr, 'data', pumpStderr); 1.262 + off(child, 'close', exitHandler); 1.263 + off(child, 'error', errorHandler); 1.264 + } 1.265 + 1.266 + function errorHandler (e) { 1.267 + error = e; 1.268 + exitHandler(); 1.269 + } 1.270 + 1.271 + function kill () { 1.272 + try { 1.273 + child.kill(options.killSignal); 1.274 + } catch (e) { 1.275 + // In the scenario where the kill signal happens when 1.276 + // the process is already closing, just abort the kill fail 1.277 + if (/library is not open/.test(e)) 1.278 + return; 1.279 + error = e; 1.280 + exitHandler(-1, options.killSignal); 1.281 + } 1.282 + } 1.283 + 1.284 + function pumpStdout (data) { 1.285 + stdout += data; 1.286 + if (stdout.length > options.maxBuffer) { 1.287 + error = new Error('stdout maxBuffer exceeded'); 1.288 + kill(); 1.289 + } 1.290 + } 1.291 + 1.292 + function pumpStderr (data) { 1.293 + stderr += data; 1.294 + if (stderr.length > options.maxBuffer) { 1.295 + error = new Error('stderr maxBuffer exceeded'); 1.296 + kill(); 1.297 + } 1.298 + } 1.299 + 1.300 + return child; 1.301 +} 1.302 +exports.execFile = execFile; 1.303 + 1.304 +exports.fork = function fork () { 1.305 + throw new Error("child_process#fork is not currently supported"); 1.306 +}; 1.307 + 1.308 +function serializeEnv (obj) { 1.309 + return Object.keys(obj || {}).map(prop => prop + '=' + obj[prop]); 1.310 +} 1.311 + 1.312 +function createProcessError (err, options = {}) { 1.313 + // If code and signal look OK, this was probably a failure 1.314 + // attempting to spawn the process (like ENOENT in node) -- use 1.315 + // the code from the error message 1.316 + if (!options.code && !options.signal) { 1.317 + let match = err.message.match(/(NS_ERROR_\w*)/); 1.318 + if (match && match.length > 1) 1.319 + err.code = match[1]; 1.320 + else { 1.321 + // If no good error message found, use the passed in exit code; 1.322 + // this occurs when killing a process that's already closing, 1.323 + // where we want both a valid exit code (0) and the error 1.324 + err.code = options.code != null ? options.code : null; 1.325 + } 1.326 + } 1.327 + else 1.328 + err.code = options.code != null ? options.code : null; 1.329 + err.signal = options.signal || null; 1.330 + err.killed = options.killed || false; 1.331 + return err; 1.332 +}