Python子进程超时?

Posted

技术标签:

【中文标题】Python子进程超时?【英文标题】:Python subprocess timeout? 【发布时间】:2011-04-13 14:14:45 【问题描述】:

是否有任何参数或选项可以为 Python 的 subprocess.Popen 方法设置超时?

类似这样的:

subprocess.Popen(['..'], ..., timeout=20)?

【问题讨论】:

相关:subprocess with timeout 【参考方案1】:

我建议查看threading 模块中的Timer class。我用它来实现Popen 的超时。

首先,创建一个回调:

def timeout( p ):
    if p.poll() is None:
        print 'Error: process taking too long to complete--terminating'
        p.kill()

然后打开进程:

proc = Popen( ... )

然后创建一个将调用回调的计时器,将进程传递给它。

t = threading.Timer( 10.0, timeout, [proc] )
t.start()
t.join()

在程序后面的某个地方,您可能想要添加以下行:

t.cancel()

否则,python 程序将一直运行,直到计时器完成运行。

编辑:有人告诉我,subprocess p 可能会在 p.poll()p.kill() 调用之间终止。我相信下面的代码可以解决这个问题:

import errno

def timeout( p ):
    if p.poll() is None:
        try:
            p.kill()
            print 'Error: process taking too long to complete--terminating'
        except OSError as e:
            if e.errno != errno.ESRCH:
                raise

虽然您可能希望清理异常处理以专门处理子进程已正常终止时发生的特定异常。

【讨论】:

迈克,你能详细说明或编辑上面的修复吗?类似的代码我已经用过好几次了,如果有问题,我一定会修复它。 print 'Error: process taking too long to complete--terminating' 可以运行,即使它是一个谎言并且你的进程在你没有杀死它的情况下终止(因为它在你的 pollkill 调用之间的时刻执行它)。 更重要的是,如果子进程在这些调用之间终止,p.kill() 将引发异常。 PyCharm 建议使用 if p.poll() is None 比使用等式关系 (==) 更好 @Thorben,你是对的。此外,这实际上是 PyCharm 通知您的 PEP8 事情。一般来说,“与像 None 这样的单例的比较应该总是用 is 或 not 来完成,而不是相等运算符。” legacy.python.org/dev/peps/pep-0008/…【参考方案2】:

subprocess.Popen 不会阻塞,因此您可以执行以下操作:

import time

p = subprocess.Popen(['...'])
time.sleep(20)
if p.poll() is None:
  p.kill()
  print 'timed out'
else:
  print p.communicate()

它的缺点是您必须始终等待至少 20 秒才能完成。

【讨论】:

这会将这个过程冻结 20 秒。这可以接受吗? 看来是!我认为这是使用子流程的一点不便。 @aaronasterling:Popen/sleep() 是一种简单的可移植且高效的方法,可以在超时的情况下运行外部永无止境的程序。虽然 p.communicate() 在这段代码中的使用是有问题的(它可能会导致丢失一些输出)。当Popen/sleep() 有用时,请参阅Stop reading process output in Python without hang? 问题。 可以通过使用类似的东西来减少缺点:wait = 0 while p.poll() is None and wait < 15: wait += 1 sleep(1) if p.poll() is None: p.kill()【参考方案3】:
import subprocess, threading

class Command(object):
    def __init__(self, cmd):
        self.cmd = cmd
        self.process = None

    def run(self, timeout):
        def target():
            print 'Thread started'
            self.process = subprocess.Popen(self.cmd, shell=True)
            self.process.communicate()
            print 'Thread finished'

        thread = threading.Thread(target=target)
        thread.start()

        thread.join(timeout)
        if thread.is_alive():
            print 'Terminating process'
            self.process.terminate()
            thread.join()
        print self.process.returncode

command = Command("echo 'Process started'; sleep 2; echo 'Process finished'")
command.run(timeout=3)
command.run(timeout=1)

这个输出应该是:

Thread started
Process started
Process finished
Thread finished
0
Thread started
Process started
Terminating process
Thread finished
-15

可以看出,在第一次执行中,进程正确完成(返回代码 0),而在第二次执行中,进程被终止(返回代码 -15)。

我没有在 Windows 中测试过;但是,除了更新示例命令之外,我认为它应该可以工作,因为我在文档中没有找到任何说明 thread.join 或 process.terminate 不受支持的内容。

【讨论】:

【参考方案4】:

你可以这样做

from twisted.internet import reactor, protocol, error, defer

class DyingProcessProtocol(protocol.ProcessProtocol):
    def __init__(self, timeout):
        self.timeout = timeout

    def connectionMade(self):
        @defer.inlineCallbacks
        def killIfAlive():
            try:
                yield self.transport.signalProcess('KILL')
            except error.ProcessExitedAlready:
                pass

        d = reactor.callLater(self.timeout, killIfAlive)

