"""
Timeout decorator.

    :copyright: (c) 2012-2013 by PN.
    :license: MIT, see LICENSE for more details.
"""

from __future__ import print_function
from __future__ import unicode_literals
from __future__ import division

import sys
import time
import multiprocessing
import signal
from functools import wraps
import psutil

############################################################
# Timeout
############################################################

# http://www.saltycrane.com/blog/2010/04/
#                                  using-python-timeout-decorator-uploading-s3/
# Used work of Stephen "Zero" Chappell <Noctis.Skytower@gmail.com>
# in https://code.google.com/p/verse-quiz/source/browse/trunk/timeout.py


class MyTimeoutError(AssertionError):

    """Thrown when a timeout occurs in the `timeout` context manager."""

    def __init__(self, value="Timed Out"):  # pylint: disable=W0231
        self.value = value

    def __str__(self):
        return repr(self.value)  # pragma: no cover


def timeout(seconds=None, use_signals=True):
    """Add a timeout parameter to a function and return it.

    :param seconds: optional time limit in seconds or fractions of a second.
        If None is passed, no timeout is applied. This adds some flexibility to
        the usage: you can disable timing out depending on the settings.
    :type seconds: float
    :param use_signals: flag indicating whether signals should be used for
        timing function out or the multiprocessing.
        When using multiprocessing, timeout granularity is limited to 10ths of
        a second.
    :type use_signals: bool

    :raises: MyTimeoutError if time limit is reached

    It is illegal to pass anything other than a function as the first
    parameter. The function is wrapped and returned to the caller.
    """
    def decorate(function):
        """Subfunction"""
        if not seconds:
            return function

        if use_signals:
            def handler(signum, frame):  # pylint: disable=W0613
                """Signal handler"""
                raise MyTimeoutError()

            @wraps(function)
            def new_function(*args, **kwargs):
                """Subfunction"""
                new_seconds = kwargs.pop('timeout', seconds)
                if new_seconds:  # pragma: no cover
                    old = signal.signal(signal.SIGALRM, handler)
                    signal.setitimer(signal.ITIMER_REAL, new_seconds)
                try:
                    return function(*args, **kwargs)
                finally:
                    if new_seconds:  # pragma: no cover
                        signal.setitimer(signal.ITIMER_REAL, 0)
                        signal.signal(signal.SIGALRM, old)
            return new_function
        else:
            return _Timeout(function, seconds)

    return decorate


def _target(queue, function, *args, **kwargs):
    """Run a function with arguments and return output via a queue.

    This is a helper function for the Process created in _Timeout. It runs
    the function with positional arguments and keyword arguments and then
    returns the function's output by way of a queue. If an exception gets
    raised, it is returned to _Timeout to be raised by the value property.
    """
    try:
        queue.put((True, function(*args, **kwargs)))
    except:  # pragma: no cover  # pylint: disable=W0702
        queue.put((False, sys.exc_info()[1]))  # pragma: no cover


def _killchildprocesses(pid):
    """This function will kill all childprocesses of :pid: """

    pproc = psutil.Process(pid)
    for proc in reversed(pproc.children(recursive=True)):
        try:  # pragma: no cover
            proc.terminate()  # pragma: no cover
        except psutil.NoSuchProcess:  # pragma: no cover
            pass  # pragma: no cover
        else:  # pragma: no cover
            try:  # pragma: no cover
                proc.wait(1)  # pragma: no cover
            except psutil.TimeoutExpired:  # pragma: no cover
                try:  # pragma: no cover
                    proc.kill()  # pragma: no cover
                except psutil.NoSuchProcess:  # pragma: no cover
                    pass  # pragma: no cover


class _Timeout:

    """Wrap a function and add a timeout (limit) attribute to it.

    Instances of this class are automatically generated by the add_timeout
    function defined above. Wrapping a function allows asynchronous calls
    to be made and termination of execution after a timeout has passed.
    """

    def __init__(self, function, limit):
        """Initialize instance in preparation for being called."""
        self.__limit = limit
        self.__function = function
        self.__name__ = function.__name__
        self.__doc__ = function.__doc__
        self.__timeout = time.time()
        self.__process = multiprocessing.Process()
        self.__queue = multiprocessing.Queue()

    def __call__(self, *args, **kwargs):
        """Execute the embedded function object asynchronously.

        The function given to the constructor is transparently called and
        requires that "ready" be intermittently polled. If and when it is
        True, the "value" property may then be checked for returned data.
        """
        self.__limit = kwargs.pop('timeout', self.__limit)
        self.__queue = multiprocessing.Queue(1)
        args = (self.__queue, self.__function) + args
        self.__process = multiprocessing.Process(target=_target,
                                                 args=args,
                                                 kwargs=kwargs)
        self.__process.daemon = True
        self.__process.start()
        self.__timeout = self.__limit + time.time()
        while not self.ready:
            time.sleep(0.01)
        return self.value

    def cancel(self):
        """Terminate any possible execution of the embedded function."""
        if self.__process.is_alive():
            _killchildprocesses(self.__process.pid)
            self.__process.terminate()
        raise MyTimeoutError()

    @property
    def ready(self):
        """Read-only property indicating status of "value" property."""
        if self.__timeout < time.time():
            self.cancel()
        return self.__queue.full() and not self.__queue.empty()

    @property
    def value(self):
        """Read-only property containing data returned from function."""
        if self.ready is True:  # pragma: no cover
            flag, load = self.__queue.get()
            if flag:
                return load
            raise load  # pragma: no cover
# vim: ft=python ts=4 sta sw=4 et ai
# python: 3
