如果在等待“read -s”时中断,则在子进程中运行 bash 会中断 tty 的标准输出



正如@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,则忽略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 不能更好地清理自己?

相关内容

  • 没有找到相关文章

最新更新