3

正如@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过程并返回到正常的、未损坏的终端。

4

2 回答 2

2

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

没有错误,终端工作。

于 2016-07-07T13:54:10.670 回答
1

正如我在对我的问题的评论中所写的那样,当read -s运行时,bash 保存当前的 tty 属性,并安装一个处理程序以在退出add_unwind_protect堆栈帧时恢复以前的 tty 属性。read

通常,在启动时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使用“交互模式”,并且它会自行清理。

就我而言,我认为这是一个错误。手册页中记录了如果不是TTY stdin,则忽略-s选项 to 。read但在我的示例stdin 仍然是 TTY,但 bash 在技术上并不处于交互模式,尽管仍然调用交互行为。SIGINT在这种情况下,它仍应从 a 中正确清理。

对于它的价值,这是一个特定于 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 不能更好地清理自己?

于 2016-07-07T14:03:16.300 回答