Python 如果在等待'read-s'时中断,则在子进程中运行bash会中断tty的标准输出?
正如@Bakuriu在评论中指出的,这基本上与中的问题相同,但是,我只能在bash作为另一个可执行文件的子进程运行时重现这个问题,而不是直接从bash运行,因为它似乎可以很好地处理终端清理。我想知道为什么bash在这方面似乎被打破了 我有一个Python脚本,用于记录由该脚本启动的子流程的输出。如果子流程恰好是一个bash脚本,该脚本在某个时刻通过调用内置的Python 如果在等待'read-s'时中断,则在子进程中运行bash会中断tty的标准输出?,python,linux,bash,subprocess,keyboardinterrupt,Python,Linux,Bash,Subprocess,Keyboardinterrupt,正如@Bakuriu在评论中指出的,这基本上与中的问题相同,但是,我只能在bash作为另一个可执行文件的子进程运行时重现这个问题,而不是直接从bash运行,因为它似乎可以很好地处理终端清理。我想知道为什么bash在这方面似乎被打破了 我有一个Python脚本,用于记录由该脚本启动的子流程的输出。如果子流程恰好是一个bash脚本,该脚本在某个时刻通过调用内置的read-s读取用户输入(防止输入字符的回声,即键),并且用户中断了脚本(即通过Ctrl-C),则bash无法将输出恢复到tty,即使它继续
read-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会显示对键盘中断的回溯,然后返回bash提示符。但是,您键入的内容不会显示在终端上。但是,键入reset
可成功重置终端
我有点不知道这里到底发生了什么
更新:我也设法在没有Python的情况下复制了它。我试着在斯特拉斯运行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 ...>
。。。
ioctl(0,SNDCTL\u TMR\u时基或SNDRV\u定时器\u ioctl\u下一个设备或TCGET,{B38400 opost isig icanon-echo…})=0
brk(0x1a93000)=0x1a93000
读取(0,进程25487)
其中PID 25487为read.sh
。这使终端处于相同的中断状态。但是,strace-I1./read.sh
只是中断/read.sh
过程,并返回到正常的、未中断的终端。这似乎与bash-c
启动一个非交互的有关trong>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'])
我获得:
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
没有错误,终端工作。正如我在对我的问题的评论中所写,当运行read-s
时,bash保存当前的tty属性,并安装add\u unwind\u protect
处理程序,以便在read
的堆栈框架退出时恢复以前的tty属性
通常,bash
在启动时为SIGINT
安装一个处理程序,除其他外,该处理程序调用堆栈的完全展开,包括运行所有unwind\u protect
处理程序,例如由read
添加的处理程序。然而,通常只有在bash运行i时才安装该SIGINT
处理程序n交互模式。根据源代码,交互模式仅在以下条件下启用:
/* 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
使用“交互模式”,并且它将在自己之后正确清理
就我而言,我认为这是一个bug。手册页中记录了,如果stdin
不是TTY,那么-s
读取的选项将被忽略。但是在我的示例中stdin
仍然是一个TTY,但是bash在技术上不是处于交互模式,尽管仍然调用交互行为。它应该仍然是在这种情况下,请正确清理SIGINT
值得一提的是,这里有一个特定于Python(但易于推广)的解决方法。首先,我确保将SIGINT
(以及SIGTERM
)传递给子流程。然后,我包装整个子流程。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()
我仍然对一个答案感兴趣,这个答案解释了为什么这是必要的——为什么bash不能更好地清理自己?在bash
源代码中进行一点挖掘就可以看出这是如何工作的。静默模式只是在tty属性中设置取消设置ECHO
标志,然后调用add\u unwind\u protect
,以恢复预处理不同的tty属性。我不知道如何add\u unwind\u protect
words,也不知道为什么在进程退出或分离之前不会调用它。您的问题是否只是重复了:?答案似乎是脚本编写者有责任恢复终端的状态。请注意,您可以编写一个中介只存储当前终端状态并使用trap
还原它并调用另一个脚本的脚本,如果您不能修改它。@Bakuriu它看起来可能是同一件事。奇怪的是,如果我只运行bash脚本,它可以很好地处理中断并还原终端状态。只有当它运行时才是这样作为一个除BASH之外的其他进程,在这个过程中失败了。你完全正确。我自己发现了这个困难的方法——通过挖掘源代码,正好在我自己回答这个问题的中间。我要完成我的答案,但是你的肯定是正确的想法!原来这是Bug(BASH)。是在v4.4RC1中修复的。bash维护人员至少同意这是一个bug,因为事实证明
/* 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. */
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()