Running Bash In Subprocess Breaks Stdout Of Tty If Interrupted While Waiting On `read -s`?
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.pyI 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`?"