【问题标题】:prevent unexpected stdin reads and lock in subprocess防止意外的标准输入读取并锁定子进程
【发布时间】:2016-01-21 12:30:13
【问题描述】:

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

这是一个经典的例子:

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

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

stdin=open(os.devnull)

等等……

如果我找到有价值的解决方案,我会发布。 足以让我在父进程中收到异常 - 而不是无休止地等待通信/等待。

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

【问题讨论】:

    标签: python linux shell pipe subprocess


    【解决方案1】:

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

    作为you've noticed,创建新会话会阻止子进程使用父进程的 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
    

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

    【讨论】:

      【解决方案2】:

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

      【讨论】:

      • 不相关:您可以在 Python 3 上使用subprocess.DEVNULL。我更喜欢os.setsid 解决方案(如果可行的话),但您也可以使用pexpect or pty.openpty() 提供伪tty(您的情况是the first reason described in pexpect docs).
      • 您可以在第一个示例中调用p.communicate() 而不是p.stdin.close(); p.wait()。在最后一个示例中没有理由调用p.communicate()(只需使用subprocess.check_call())。
      • 元:你可以accept your own answer
      • @J.F.Sebastian 太棒了,必须查看通信源才能看到它在写入输入后关闭标准输入。太好了,谢谢!
      • @J.F.Sebastian 如果我理解正确,check_call 是当您不希望进程中的 stdout/stderr 并且只希望在失败时通知时的选项。也是一个可行的选择 - 但使用 check_call 我也必须使用我假设的 setsid。对吗?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-11-23
      • 1970-01-01
      • 1970-01-01
      • 2012-04-26
      • 1970-01-01
      • 1970-01-01
      • 2015-12-31
      相关资源
      最近更新 更多