防止意外的stdin读取和锁定在子进程中

时间:2015-10-22 09:17:00

标签: python linux shell pipe subprocess

我正试图解决所有情况的一个简单案例。 我正在运行一个子进程来执行某个任务,我不希望它要求stdin,但在极少数情况下,我甚至可能没想到,它可能会尝试阅读。 我想在这种情况下防止它挂起。

这是一个典型的例子:

import subprocess
p = subprocess.Popen(["unzip", "-tqq", "encrypted.zip"])
p.wait()

这将永远挂起。 我已经尝试添加

stdin=open(os.devnull)

等......

如果找到有价值的解决方案,

会发布。 对于我来说,在父进程中接收异常就足够了 - 而不是无休止地继续沟通/等待。

更新:似乎问题可能比我最初的预期更复杂,子进程(在密码和其他情况下)从其他文件描述符读取 - 比如/ dev / tty与shell交互。可能不像我想的那么容易解决..

2 个答案:

答案 0 :(得分:3)

如果你的子进程可能要求输入密码,那么如果tty可用,它可以在标准输入/输出/错误流之外进行,请参阅Q: Why not just use a pipe (popen())?

中的第一个原因

作为you've noticed,创建新会话会阻止子进程使用父进程,例如,如果您有ask-password.py脚本:

#!/usr/bin/env python
"""Ask for password. It defaults to working with a terminal directly."""
from getpass import getpass

try:
    _ = getpass()
except EOFError:
    pass # ignore
else:
    assert 0

然后将其作为子进程调用,以便它不会挂起等待密码,您可以使用start_new_session=True参数:

#!/usr/bin/env python3
import subprocess
import sys

subprocess.check_call([sys.executable, 'ask-password.py'],
                      stdin=subprocess.DEVNULL, start_new_session=True,
                      stderr=subprocess.DEVNULL)

stderr也被重定向到此处,因为getpass()将其用作后备,以打印警告和提示。

要在Python 2上的Unix上模拟start_new_session=True,您可以使用preexec_fn=os.setsid

To emulate subprocess.DEVNULL on Python 2, you could use DEVNULL=open(os.devnull, 'r+b', 0)或通过stdin=PIPE并使用.communicate()立即关闭它:

#!/usr/bin/env python2
import os
import sys
from subprocess import Popen, PIPE

Popen([sys.executable, 'ask-password.py'],
      stdin=PIPE, preexec_fn=os.setsid,
      stderr=PIPE).communicate() #NOTE: assume small output on stderr

注意:除非您使用.communicate(),否则您不需要subprocess.PIPE。如果您使用具有真实文件描述符(check_call())的对象(.fileno()返回),则open(os.devnull, ..)非常安全。重定向发生在子进程执行之前(fork()之后,exec()之前) - 没有理由在此使用.communicate()而不是check_call()

答案 1 :(得分:1)

显然,罪魁祸首是直接使用/ dev / tty等。

至少在Linux上,一个解决方案是向Popen调用以下参数:

preexec_fn=os.setsid

会导致设置新的会话ID,并且不允许直接从tty读取。我可能会使用以下代码(stdin close就是以防万一):

import subprocess
import os
p = subprocess.Popen(["unzip", "-tqq", "encrypted.zip"],
                     stdin=subprocess.PIPE, preexec_fn=os.setsid)
p.stdin.close() #just in case
p.wait()

最后两行可以被一次调用替换:

p.communicate()

因为在发送所有提供的输入后,communic()关闭了stdin文件。

看起来简单而优雅。

可替换地:

import subprocess
import os
p = subprocess.Popen(["unzip", "-tqq", "encrypted.zip"],
                     stdin=open(os.devnull), preexec_fn=os.setsid)
p.communicate()