运行 bash 如果在等待 `read -s` 时中断,子进程中的 tty 的标准输出会中断吗?
Running bash in subprocess breaks stdout of tty if interrupted while waiting on `read -s`?
正如@Bakuriu 在评论中指出的那样,这基本上与 中的问题相同但是,我只能在 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 脚本 returns 并按预期回显您的输入,则没有问题。但是,如果您立即点击 "Ctrl-C",Python 会显示 KeyboardInterrupt
的回溯,然后 returns 会显示 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
进程,returns 到一个正常的、未损坏的终端。
这似乎与 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 在 interactive 模式下 运行ning,则此 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 中的 运行ning bash 简单地重现问题。但是当我在 strace
中 运行 或从 Python 开始的子进程时,我要么使用 -c
,要么程序的 stderr
不是终端,等等
正如@Baikuriu 在 中发现的那样,就像我在写这篇文章的过程中发布的那样,-i
将强制 bash
使用 "interactive mode",并且它会自行清理。
就我而言,我认为这是一个错误。它 是 在手册页中记录的,如果 stdin
不是 TTY,read
的 -s
选项将被忽略。但是在我的示例中 stdin
是 仍然是 TTY,但是 bash 在技术上并不处于交互模式,尽管仍然调用交互行为。在这种情况下,它仍应从 SIGINT
中正确清理。
对于它的价值,这里有一个 Python 特定(但很容易概括)的解决方法。首先,我确保将 SIGINT
(和 SIGTERM
作为衡量标准)传递给子进程。然后我将整个 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()
我仍然对解释为什么这是必要的答案感兴趣——为什么 bash 不能更好地自我清理?
正如@Bakuriu 在评论中指出的那样,这基本上与
我有一个 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 脚本 returns 并按预期回显您的输入,则没有问题。但是,如果您立即点击 "Ctrl-C",Python 会显示 KeyboardInterrupt
的回溯,然后 returns 会显示 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
进程,returns 到一个正常的、未损坏的终端。
这似乎与 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 在 interactive 模式下 运行ning,则此 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 中的 运行ning bash 简单地重现问题。但是当我在 strace
中 运行 或从 Python 开始的子进程时,我要么使用 -c
,要么程序的 stderr
不是终端,等等
正如@Baikuriu 在 -i
将强制 bash
使用 "interactive mode",并且它会自行清理。
就我而言,我认为这是一个错误。它 是 在手册页中记录的,如果 stdin
不是 TTY,read
的 -s
选项将被忽略。但是在我的示例中 stdin
是 仍然是 TTY,但是 bash 在技术上并不处于交互模式,尽管仍然调用交互行为。在这种情况下,它仍应从 SIGINT
中正确清理。
对于它的价值,这里有一个 Python 特定(但很容易概括)的解决方法。首先,我确保将 SIGINT
(和 SIGTERM
作为衡量标准)传递给子进程。然后我将整个 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()
我仍然对解释为什么这是必要的答案感兴趣——为什么 bash 不能更好地自我清理?