Search code examples
pythonlinuxbashsubprocesskeyboardinterrupt

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 -s, 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
#!/usr/bin/python
import subprocess as sp
p = sp.Popen(['bash', '-c', 'read -s foo; echo $foo'])
p.wait()

Upon running ./test.py 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 KeyboardInterrupt, and then returns to the bash prompt. However, nothing you type is displayed to the terminal. Typing reset<enter> 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
#!/bin/bash
read -s foo
echo $foo

Running 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 read.sh. This leaves the terminal in the same broken state. However, strace -I1 ./read.sh simply interrupts the ./read.sh process and returns to a normal, non-broken terminal.


Solution

  • 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 
    #!/usr/bin/python3
    from subprocess import Popen
    p = Popen(['bash', '-c', 'read -s foo; echo $foo'])
    p.wait()
    $ diff test_read.py test_read_i.py 
    3c3
    < 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>
        p.wait()
      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)
    KeyboardInterrupt
    

    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
    hi
    

    no error, and terminal works.