| # Copyright (C) 2017 Apple Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND |
| # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR |
| # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| import logging |
| import math |
| import os |
| import signal |
| import time |
| import threading |
| |
| _log = logging.getLogger(__name__) |
| |
| |
| class Timeout(object): |
| |
| thread_exception = RuntimeError('Timeout originates from a different thread') |
| _process_to_timeout_map = {} |
| |
| class TimeoutData(object): |
| |
| def __init__(self, alarm_time, handler): |
| self.alarm_time = alarm_time |
| self.handler = handler |
| self.thread_id = threading.current_thread().ident |
| |
| @staticmethod |
| def default_handler(signum, frame): |
| raise RuntimeError('Timeout alarm was triggered') |
| |
| @staticmethod |
| def current(): |
| result = Timeout._process_to_timeout_map.get(os.getpid(), []) |
| if not result: |
| return None |
| if result[0].thread_id != threading.current_thread().ident: |
| _log.critical('Using both alarms and threading in the same process, this is unsupported') |
| raise Timeout.thread_exception |
| return result[0] |
| |
| def __init__(self, seconds=1, handler=None): |
| if seconds == 0: |
| raise RuntimeError('Cannot have a timeout of 0 seconds') |
| |
| if isinstance(handler, BaseException): |
| exception = handler |
| |
| def exception_handler(signum, frame): |
| raise exception |
| |
| handler = exception_handler |
| |
| self._timeout = seconds |
| self._handler = handler if handler else Timeout.default_handler |
| self.data = None |
| |
| @staticmethod |
| def _bind_timeout_data_to_alarm(data): |
| def handler(signum, frame): |
| assert signum == signal.SIGALRM |
| if data.thread_id != threading.current_thread().ident: |
| raise Timeout.thread_exception |
| data.handler(signum, frame) |
| |
| current_time = time.time() |
| if data.alarm_time <= current_time: |
| handler(signal.SIGALRM, None) |
| |
| signal.signal(signal.SIGALRM, handler) |
| signal.alarm(int(math.ceil(data.alarm_time - current_time))) |
| |
| def __enter__(self): |
| signal.alarm(0) # Imiediatly disable the alarm so we aren't interupted. |
| self.data = Timeout.TimeoutData(time.time() + self._timeout, self._handler) |
| current_timeout = Timeout.current() |
| |
| # Another timeout is more urgent. |
| if current_timeout and current_timeout.alarm_time < self.data.alarm_time: |
| for i in xrange(len(Timeout._process_to_timeout_map[os.getpid()]) - 1): |
| if self.data.alarm_time < Timeout._process_to_timeout_map[os.getpid()][i + 1].alarm_time: |
| Timeout._process_to_timeout_map[os.getpid()].insert(i, self.data) |
| break |
| Timeout._process_to_timeout_map[os.getpid()].append(self.data) |
| |
| # This is the most urgent timeout |
| else: |
| Timeout._process_to_timeout_map[os.getpid()] = [self.data] + Timeout._process_to_timeout_map.get(os.getpid(), []) |
| |
| Timeout._bind_timeout_data_to_alarm(Timeout.current()) |
| return self |
| |
| def __exit__(self, exc_type, exc_value, traceback): |
| signal.alarm(0) # Imiediatly disable the alarm so we aren't interupted. |
| |
| if not Timeout._process_to_timeout_map[os.getpid()]: |
| raise RuntimeError('No timeout registered') |
| Timeout._process_to_timeout_map[os.getpid()].remove(self.data) |
| self.data = None |
| |
| if Timeout._process_to_timeout_map[os.getpid()]: |
| Timeout._bind_timeout_data_to_alarm(Timeout.current()) |