Skip to content Skip to sidebar Skip to footer

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 ba

Solution 1:

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/python3from 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.

Solution 2:

As I wrote in a comment on my question, when read -s is run, bash saves the current tty attributes, and installs an add_unwind_protect handler to restore the previous tty attributes when the stack frame for read exits.

Normally, bash installs a handler for SIGINT at its startup which, among other things, invokes a full unwinding of the stack, including running all unwind_protect handlers, such as the one added by read. However, this SIGINT handler is normally only installed if bash is running in interactive mode. According to the source code, interactive mode is enabled only in the following conditions:

 /* First, let the outside world know about our interactive status.
     A shell is interactive if the `-i' flag was given, or if all of
     the following conditions are met:
    no -c command
    no arguments remaining or the -s flag given
    standard input is a terminal
    standard erroris a terminal
     Refer to Posix.2, the description of the `sh' utility. */

I think this should also explain why I couldn't reproduce the problem simply by running bash from within bash. But when I run it in strace, or a subprocess started from Python, I was either using -c, or the program's stderr is not a terminal, etc.

As @Baikuriu found in their answer, posted just as I was in the process of writing this, -i will force bash to use "interactive mode", and it will clean up properly after itself.

For my part, I think this is a bug. It is documented in the man page that if stdin is not a TTY, the -s option to read is ignored. But in my example stdinis still a TTY, but bash is not otherwise technically in interactive mode, despite still invoking interactive behavior. It should still clean up properly from a SIGINT in this case.

For what it's worth, here's a Python-specific (but easily generalizeable) workaround. First I make sure that SIGINT (and SIGTERM for good measure) are passed to the subprocess. Then I wrap the whole subprocess.Popen call in a little context manager for the terminal settings:

import contextlib
import os
import signal
import subprocess as sp
import sys
import termios

@contextlib.contextmanagerdefrestore_tty(fd=sys.stdin.fileno()):
    if os.isatty(fd):
        save_tty_attr = termios.tcgetattr(fd)
        yield
        termios.tcsetattr(fd, termios.TCSAFLUSH, save_tty_attr)
    else:
        yield@contextlib.contextmanagerdefsend_signals(proc, *sigs):
    defhandle_signal(signum, frame):
        try:
            proc.send_signal(signum)
        except OSError:
            # process has already exited, most likelypass

    prev_handlers = []

    for sig in sigs:
        prev_handlers.append(signal.signal(sig, handle_signal))

    yieldfor sig, handler inzip(sigs, prev_handlers):
        signal.signal(sig, handler)


with restore_tty():
    p = sp.Popen(['bash', '-c', 'read -s test; echo $test'])
    with send_signals(p, signal.SIGINT, signal.SIGTERM):
        p.wait()

I'd still be interested in an answer that explains why this is necessary at all though--why can't bash clean itself up better?

Post a Comment for "Running Bash In Subprocess Breaks Stdout Of Tty If Interrupted While Waiting On `read -s`?"