Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why subprocess.Popen returncode differs for similar commands with bash

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.

like image 745
Lehych Avatar asked Aug 22 '17 18:08

Lehych


People also ask

What is the difference between subprocess run and Popen?

The main difference is that subprocess. run() executes a command and waits for it to finish, while with subprocess. Popen you can continue doing your stuff while the process finishes and then just repeatedly call Popen. communicate() yourself to pass and receive data to your process.

How does subprocess Popen work?

Popen FunctionThe function should return a pointer to a stream that may be used to read from or write to the pipe while also creating a pipe between the calling application and the executed command. Immediately after starting, the Popen function returns data, and it does not wait for the subprocess to finish.

Does subprocess Popen block?

Popen is nonblocking. call and check_call are blocking. You can make the Popen instance block by calling its wait or communicate method.

Do you need to close Popen?

Popen do we need to close the connection or subprocess automatically closes the connection? Usually, the examples in the official documentation are complete. There the connection is not closed. So you do not need to close most probably.


1 Answers

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.

like image 76
user2722968 Avatar answered Sep 22 '22 03:09

user2722968