关于python:子进程超时失败

Subprocess timeout failure

我想在子进程上使用超时

1
2
 from subprocess32 import check_output
 output = check_output("sleep 30", shell=True, timeout=1)

不幸的是,虽然这会引发超时错误,但它会在30秒后发生。 似乎check_output不能中断shell命令。

我可以在Python端做些什么来阻止它?
我怀疑subprocess32无法终止超时进程。


超时的check_output()基本上是:

1
2
3
4
5
6
7
with Popen(*popenargs, stdout=PIPE, **kwargs) as process:
    try:
        output, unused_err = process.communicate(inputdata, timeout=timeout)
    except TimeoutExpired:
        process.kill()
        output, unused_err = process.communicate()
        raise TimeoutExpired(process.args, timeout, output=output)

有两个问题:

  • [第二个] .communicate()可能会等待后代进程,而不仅仅是直接子进程,请参阅Python子进程.check_call vs
    .check_output
  • process.kill()可能不会终止整个进程树,请参阅如何终止使用shell = True启动的python子进程

它会导致您观察到的行为:TimeoutExpired在一秒内发生,shell被终止,但check_output()仅在孙子sleep进程退出后30秒内返回。

要解决问题,请终止整个进程树(属于同一组的所有子进程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env python3
import os
import signal
from subprocess import Popen, PIPE, TimeoutExpired
from time import monotonic as timer

start = timer()
with Popen('sleep 30', shell=True, stdout=PIPE, preexec_fn=os.setsid) as process:
    try:
        output = process.communicate(timeout=1)[0]
    except TimeoutExpired:
        os.killpg(process.pid, signal.SIGINT) # send signal to the process group
        output = process.communicate()[0]
print('Elapsed seconds: {:.2f}'.format(timer() - start))

产量

1
Elapsed seconds: 1.00


Python 3.6的更新。

这仍然在发生,但我已经测试了很多check_outputcommunicaterun方法的组合,现在我清楚地了解了bug的位置以及如何在Python 3.5上以简单的方式避免它Python 3.6。

我的结论:在stdoutstderrstdin参数(用于Popenrun方法)中混合使用shell=True和任何PIPE时会发生这种情况。

注意:check_output在里面使用PIPE
如果你看看Python 3.6里面的代码,它基本上是用stdout=PIPE调用run:https://github.com/python/cpython/blob/ae011e00189d9083dd84c357718264e24fe77314/Lib/subprocess.py#L335

因此,要解决Python 3.5或3.6上的@innisfree问题,请执行以下操作:

1
check_output(['sleep', '30'], timeout=1)

对于其他情况,请避免混合shell=TruePIPE,请记住check_output使用PIPE