reactor.spawnProcess(DyingProcessProtocol(20), ...)

使用 Twisted 的异步进程 API。

【讨论】:

【参考方案5】:

没有内置 Python 子进程自动超时功能,因此您必须自己构建。

这适用于我在运行 python 2.7.3 的 Ubuntu 12.10 上

把它放在一个名为 test.py 的文件中

#!/usr/bin/python
import subprocess
import threading

class RunMyCmd(threading.Thread):
    def __init__(self, cmd, timeout):
        threading.Thread.__init__(self)
        self.cmd = cmd 
        self.timeout = timeout

    def run(self):
        self.p = subprocess.Popen(self.cmd)
        self.p.wait()

    def run_the_process(self):
        self.start()
        self.join(self.timeout)

        if self.is_alive():
            self.p.terminate()   #if your process needs a kill -9 to make 
                                 #it go away, use self.p.kill() here instead.

            self.join()

RunMyCmd(["sleep", "20"], 3).run_the_process()

保存并运行它:

python test.py

sleep 20 命令需要 20 秒才能完成。如果它没有在 3 秒内终止(它不会),则该进程终止。

el@apollo:~$  python test.py 
el@apollo:~$ 

从进程运行到终止之间有 3 秒的时间。

【讨论】:

【参考方案6】:

很遗憾,没有这样的解决方案。我设法使用线程计时器来执行此操作,该计时器将与在超时后将其终止的进程一起启动,但由于僵尸进程或诸如此类的原因,我确实遇到了一些陈旧的文件描述符问题。

【讨论】:

僵尸进程的文件描述符我也遇到过这样的问题。 苏丹。应该可以纠正这些。我确实设法最终将我的应用程序打磨成可行的东西,但它还不够通用,无法发布。【参考方案7】:

不,没有超时。我想,您正在寻找的是在一段时间后杀死子进程。既然您能够向子进程发出信号,那么您也应该能够杀死它。

向子进程发送信号的通用方法:

proc = subprocess.Popen([command])
time.sleep(1)
print 'signaling child'
sys.stdout.flush()
os.kill(proc.pid, signal.SIGUSR1)

您可以使用此机制在超时后终止。

【讨论】:

对于所有使用子进程的应用来说,它是通用的 sys.stdout.flush() 吗? 这很像 Abhishek 的回答。他正在使用 SIGKILL,而您正在使用 SIGUSR1。它有同样的问题。【参考方案8】:

从 Python 3.3 开始,子进程模块中的阻塞辅助函数还有一个 timeout 参数。

https://docs.python.org/3/library/subprocess.html

【讨论】:

【参考方案9】:

是的,https://pypi.python.org/pypi/python-subprocess2 将为 Popen 模块扩展两个附加功能,

Popen.waitUpTo(timeout=seconds)

这将等待特定秒数以完成该过程,否则返回无

还有,

Popen.waitOrTerminate

这将等待一个点,然后调用 .terminate(),然后调用 .kill(),一个或另一个或两者的某种组合,请参阅文档以获取完整详细信息:

http://htmlpreview.github.io/?https://github.com/kata198/python-subprocess2/blob/master/doc/subprocess2.html

【讨论】:

【参考方案10】:

对于 Linux,您可以使用信号。这取决于平台,因此 Windows 需要另一种解决方案。不过它可能适用于 Mac。

def launch_cmd(cmd, timeout=0):
    '''Launch an external command

    It launchs the program redirecting the program's STDIO
    to a communication pipe, and appends those responses to
    a list.  Waits for the program to exit, then returns the
    ouput lines.

    Args:
        cmd: command Line of the external program to launch
        time: time to wait for the command to complete, 0 for indefinitely
    Returns:
        A list of the response lines from the program    
    '''

    import subprocess
    import signal

    class Alarm(Exception):
        pass

    def alarm_handler(signum, frame):
        raise Alarm

    lines = []

    if not launch_cmd.init:
        launch_cmd.init = True
        signal.signal(signal.SIGALRM, alarm_handler)

    p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
    signal.alarm(timeout)  # timeout sec

    try:
        for line in p.stdout:
            lines.append(line.rstrip())
        p.wait()
        signal.alarm(0)  # disable alarm
    except:
        print "launch_cmd taking too long!"
        p.kill()

    return lines        
launch_cmd.init = False

【讨论】:

以上是关于Python子进程超时?的主要内容,如果未能解决你的问题,请参考以下文章

多个子进程超时

使用子进程时如何超时[重复]

子进程超时:TimeoutExpired 异常后怎么办? [复制]

C Windows:生成子进程以停止和重新启动(超时后)父进程

用 Python 子进程关闭 Excel 自动化中的弹窗

fork进程与Threading之超时退出