Capture console output when using child_process.execSync in node.js

I’m working on a nodejs project recently, and need to execute a command by using child_process.execSync().

The ideal solution should:

  • Support color output from command, which generally means the stdout/stderr should be a tty
  • Be testable, which means I can get the command output and verify

From the node.js documentation, I can use options.stdio to change stdio fds in child process.

For convenience, options.stdio may be one of the following strings:

‘pipe’ – equivalent to [‘pipe’, ‘pipe’, ‘pipe’] (the default)
‘ignore’ – equivalent to [‘ignore’, ‘ignore’, ‘ignore’]
‘inherit’ – equivalent to [process.stdin, process.stdout, process.stderr] or [0,1,2]

I started from the default behaviour – using pipe, which returns the console output after child process finishes. But it doesn’t support colours by default because piped stdout/stderr is not a tty. Although some commands support options to force coloured output, but I think it’s not an ideal solution.

Then I tried to use ‘inherit’ which supports coloured output. But I could not get the command output in parent process, the returned result always be empty because child_process.execSync is a synchronized method that will not return until the child process has fully closed.

During child execution in child_process.execSync, the current nodejs event loop is blocked and unable to process output. But we can pipe console outputs to other files and check file content later. Unfortunately, there’s no dup2 support in nodejs. Aha, wait, why do we need dup2? because it able to close the existing stdio fd and create a new one.

Let’s check the documentation again, the option ‘inherit’ equivalent to [process.stdin, process.stdout, process.stderr], so the following two method calls have the same behaviour:

execSync(hookScript, {stdio: 'inherit'});
//same with the following one
execSync(hookScript, {stdio: [process.stdin, process.stdout, process.stderr]});

Now we get the ideal solution, which is:

  • Pass current process’s stdios by default, which support coloured output
  • Pass customized fds in tests, which support output capture, so we can verify the execution results.
class Scriptable {
  constructor() {
    this.stdin = process.stdin;
    this.stdout = process.stdout;
    this.stderr = process.stderr;
  }

  runCommand(hookScript) {
    return () => {
      console.log(`Running script: ${hookScript}`);
      return execSync(hookScript, {stdio: [this.stdin, this.stdout, this.stderr]});
    }
  }
}

The reason of adding three fields for stdin/stdout/stderr is to allow me to change the value in tests. For example, the following code pipes stdout and stderr to file in child process and then verify outputs with the expected result.

const fs = require('fs');
const tmp = require('tmp');
const expect = require('chai').expect;

var scriptable = new Scriptable();
scriptable.stdout = tmp.fileSync({prefix: 'stdout-'});
scriptable.stderr = tmp.fileSync({prefix: 'stderr-'});

var randomString = `current time ${new Date().getTime()}`;
scriptable.runCommand(`echo ${randomString}`)();

expect(fs.readFileSync(scriptable.stdout.name, {encoding: 'utf-8'})).string(randomString);
expect(fs.readFileSync(scriptable.stderr.name, {encoding: 'utf-8'})).equal('');

For more details, check github code at: serverless-scriptable-plugin

Leave a Reply

Your email address will not be published. Required fields are marked *