child_process.spawn() hangs when child process prompts for input

Refresh

April 2019

Views

47 time

1

I've written a node script to manage deployment of a git repository to a AWS autoscaling group.

The script uses child_process.spawn() to automate git, to clone repositories, checkout tags etc.

It works fine if git can find appropriate credentials. However if credentials aren't automatically found, then the spawned process will attempt to prompt for credentials, and at that point will hang. Even Ctrl-C cannot exit. The whole shell session must be ended.

The spawn() call is wrapped in a function to return a Promise. My function looks like so...

const cp = require('child_process');

let spawn_promise = (command, args, options, stream_output) => {
    return new Promise((resolve, reject) => {
        console.log(chalk.cyan(`${command} [${args}]`));

        let childProcess = cp.spawn(command, args, options);
        let std_out = '';
        let std_err = '';

        childProcess.stdout.on('data', function (data) {
            std_out += data.toString();
            if (stream_output)
                console.log(chalk.green(data.toString()));
        });
        childProcess.stderr.on('data', function (data) {
            std_err += data.toString();
            if (stream_output)
                console.log(chalk.red(data.toString()));
        });

        childProcess.on('close', (code) => {
            if (code === 0) {
                console.log(chalk.blue(`exit_code = ${code}`));
                return resolve(std_out);
            }
            else {
                console.log(chalk.yellow(`exit_code = ${code}`));
                return reject(std_err);
            }
        });

        childProcess.on('error', (error) => {
            std_err += error.toString();
            if (stream_output)
                console.log(chalk.red(error.toString()));
        });
    });
}

I call it like so...

return spawn_promise('git', ['fetch', '--all'], {env: process.env})
   .then(() => {
      ...

It mostly works very well, and allows easily manipulation of output and errors etc.

I'm having trouble figuring out a nice way to to handle input though, if a spawned process needs it.

A temporary work-around for the problem is to add an environment variable to prevent git from prompting for credentials, and instead to throw an error if it can't find credentials in the users environment. However this isn't ideal. Ideally I would like to be able to gracefully handle standard input, and still be able to capture and process the output and errors as I'm currently doing.

I can fix the problem with input by doing this...

let childProcess = cp.spawn(command, args, { stdio: [process.stdin, process.stdout, process.stderr] });

This allows git to prompt for credentials correctly. However I then lose the ability to capture the command output.

What is the correct way to be able to handle this?

I should also mention, that the function also automates some relatively long running processes, to build AMI's etc. This is what the "stream_output" parameter is for. I want to be able to view the output from the command in real-time, rather than waiting until the process completes.

1 answers

1

The child_process has stdin to handle the input and same can be used to enter the input when the child_process is running.

See below an example:

test.sh:

#!/bin/sh

echo "Please enter something:"
read ch
echo "Thanks"

When I run on this terminal:

shell-input $ ./test.sh 
Please enter something:
something
Thanks
shell-input $ 

When I use your code to run this: test.js:

const cp = require('child_process');
const chalk = require('chalk');

let spawn_promise = (command, args, options, stream_output) => {
    return new Promise((resolve, reject) => {
        console.log(chalk.cyan(`${command} [${args}]`));

        let childProcess = cp.spawn(command, args, options);
        let std_out = '';
        let std_err = '';

        childProcess.stdout.on('data', function (data) {
            std_out += data.toString();
            if (stream_output)
                console.log(chalk.green(data.toString()));
        });
        childProcess.stderr.on('data', function (data) {
            std_err += data.toString();
            if (stream_output)
                console.log(chalk.red(data.toString()));
        });

        childProcess.on('close', (code) => {
            if (code === 0) {
                console.log(chalk.blue(`exit_code = ${code}`));
                return resolve(std_out);
            }
            else {
                console.log(chalk.yellow(`exit_code = ${code}`));
                return reject(std_err);
            }
        });

        childProcess.on('error', (error) => {
            std_err += error.toString();
            if (stream_output)
                console.log(chalk.red(error.toString()));
        });
    });
}

spawn_promise('./test.sh',  { env: process.env})
   .then(() => {
   });

Output:

 $ node test.js 
./test.sh [[object Object]]

<stuck here>

I modify your code to include the following:

...
        childProcess.stdout.on('data', function (data) {


            if (data == "Please enter something:\n")
            {
                childProcess.stdin.write("something\n");
                //childProcess.stdin.end(); // Call this to end the session
            }

            std_out += data.toString();
            if (stream_output)
                console.log(chalk.green(data.toString()));
        });
  ...

Then I run again:

$ node test.js 
./test.sh [[object Object]]
exit_code = 0

It works. Basically you need to find out when stdin is waiting for input. You can use data event on stdout for that and then write on stdin. If you don't have credentials to write, you can end the session by calling childProcess.stdin.end();