Why subprocess.Popen returncode differs for similar commands with bash
Asked Answered
E

1

7

Why

import subprocess

p = subprocess.Popen(["/bin/bash", "-c", "timeout -s KILL 1 sleep 5 2>/dev/null"])
p.wait()
print(p.returncode)

returns

[stderr:] /bin/bash: line 1: 963663 Killed                  timeout -s KILL 1 sleep 5 2> /dev/null
[stdout:] 137

when

import subprocess

p = subprocess.Popen(["/bin/bash", "-c", "timeout -s KILL 1 sleep 5"])
p.wait()
print(p.returncode)

returns

[stdout:] -9

If you change bash to dash, you'll get 137 in both cases. I know that -9 is KILL code and 137 is 128 + 9. But seems weird for similar code to get different returncode.

Happens on Python 2.7.12 and python 3.4.3

Looks like Popen.wait() does not call Popen._handle_exitstatus https://github.com/python/cpython/blob/3.4/Lib/subprocess.py#L1468 when using /bin/bash but I could not figure out why.

Engage answered 22/8, 2017 at 18:53 Comment(1)
Looks like timeout is Linux specific, by the way. I get a return code of 127 with both code versions on 2.7.10 and 3.6.0 on OS X.Meta
T
4

This is due to the fact how bash executes timeout with or without redirection/pipes or any other bash features:

  • With redirection

    1. python starts bash
    2. bash starts timeout, monitors the process and does pipe handling.
    3. timeout transfers itself into a new process group and starts sleep
    4. After one second, timeout sends SIGKILL into its process group
    5. As the process group died, bash returns from waiting for timeout, sees the SIGKILL and prints the message pasted above to stderr. It then sets its own exit status to 128+9 (a behaviour simulated by timeout).
  • Without redirection

    1. python starts bash.
    2. bash sees that it has nothing to do on its own and calls execve() to effectively replace itself with timeout.
    3. timeout acts as above, the whole process group dies with SIGKILL.
    4. python get's an exit status of 9 and does some mangling to turn this into -9 (SIGKILL)

In other words, without redirection/pipes/etc. bash withdraws itself from the call-chain. Your second example looks like subprocess.Popen() is executing bash, yet effectively it does not. bash is no longer there when timeout does its deed, which is why you don't get any messages and an unmangled exit status.

If you want consistent behaviour, use timeout --foreground; you'll get an exit status of 124 in both cases.

I don't know about dash; yet suppose it does not do any execve() trickery to effectively replace itself with the only program it's executing. Therefore you always see the mangled exit status of 128+9 in dash.

Update: zshshows the same behaviour, while it drops out even for simple redirections such as timeout -s KILL 1 sleep 5 >/tmp/foo and the like, giving you an exit status of -9. timeout -s KILL 1 sleep 5 && echo $? will give you status 137 in zsh also.

Tommie answered 29/8, 2017 at 21:5 Comment(1)
Sorry but I do not completly understood. If in first case timeout return 128+9 why in second case on step 4 python gets an exit status of 9. Since timeout is the one responsible for 128+9 code, shouldn't it pass this code in second case on step 4?Engage

© 2022 - 2024 — McMap. All rights reserved.