防止意外的标准输入读取并锁定子进程

prevent unexpected stdin reads and lock in subprocess

一个我试图解决所有情况的简单案例。 我是 运行 执行某项任务的子进程,我不希望它请求标准输入,但在极少数情况下,我可能甚至没有想到,它可能会尝试读取。 我想防止它在那种情况下挂起。

这是一个经典的例子:

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

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

stdin=open(os.devnull)

等等..

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

会 post。 足以让我在父进程中收到异常 - 而不是无休止地挂在 communicate/wait 上。

更新:看来问题可能比我最初预期的还要复杂,子进程(在密码和其他情况下)从其他文件描述符读取 - 比如 /dev/tty 与 [=27 交互=].可能没有我想的那么容易解决..

显然罪魁祸首是直接使用 /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()

因为 communicate() 在发送所有提供的输入后关闭标准输入文件。

看起来简单大方。

或者:

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

如果您的子进程可能要求输入密码,那么如果 tty 可用,它可能会在标准 input/output/error 流之外执行此操作,请参阅 Q: Why not just use a pipe (popen())?

中的第一个原因

一样,创建新会话会阻止子进程使用父进程的 tty,例如,如果您有 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

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