6

According to the docs, the child_process.exec command's callback's stdout parameter should be of a Buffer type.

http://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback

The problem is that it isn't:

var exec, child;
exec = require('child_process').exec;

child = exec('/bin/echo -n "hello"', function (error, stdout, stderr) {
    if (error) {
        console.log("error!");
        return;
    }
    console.log("isBuffer: " + Buffer.isBuffer(stdout));
    console.log("type: " + typeof stdout);
});

This prints

isBuffer: false
type: string

Why? Is this a documentation bug?

This is a major problem because if there is binary data in stdout, I get the incorrect data out. Putting {encoding: 'binary'} into options did not help.

3
  • What Node.js version are you running? Commented Sep 20, 2013 at 20:38
  • @Brad The latest, 0.10.18. Commented Sep 20, 2013 at 20:50
  • 1
    this will be fixed in 0.12 Commented Sep 21, 2013 at 8:58

1 Answer 1

4

The node.js source code shows that, as of 0.10.18, stdout is always a string. If you want to get binary data, you have to use spawn.

However, it seems like node.js 0.12 will introduce an option to get binary data if you pass {encoding: 'buffer'}:

(https://github.com/joyent/node/blob/master/lib/child_process.js#L617):

if (options.encoding !== 'buffer' && Buffer.isEncoding(options.encoding)) {

EDIT: code example was taken from master and is not in stable version yet.

EDIT 2: Here's a backported version of the version of exec which will accept the buffer encoding:

var util = require('util'),
    spawn = require('child_process').spawn;

var exec = function(command /*, options, callback */) {
  var file, args, options, callback;

  if (typeof arguments[1] === 'function') {
    options = undefined;
    callback = arguments[1];
  } else {
    options = arguments[1];
    callback = arguments[2];
  }

  if (process.platform === 'win32') {
    file = 'cmd.exe';
    args = ['/s', '/c', '"' + command + '"'];
    // Make a shallow copy before patching so we don't clobber the user's
    // options object.
    options = util._extend({}, options);
    options.windowsVerbatimArguments = true;
  } else {
    file = '/bin/sh';
    args = ['-c', command];
  }

  if (options && options.shell)
    file = options.shell;

  return execFile(file, args, options, callback);
};


var execFile = function(file /* args, options, callback */) {
  var args, callback;
  var options = {
    encoding: 'utf8',
    timeout: 0,
    maxBuffer: 200 * 1024,
    killSignal: 'SIGTERM',
    cwd: null,
    env: null
  };

  // Parse the parameters.

  if (typeof(arguments[arguments.length - 1]) === 'function') {
    callback = arguments[arguments.length - 1];
  }

  if (util.isArray(arguments[1])) {
    args = arguments[1];
    options = util._extend(options, arguments[2]);
  } else {
    args = [];
    options = util._extend(options, arguments[1]);
  }

  var child = spawn(file, args, {
    cwd: options.cwd,
    env: options.env,
    windowsVerbatimArguments: !!options.windowsVerbatimArguments
  });

  var encoding;
  var _stdout;
  var _stderr;
  if (options.encoding !== 'buffer' && Buffer.isEncoding(options.encoding)) {
    encoding = options.encoding;
    _stdout = '';
    _stderr = '';
  } else {
    _stdout = [];
    _stderr = [];
    encoding = null;
  }
  var stdoutLen = 0;
  var stderrLen = 0;
  var killed = false;
  var exited = false;
  var timeoutId;

  var ex;

  function exithandler(code, signal) {
    if (exited) return;
    exited = true;

    if (timeoutId) {
      clearTimeout(timeoutId);
      timeoutId = null;
    }

    if (!callback) return;

    // merge chunks
    var stdout;
    var stderr;
    if (!encoding) {
      stdout = Buffer.concat(_stdout);
      stderr = Buffer.concat(_stderr);
    } else {
      stdout = _stdout;
      stderr = _stderr;
    }

    if (ex) {
      callback(ex, stdout, stderr);
    } else if (code === 0 && signal === null) {
      callback(null, stdout, stderr);
    } else {
      ex = new Error('Command failed: ' + stderr);
      ex.killed = child.killed || killed;
      ex.code = code < 0 ? uv.errname(code) : code;
      ex.signal = signal;
      callback(ex, stdout, stderr);
    }
  }

  function errorhandler(e) {
    ex = e;
    child.stdout.destroy();
    child.stderr.destroy();
    exithandler();
  }

  function kill() {
    child.stdout.destroy();
    child.stderr.destroy();

    killed = true;
    try {
      child.kill(options.killSignal);
    } catch (e) {
      ex = e;
      exithandler();
    }
  }

  if (options.timeout > 0) {
    timeoutId = setTimeout(function() {
      kill();
      timeoutId = null;
    }, options.timeout);
  }

  child.stdout.addListener('data', function(chunk) {
    stdoutLen += chunk.length;

    if (stdoutLen > options.maxBuffer) {
      ex = new Error('stdout maxBuffer exceeded.');
      kill();
    } else {
      if (!encoding)
        _stdout.push(chunk);
      else
        _stdout += chunk;
    }
  });

  child.stderr.addListener('data', function(chunk) {
    stderrLen += chunk.length;

    if (stderrLen > options.maxBuffer) {
      ex = new Error('stderr maxBuffer exceeded.');
      kill();
    } else {
      if (!encoding)
        _stderr.push(chunk);
      else
        _stderr += chunk;
    }
  });

  if (encoding) {
    child.stderr.setEncoding(encoding);
    child.stdout.setEncoding(encoding);
  }

  child.addListener('close', exithandler);
  child.addListener('error', errorhandler);

  return child;
};
Sign up to request clarification or add additional context in comments.

4 Comments

As I mentioned in my question, {encoding: 'binary'} has no effect. But your {encoding: 'buffer'} does not work at all, an exception is thrown at string_decoder.js:24: Error: Unknown encoding: buffer
you're right, this code hasn't been released yet. I've updated my answer.
Can you post spawn code that does the same thing as my exec code except that it gets a buffer? I'm not sure how to do this, the docs for spawn don't mention buffers anywhere.
Thanks! {encoding: 'buffer'} works perfect in node v6.2.1.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.