在subprocess中运行bash,如果在等待`read -s`时中断了tty的stdout?

正如@Bakuriu在注释中指出的,这与BASH中的问题基本相同:在input中断Ctrl + C时终止当前terminal然而,当bash作为另一个可执行文件的subprocess运行时,我只能重现问题,而不是直接来自bash ,它似乎处理terminal清理罚款。 我会感兴趣的是为什么bash似乎在这方面被打破。

我有一个Python脚本,旨在logging由该脚本启动的subprocess的输出。 如果subprocess恰好是一个bash脚本,在某些时候通过调用read -s内build-s-s ,防止input字符的回显,作为键)读取用户input,并且用户中断脚本Ctrl-C),然后bash无法将输出恢复到tty,即使它继续接受input。

我把这个简化为一个简单的例子:

 $ cat test.py #!/usr/bin/python import subprocess as sp p = sp.Popen(['bash', '-c', 'read -s foo; echo $foo']) p.wait() 

在运行./test.py它会等待一些input。 如果您键入一些input,然后按Enter键,则脚本会按照预期返回并回显您的input,而不存在任何问题。 但是,如果您立即点击“Ctrl-C”,Python会显示KeyboardInterrupt的回溯,然后返回到bash提示符。 但是,您键入的任何内容都不会显示在terminal上。 键入reset<enter>成功重置terminal,但是。

我对这里发生的事情感到有些不知所措。

更新:我设法在没有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 ...> 

其中PID 25487是read.sh 这使terminal处于相同的中断状态。 但是, strace -I1 ./read.sh只是简单地中断./read.sh过程并返回到一个正常的,没有中断的terminal。

看起来这与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添加的处理程序)。 但是,这个SIGINT处理程序通常在bash以交互模式运行的情况下安装。 根据源代码,只有在以下情况下才能启用交互模式:

  /* 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选项。 但在我的例子中, 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() 

尽管如此,我仍然对一个解释为什么这是必要的答案感兴趣 – 为什么不能更好地清理自己?