正如@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()
尽管如此,我仍然对一个解释为什么这是必要的答案感兴趣 – 为什么不能更好地清理自己?