为什么在这段代码中Python有时会抛出ValueError?

4

我有一些类似于这样的Python 3.5代码:

try:
    my_process = Popen(someargs, stdin=None, stdout=PIPE, stderr=PIPE)
    stdout, stderr = my_process.communicate(timeout=10)
    my_process.wait()
except TimeoutExpired:
    my_process.kill()
    stdout, stderr = my_process.communicate()

我正在尝试遵循Python子进程文档中描述的原则,链接在此处:https://docs.python.org/3/library/subprocess.html#subprocess.Popen.communicate,即在出现TimeoutError的情况下,我应该手动终止进程,然后完成通信。
原则上这听起来不错,但是定期(大约每50次中的1次左右)我会收到以下错误消息:
Traceback (most recent call last):
  File "/Users/xyz/myprogram/myprogram", line 125, in <module>
    stdout, stderr = my_process.communicate()
  File "/usr/local/Cellar/python3/3.5.0/Frameworks/Python.framework/Versions/3.5/lib/python3.5/subprocess.py", line 1068, in communicate
    stdout, stderr = self._communicate(input, endtime, timeout)
  File "/usr/local/Cellar/python3/3.5.0/Frameworks/Python.framework/Versions/3.5/lib/python3.5/subprocess.py", line 1689, in _communicate
    selector.register(self.stdout, selectors.EVENT_READ)
  File "/usr/local/Cellar/python3/3.5.0/Frameworks/Python.framework/Versions/3.5/lib/python3.5/selectors.py", line 342, in register
    key = super().register(fileobj, events, data)
  File "/usr/local/Cellar/python3/3.5.0/Frameworks/Python.framework/Versions/3.5/lib/python3.5/selectors.py", line 228, in register
    key = SelectorKey(fileobj, self._fileobj_lookup(fileobj), events, data)
  File "/usr/local/Cellar/python3/3.5.0/Frameworks/Python.framework/Versions/3.5/lib/python3.5/selectors.py", line 215, in _fileobj_lookup
    return _fileobj_to_fd(fileobj)
  File "/usr/local/Cellar/python3/3.5.0/Frameworks/Python.framework/Versions/3.5/lib/python3.5/selectors.py", line 39, in _fileobj_to_fd
    "{!r}".format(fileobj)) from None
ValueError: Invalid file object: <_io.BufferedReader name=5>

在我的情况下,第125行是第二个communicate()行。

看起来这个失败是因为某些流已关闭或终止 - 也许有时在kill()communicate()之间发生了这种情况?但如果是这样的话,我应该如何优雅地处理它呢?Python文档似乎没有涵盖这种情况。


你是否使用了多线程?否则,这看起来像是一个 bug。你能否创建一个虚拟的 child.py 文件,并传入 someargs 来重现这个问题? - jfs
以下是在 3.6.0 版本中重现此问题的代码:import subprocess import time po = subprocess.Popen(['/bin/ls'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) time.sleep(1) out, err = po.communicate(timeout=1) out, err = po.communicate(timeout=1) - mattismyname
2个回答

2
为了解决在异常处理程序中引发“ValueError”的问题,您可以直接从流中读取(简化代码路径——不要在一般情况下使用它):

要解决此问题,请从流中直接读取,而不是使用my_process.communicate()方法(这种方式更为简单):

from subprocess import Popen, PIPE, TimeoutExpired

with Popen(cmd, stdout=PIPE, stderr=PIPE) as process:
    try:
        stdout, stderr = process.communicate(timeout=10)
    except TimeoutExpired:
        process.kill()
        stdout = process.stdout.read() # the process is dead, no deadlock
        stderr = process.stderr.read()

在Python 3.5及以上版本中,您可以使用subprocess.run()函数:
import subprocess
from subprocess import PIPE, TimeoutExpired

try:
    result = subprocess.run(cmd, timeout=10, stdout=PIPE, stderr=PIPE)
except TimeoutExpired as e:
    result = e
stdout, stderr = result.stdout, result.stderr

虽然它与您的代码相同处理TimeoutExpired,因此您仍可能遇到ValueError。如果您使用此代码遇到ValueError,请在http://bugs.python.org报告问题。


非常有趣。不知道Python 3.5的改进。 - Mike Müller

1

改进的答案

使用空的字节对象作为stdoutstderr的返回值可能是一种解决方案。my_process.communicate()会帮你读取这些管道。因此,在这里最好不要使用文件对象:

try:
    my_process = Popen(someargs, stdin=None, stdout=PIPE, stderr=PIPE)
    stdout, stderr = my_process.communicate(timeout=10)
    my_process.wait()
except TimeoutExpired:
    my_process.kill()
    try:
        stdout, stderr = my_process.communicate()
    except ValueError:
        stdout = b''
        stderr = b''

原始答案

stdoutstderr 创建空文件对象可能是一种解决方案:

import io

try:
    my_process = Popen(someargs, stdin=None, stdout=PIPE, stderr=PIPE)
    stdout, stderr = my_process.communicate(timeout=10)
    my_process.wait()
except TimeoutExpired:
    my_process.kill()
    try:
        stdout, stderr = my_process.communicate()
    except ValueError:
        stdout = io.BytesIO()
        stderr = io.BytesIO()

Mike,这样做不会丢弃在“try”块期间检索到的任何缓冲区中的信息吗? - Andrew Ferrier
你应该使用 stdout, stderr = b'', b'',而不是 io.BytesIO() - jfs
谢谢提示。已添加改进。 - Mike Müller
需要使用 my_process.kill() 吗?文档中说:“如果超时时间到期,子进程将被杀死并等待。” - galath
这是subprocess.run()的帮助文档。subprocess.Popen()的帮助文档中提到:“如果超时时间到期,子进程不会被终止,因此为了正确清理,一个行为良好的应用程序应该杀死子进程并完成通信。” - Mike Müller

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接