Iguananaut Iguananaut - 3 months ago 24x
Linux Question

Running bash in subprocess breaks stdout of tty if interrupted while waiting on `read -s`?

As @Bakuriu points out in the comments this is basically the same problem as in BASH: Ctrl+C during input breaks current terminal However, I can only reproduce the problem when bash is run as a subprocess of another executable, and not directly from bash, where it seems to handle terminal cleanup fine. I would be interested in an answer as to why bash seems to be broken in this regard.

I have a Python script meant to log the output of subprocess that is started by that script. If the subprocess happens to be a bash script that at some point reads user input by calling the

read -s
built-in (the
, which prevents echoing of entered characters, being key), and the user interrupts the script (i.e. by Ctrl-C), then bash fails to restore output to the tty, even though it continues to accept input.

I whittled this down to a simple example:

$ cat test.py
import subprocess as sp
p = sp.Popen(['bash', '-c', 'read -s foo; echo $foo'])

Upon running
it will wait for some input. If you type some input and press Enter the script returns and echos your input as expected, and there is no issue. However, if you immediately hit "Ctrl-C", Python displayed a traceback for the
, and then returns to the bash prompt. However, nothing you type is displayed to the terminal. Typing
successfully resets the terminal, however.

I'm somewhat at a loss as to exactly what's happening here.

Update: I managed to reproduce this without Python in the mix either. I was trying to run bash in strace to see if I could glean anything that was going on. With the following bash script:

$ cat read.sh
read -s foo
echo $foo

strace ./read.sh
and immediately hitting Ctrl-C produces:

ioctl(0, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, {B38400 opost isig icanon -echo ...}) = 0
brk(0x1a93000) = 0x1a93000
read(0, Process 25487 detached
<detached ...>

Where PID 25487 was
. This leaves the terminal in the same broken state. However,
strace -I1 ./read.sh
simply interrupts the
process and returns to a normal, non-broken terminal.


It seems like this is related to the fact that bash -c starts a non-interactive shell. This probably prevents it from restoring the terminal state.

To explicitly start an interactive shell you can just pass the -i option to bash.

$ cat test_read.py 
from subprocess import Popen
p = Popen(['bash', '-c', 'read -s foo; echo $foo'])
$ diff test_read.py test_read_i.py 
< p = Popen(['bash', '-c', 'read -s foo; echo $foo'])
> p = Popen(['bash', '-ic', 'read -s foo; echo $foo'])

When I run and press Ctrl+C:

$ ./test_read.py

I obtain:

Traceback (most recent call last):
  File "./test_read.py", line 4, in <module>
  File "/usr/lib/python3.5/subprocess.py", line 1648, in wait
    (pid, sts) = self._try_wait(0)
  File "/usr/lib/python3.5/subprocess.py", line 1598, in _try_wait
    (pid, sts) = os.waitpid(self.pid, wait_flags)

and the terminal isn't properly restored.

If I run the test_read_i.py file in the same way I just get:

$ ./test_read_i.py 

$ echo hi

no error, and terminal works.