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

changeset 0
6474c204b198
     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 +}

mercurial