2016-06-29 00:14:07 +02:00
|
|
|
from types import TracebackType
|
2017-01-31 10:45:32 +01:00
|
|
|
from typing import Any, Callable, Optional, Tuple, Type, TypeVar
|
2013-04-23 18:51:17 +02:00
|
|
|
|
2017-09-27 10:06:17 +02:00
|
|
|
import six
|
2013-05-01 21:59:56 +02:00
|
|
|
import sys
|
2013-01-29 22:19:05 +01:00
|
|
|
import time
|
|
|
|
import ctypes
|
2013-01-29 21:47:53 +01:00
|
|
|
import threading
|
2015-11-01 17:15:05 +01:00
|
|
|
from six.moves import range
|
2013-01-29 21:47:53 +01:00
|
|
|
|
|
|
|
# Based on http://code.activestate.com/recipes/483752/
|
|
|
|
|
|
|
|
class TimeoutExpired(Exception):
|
|
|
|
'''Exception raised when a function times out.'''
|
2016-11-29 07:22:02 +01:00
|
|
|
|
2013-01-29 21:47:53 +01:00
|
|
|
def __str__(self):
|
2016-06-29 00:14:07 +02:00
|
|
|
# type: () -> str
|
2013-01-29 21:47:53 +01:00
|
|
|
return 'Function call timed out.'
|
|
|
|
|
2016-06-29 00:14:07 +02:00
|
|
|
ResultT = TypeVar('ResultT')
|
|
|
|
|
2013-01-29 21:47:53 +01:00
|
|
|
def timeout(timeout, func, *args, **kwargs):
|
2016-06-29 00:14:07 +02:00
|
|
|
# type: (float, Callable[..., ResultT], *Any, **Any) -> ResultT
|
2013-01-29 21:47:53 +01:00
|
|
|
'''Call the function in a separate thread.
|
|
|
|
Return its return value, or raise an exception,
|
2013-01-29 22:19:05 +01:00
|
|
|
within approximately 'timeout' seconds.
|
2013-01-29 21:47:53 +01:00
|
|
|
|
2013-01-29 22:19:05 +01:00
|
|
|
The function may receive a TimeoutExpired exception
|
|
|
|
anywhere in its code, which could have arbitrary
|
|
|
|
unsafe effects (resources not released, etc.).
|
|
|
|
It might also fail to receive the exception and
|
|
|
|
keep running in the background even though
|
|
|
|
timeout() has returned.
|
2013-01-29 21:47:53 +01:00
|
|
|
|
|
|
|
This may also fail to interrupt functions which are
|
|
|
|
stuck in a long-running primitive interpreter
|
|
|
|
operation.'''
|
|
|
|
|
|
|
|
class TimeoutThread(threading.Thread):
|
|
|
|
def __init__(self):
|
2016-06-29 00:14:07 +02:00
|
|
|
# type: () -> None
|
2013-01-29 21:47:53 +01:00
|
|
|
threading.Thread.__init__(self)
|
2017-05-07 17:12:41 +02:00
|
|
|
self.result = None # type: Optional[ResultT]
|
2017-05-26 02:08:16 +02:00
|
|
|
self.exc_info = None # type: Optional[Tuple[Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]]]
|
2013-01-29 21:47:53 +01:00
|
|
|
|
|
|
|
# Don't block the whole program from exiting
|
|
|
|
# if this is the only thread left.
|
|
|
|
self.daemon = True
|
|
|
|
|
|
|
|
def run(self):
|
2016-06-29 00:14:07 +02:00
|
|
|
# type: () -> None
|
2013-01-29 21:47:53 +01:00
|
|
|
try:
|
|
|
|
self.result = func(*args, **kwargs)
|
2013-05-01 21:59:56 +02:00
|
|
|
except BaseException:
|
|
|
|
self.exc_info = sys.exc_info()
|
2013-01-29 21:47:53 +01:00
|
|
|
|
2013-01-29 22:19:05 +01:00
|
|
|
def raise_async_timeout(self):
|
2016-06-29 00:14:07 +02:00
|
|
|
# type: () -> None
|
2013-01-29 22:19:05 +01:00
|
|
|
# Called from another thread.
|
|
|
|
# Attempt to raise a TimeoutExpired in the thread represented by 'self'.
|
|
|
|
tid = ctypes.c_long(self.ident)
|
|
|
|
result = ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
|
|
|
tid, ctypes.py_object(TimeoutExpired))
|
|
|
|
if result > 1:
|
|
|
|
# "if it returns a number greater than one, you're in trouble,
|
|
|
|
# and you should call it again with exc=NULL to revert the effect"
|
|
|
|
#
|
|
|
|
# I was unable to find the actual source of this quote, but it
|
|
|
|
# appears in the many projects across the Internet that have
|
|
|
|
# copy-pasted this recipe.
|
|
|
|
ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
|
|
|
|
|
2013-01-29 21:47:53 +01:00
|
|
|
thread = TimeoutThread()
|
|
|
|
thread.start()
|
|
|
|
thread.join(timeout)
|
|
|
|
|
2016-07-03 14:37:59 +02:00
|
|
|
if thread.is_alive():
|
2013-01-29 22:19:05 +01:00
|
|
|
# Gamely try to kill the thread, following the dodgy approach from
|
2013-05-01 22:00:45 +02:00
|
|
|
# http://stackoverflow.com/a/325528/90777
|
2013-01-29 22:19:05 +01:00
|
|
|
#
|
|
|
|
# We need to retry, because an async exception received while the
|
|
|
|
# thread is in a system call is simply ignored.
|
2015-11-01 17:15:05 +01:00
|
|
|
for i in range(10):
|
2013-01-29 22:19:05 +01:00
|
|
|
thread.raise_async_timeout()
|
|
|
|
time.sleep(0.1)
|
2016-07-03 14:37:59 +02:00
|
|
|
if not thread.is_alive():
|
2013-01-29 22:19:05 +01:00
|
|
|
break
|
2013-01-29 21:47:53 +01:00
|
|
|
raise TimeoutExpired
|
2013-01-29 22:19:05 +01:00
|
|
|
|
2013-05-01 21:59:56 +02:00
|
|
|
if thread.exc_info:
|
|
|
|
# Raise the original stack trace so our error messages are more useful.
|
|
|
|
# from http://stackoverflow.com/a/4785766/90777
|
2015-11-01 17:14:59 +01:00
|
|
|
six.reraise(thread.exc_info[0], thread.exc_info[1], thread.exc_info[2])
|
2017-06-04 11:36:29 +02:00
|
|
|
assert thread.result is not None # assured if above did not reraise
|
2013-01-29 21:47:53 +01:00
|
|
|
return thread.result
|