[英]Running bash in subprocess breaks stdout of tty if interrupted while waiting on `read -s`?
正如@Bakuriu在评论中指出的那样,这与BASH中的问题基本相同:输入时Ctrl + C打破了当前终端但是,我只能在bash作为另一个可执行文件的子进程运行时重现问题,而不是直接来自bash ,它似乎处理终端清理罚款。 关于为什么bash在这方面似乎被打破了,我会感兴趣。
我有一个Python脚本,用于记录由该脚本启动的子进程的输出。 如果子进程碰巧是一个bash脚本,在某些时候通过调用内置的read -s
读取用户输入( -s
,它可以防止回输输入的字符,成为键),并且用户中断脚本(即Ctrl-C),然后bash无法将输出恢复到tty,即使它继续接受输入。
我把它简化为一个简单的例子:
$ cat test.py
#!/usr/bin/python
import subprocess as sp
p = sp.Popen(['bash', '-c', 'read -s foo; echo $foo'])
p.wait()
运行./test.py
,它将等待一些输入。 如果您键入一些输入并按Enter键,脚本将返回并按预期回显您的输入,并且没有问题。 但是,如果您立即点击“Ctrl-C”,Python会显示KeyboardInterrupt
的回溯,然后返回到bash提示符。 但是,您输入的任何内容都不会显示在终端上。 但是,键入reset<enter>
成功重置终端。
我有点不知道到底发生了什么。
更新:我设法在没有Python的情况下重现这一点。 我试图在strace中运行bash,看看我是否可以收集任何正在发生的事情。 使用以下bash脚本:
$ cat read.sh
#!/bin/bash
read -s foo
echo $foo
运行strace ./read.sh
并立即按Ctrl-C会产生:
...
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 ...>
其中PID 25487被read.sh
。 这使终端处于相同的破坏状态。 但是, strace -I1 ./read.sh
只是中断./read.sh
进程并返回正常的非破坏终端。
看起来这与bash -c
启动非交互式 shell的事实有关。 这可能会阻止它恢复终端状态。
要显式启动交互式shell,您只需将-i
选项传递给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'])
当我运行并按Ctrl + C时 :
$ ./test_read.py
我获得:
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
并且终端未正确恢复。
如果我以与我得到的相同的方式运行test_read_i.py
文件:
$ ./test_read_i.py
$ echo hi
hi
没有错误,终端工作。
正如我在对我的问题的评论中所写,当运行read -s
时,bash保存当前的tty属性,并安装add_unwind_protect
处理程序,以便在read
的堆栈帧退出时恢复先前的tty属性。
通常, bash
在启动时为SIGINT
安装一个处理程序,除其他外,它调用堆栈的完全展开,包括运行所有unwind_protect
处理程序,例如read
添加的处理程序。 但是,通常仅在bash以交互模式运行时才安装此SIGINT
处理程序。 根据源代码,仅在以下条件下启用交互模式:
/* 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 error is a terminal
Refer to Posix.2, the description of the `sh' utility. */
我认为这也应该解释为什么我不能简单地通过在bash中运行bash来重现问题。 但是当我在strace
运行它,或者从Python启动子进程时,我要么使用-c
,要么程序的stderr
不是终端等。
正如@Baikuriu在他们的回答中发现的那样,就像我在编写这个过程中一样, -i
将强制bash
使用“交互模式”,并且它将在自身之后正确清理。
就我而言,我认为这是一个错误。 它在手册页,如果证明stdin
不是一个TTY,则-s
来选择read
被忽略。 但是,在我的例子stdin
仍然是一个TTY,但庆典是不是在交互模式,否则技术上,尽管仍然调用交互行为。 在这种情况下,它仍应从SIGINT
中正确清理。
对于它的价值,这里是一个特定于Python(但很容易概括)的解决方法。 首先,我确保将SIGINT
(和SIGTERM
for good measure)传递给子进程。 然后我在一个小的上下文管理器中包装整个subprocess.Popen
调用终端设置:
import contextlib
import os
import signal
import subprocess as sp
import sys
import termios
@contextlib.contextmanager
def restore_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.contextmanager
def send_signals(proc, *sigs):
def handle_signal(signum, frame):
try:
proc.send_signal(signum)
except OSError:
# process has already exited, most likely
pass
prev_handlers = []
for sig in sigs:
prev_handlers.append(signal.signal(sig, handle_signal))
yield
for sig, handler in zip(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()
我仍然感兴趣的答案解释了为什么这是必要的 - 为什么不能更好地清理自己呢?
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.