Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

exec() behaves differently across platforms (was "Child process should handle exit status >128 like exit status <0") #4432

Closed
isaacs opened this issue Dec 26, 2015 · 7 comments
Labels
child_process Issues and PRs related to the child_process subsystem.

Comments

@isaacs
Copy link
Contributor

isaacs commented Dec 26, 2015

On linux systems (perhaps anything with a "real" /bin/sh which is not an alias to /bin/bash?) signal exits are reported with a high exit status, rather than a negative number.

I'm not sure if this is a discrepancy that should be handled in libuv, or somewhere else, but this test shows the difference between running a child process with sh vs running with bash, and then killing the process with a SIGTERM signal.

The bash child process exits with a properly communicated signal, but the sh process has a weirdly large exit code when a negative exit code is expected (indicating a signal exit).

If this isn't something that Node can or should address, then I think that a caveat in the child_process.exec documentation is worth adding.

var cmd = process.execPath + ' ' + __filename + ' SIGTERM'
var spawn = require('child_process').spawn

if (process.argv[2]) {
  process.kill(process.pid, process.argv[2])
  // race condition: don't exit before the signal hits
  setTimeout(function () {}, 1000)
  return
} else {
  test('/bin/sh', function (shcode, shsignal, shfixed) {
    test('/bin/bash', function (bashcode, bashsignal, bashfixed) {
      console.log({
        bash: [bashcode, bashsignal, bashfixed],
        sh: [shcode, shsignal, shfixed]
      })
    })
  })
}

var constants = require('constants')
var signals = Object.keys(constants).filter(function (k) {
  return k.match(/^SIG/)
}).reduce(function (set, k) {
  set[constants[k]] = k
  return set
}, {})

function test (exe, cb) {
  var child = spawn(exe, ['-c', cmd])
  var stdout = ''
  var stderr = ''
  child.stdout.on('data', function (c) {})
  child.stderr.on('data', function (c) {})
  child.on('close', function (code, signal) {
    var fixed
    if (code > 128 && signal === null && signals[code - 128]) {
      fixed = [null, signals[code - 128]]
    } else {
      fixed = [code, signal]
    }

    cb(code, signal, fixed)
  })
}

Expected output (as seen on darwin, where sh is bash)

{ bash: [ null, 'SIGTERM', [ null, 'SIGTERM' ] ],
  sh: [ null, 'SIGTERM', [ null, 'SIGTERM' ] ] }

Actual output (on linux, where sh is sh)

{ bash: [ null, 'SIGTERM', [ null, 'SIGTERM' ] ],
  sh: [ 143, null, [ null, 'SIGTERM' ] ] }
isaacs added a commit to tapjs/foreground-child that referenced this issue Dec 26, 2015
@mscdex mscdex added the child_process Issues and PRs related to the child_process subsystem. label Dec 27, 2015
@isaacs
Copy link
Contributor Author

isaacs commented Dec 27, 2015

So, it seems like this is a bug (or lack of feature?) in /bin/sh, which perhaps Node can't/shouldn't work around.

sh -c 'kill -15 $$' will accurately report a SIGTERM exit. However, sh -c 'sh -c \'kill $$\'' will report an exit status of 143, and no signal.

So, it looks like sh -c simply does not propagate a signal exit from its child process, and only propagates the exit status code, which by convention is 128 + the signal.

I'm honestly not sure the best fix here (including the default of just shrugging and documenting it).

This is a weird source of platform incompatibility stemming from the fact that /bin/sh is a different thing on different systems. Perhaps it'd be worth considering using /bin/bash on unix instead of /bin/sh for child_process.exec(), since at least that's a little bit more consistent across the unixes Node supports?

@isaacs
Copy link
Contributor Author

isaacs commented Dec 27, 2015

More investigation: turns out that this linux system is an Ubuntu system, where /bin/sh is actually dash.

However, running this test with dash on OS X yields the same results as bash. I'm a bit lost, and tempted to just shrug and not worry about catching signal exits on cp.exec() child processes.

@isaacs
Copy link
Contributor Author

isaacs commented Dec 27, 2015

Test case showing this is a /bin/dash weirdness, and probably Node is doing the right thing:

var spawn = require('child_process').spawn
var exe = '/bin/sh'  // replace with /bin/bash or /bin/dash to test
var sh = spawn(exe, ['-c', exe + ' -c \'kill -15 $$\''])
sh.on('exit', function (code, signal) {
  console.error(code, signal)
})

@isaacs isaacs changed the title Child process should handle exit status >128 like exit status <0 exec() behaves differently across platforms (was "Child process should handle exit status >128 like exit status <0") Dec 28, 2015
@okdistribute
Copy link

@isaacs check out AndreasMadsen/execspawn#2. Would love to get this use case working. Does replacing /bin/sh with /bin/bash harm anything?

@okdistribute
Copy link

if so, could also be the culprit here.. #2098

@sam-github
Copy link
Contributor

Relying on existence of /bin/bash isn't a good idea. /bin/sh is guaranteed to exist, bash is not.

I usually check for either 128+<signo> OR a signalled error condition.

Also, there are further weirdnesses unrelated to the shell, but related to node signal emulation.

If you use process.kill() in node, libuv "remembers" the signal used to kill the child, and reports it as the cause of death... even if the child died for some other reason. This is a side-effect of the windows signal emulation layer in libuv.

And various versions of node, depending on whether stdio has been used :-(, will possibly internally catch signals, to reset the tty state, then exit with 128 + <signo>.

Its a bit of a mine field.

@Trott
Copy link
Member

Trott commented Jun 24, 2016

Feel free to comment or re-open if you think I'm mistaken, but I'm going to close this seeing as:

  • This issue has been inactive for about six months;
  • It seems like this is a suite of problems, most or all of which are not really Node.js's issue to fix, and many or all of which may not be reasonably fixable;
  • Even documenting the issue seems challenging without getting into specifics about shells that are probably beyond the scope of what our API documentation should provide

All that said, I don't feel strongly about it, so feel free to re-open. (I'm just going through old issues and closing ones that don't seem actionable based on what I understand.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
child_process Issues and PRs related to the child_process subsystem.
Projects
None yet
Development

No branches or pull requests

5 participants