Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/python/316.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181

Warning: file_get_contents(/data/phpspider/zhask/data//catemap/1/cocoa/3.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Python 如果在等待'read-s'时中断,则在子进程中运行bash会中断tty的标准输出?_Python_Linux_Bash_Subprocess_Keyboardinterrupt - Fatal编程技术网

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

Python 如果在等待'read-s'时中断,则在子进程中运行bash会中断tty的标准输出?,python,linux,bash,subprocess,keyboardinterrupt,Python,Linux,Bash,Subprocess,Keyboardinterrupt,正如@Bakuriu在评论中指出的,这基本上与中的问题相同,但是,我只能在bash作为另一个可执行文件的子进程运行时重现这个问题,而不是直接从bash运行,因为它似乎可以很好地处理终端清理。我想知道为什么bash在这方面似乎被打破了 我有一个Python脚本,用于记录由该脚本启动的子流程的输出。如果子流程恰好是一个bash脚本,该脚本在某个时刻通过调用内置的read-s读取用户输入(防止输入字符的回声,即键),并且用户中断了脚本(即通过Ctrl-C),则bash无法将输出恢复到tty,即使它继续

正如@Bakuriu在评论中指出的,这基本上与中的问题相同,但是,我只能在bash作为另一个可执行文件的子进程运行时重现这个问题,而不是直接从bash运行,因为它似乎可以很好地处理终端清理。我想知道为什么bash在这方面似乎被打破了

我有一个Python脚本,用于记录由该脚本启动的子流程的输出。如果子流程恰好是一个bash脚本,该脚本在某个时刻通过调用内置的
read-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会显示对
键盘中断的回溯,然后返回bash提示符。但是,您键入的内容不会显示在终端上。但是,键入
reset
可成功重置终端

我有点不知道这里到底发生了什么

更新:我也设法在没有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 ...>
。。。
ioctl(0,SNDCTL\u TMR\u时基或SNDRV\u定时器\u ioctl\u下一个设备或TCGET,{B38400 opost isig icanon-echo…})=0
brk(0x1a93000)=0x1a93000
读取(0,进程25487)

其中PID 25487为
read.sh
。这使终端处于相同的中断状态。但是,
strace-I1./read.sh
只是中断
/read.sh
过程,并返回到正常的、未中断的终端。

这似乎与
bash-c
启动一个非交互的有关trong>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'])
我获得:

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

没有错误,终端工作。

正如我在对我的问题的评论中所写,当运行
read-s
时,bash保存当前的tty属性,并安装
add\u unwind\u protect
处理程序,以便在
read
的堆栈框架退出时恢复以前的tty属性

通常,
bash
在启动时为
SIGINT
安装一个处理程序,除其他外,该处理程序调用堆栈的完全展开,包括运行所有
unwind\u protect
处理程序,例如由
read
添加的处理程序。然而,通常只有在bash运行i时才安装该
SIGINT
处理程序n交互模式。根据源代码,交互模式仅在以下条件下启用:

 /* 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
使用“交互模式”,并且它将在自己之后正确清理

就我而言,我认为这是一个bug。手册页中记录了,如果
stdin
不是TTY,那么
-s
读取
的选项将被忽略。但是在我的示例中
stdin
仍然是一个TTY,但是bash在技术上不是处于交互模式,尽管仍然调用交互行为。它应该仍然是在这种情况下,请正确清理
SIGINT

值得一提的是,这里有一个特定于Python(但易于推广)的解决方法。首先,我确保将
SIGINT
(以及
SIGTERM
)传递给子流程。然后,我包装整个
子流程。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不能更好地清理自己?

bash
源代码中进行一点挖掘就可以看出这是如何工作的。静默模式只是在tty属性中设置取消设置
ECHO
标志,然后调用
add\u unwind\u protect
,以恢复预处理不同的tty属性。我不知道如何
add\u unwind\u protect
words,也不知道为什么在进程退出或分离之前不会调用它。您的问题是否只是重复了:?答案似乎是脚本编写者有责任恢复终端的状态。请注意,您可以编写一个中介只存储当前终端状态并使用
trap
还原它并调用另一个脚本的脚本,如果您不能修改它。@Bakuriu它看起来可能是同一件事。奇怪的是,如果我只运行bash脚本,它可以很好地处理中断并还原终端状态。只有当它运行时才是这样作为一个除BASH之外的其他进程,在这个过程中失败了。你完全正确。我自己发现了这个困难的方法——通过挖掘源代码,正好在我自己回答这个问题的中间。我要完成我的答案,但是你的肯定是正确的想法!原来这是Bug(BASH)。是在v4.4RC1中修复的。bash维护人员至少同意这是一个bug,因为事实证明
 /* 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. */
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()