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.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 stdin
is 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`?"