#
# This file is part of Dragonfly.
# (c) Copyright 2007, 2008 by Christo Butcher
# Licensed under the LGPL.
#
# Dragonfly is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Dragonfly is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with Dragonfly. If not, see
# <http://www.gnu.org/licenses/>.
#
"""
Multiplexing interface to a timer
============================================================================
"""
import time
import logging
from threading import Thread, current_thread, RLock
#---------------------------------------------------------------------------
[docs]
class Timer(object):
"""
Timer class for calling a function every N seconds.
Constructor arguments:
- *function* (*callable*) -- the function to call every N seconds. Must
have no required arguments.
- *interval* (*float*) -- number of seconds between calls to the
function. Note that this is on a best-effort basis only.
- *manager* (:class:`TimerManagerBase`) -- engine timer manager instance.
- *repeating* (*bool*) -- whether to call the function every N seconds
or just once (default: True).
Instances of this class are normally initialised from
:meth:`engine.create_timer`.
"""
_log = logging.getLogger("engine.timer")
def __init__(self, function, interval, manager, repeating=True):
self.function = function
self.interval = interval
self.manager = manager
self.repeating = repeating
self.active = False
self.next_time = None
self.start()
[docs]
def start(self):
"""
Start calling the timer's function on an interval.
This method is called on initialisation.
"""
if self.active:
return
self.manager.add_timer(self)
self.active = True
self.next_time = time.time() + self.interval
[docs]
def stop(self):
""" Stop calling the timer's function on an interval. """
if not self.active:
return
self.manager.remove_timer(self)
self.active = False
self.next_time = None
[docs]
def call(self):
"""
Call the timer's function.
This method is normally called by the timer manager.
"""
self.next_time = time.time() + self.interval
try:
self.function()
except Exception as e:
self._log.exception("Exception during timer callback: %s" % (e,))
if not self.repeating:
self.stop()
[docs]
class TimerManagerBase(object):
""" Base timer manager class. """
_log = logging.getLogger("engine.timer")
def __init__(self, interval, engine):
self.interval = interval
self.engine = engine
self.timers = []
self._lock = RLock()
self._enabled = True
self._active = False
[docs]
def add_timer(self, timer):
""" Add a timer and activate the main callback if required. """
with self._lock:
self.timers.append(timer)
if len(self.timers) == 1 and self._enabled:
self._activate_main_callback(self.main_callback,
self.interval)
self._active = True
[docs]
def remove_timer(self, timer):
""" Remove a timer and deactivate the main callback if required. """
try:
with self._lock:
self.timers.remove(timer)
except Exception as e:
self._log.exception("Failed to remove timer: %s" % e)
return
if len(self.timers) == 0 and self._enabled:
self._deactivate_main_callback()
self._active = False
[docs]
def enable(self):
"""
Method to re-enable the main timer callback.
The main timer callback is enabled by default. This method is only
useful if :meth:`disable` is called.
"""
self._enabled = True
[docs]
def disable(self):
"""
Method to disable execution of the main timer callback.
This method is used for testing timer-related functionality without
race conditions.
"""
self._enabled = False
[docs]
def main_callback(self):
""" Method to call each timer's function when required. """
# Deactivate the engine's main callback if necessary. We don't
# return early here because this method should work if called
# manually.
if not self._enabled and self._active:
self._deactivate_main_callback()
self._active = False
now = time.time()
for c in tuple(self.timers):
if c.next_time < now:
try:
c.call()
except Exception as e:
self._log.exception("Exception occurred during"
" timer callback: %s" % (e,))
[docs]
def _activate_main_callback(self, callback, msec):
"""
Virtual method to implement to start calling :meth:`main_callback`
on an interval.
"""
raise NotImplementedError("_activate_main_callback() not implemented for"
" %s class." % self.__class__.__name__)
[docs]
def _deactivate_main_callback(self):
"""
Virtual method to implement to stop calling :meth:`main_callback` on
an interval.
"""
raise NotImplementedError("_deactivate_main_callback() not implemented for"
" %s class." % self.__class__.__name__)
[docs]
class ThreadedTimerManager(TimerManagerBase):
"""
Timer manager class using a daemon thread.
This class is used by the "text" engine. It is only suitable for engine
backends with no recognition loop to execute timer functions on.
.. warning::
The timer interface is **not** thread-safe. Use the :meth:`enable`
and :meth:`disable` methods if you need finer control over timer
function execution.
"""
def __init__(self, interval, engine):
TimerManagerBase.__init__(self, interval, engine)
self._running = False
self._thread = None
def _activate_main_callback(self, callback, sec):
""""""
# Do nothing if the thread is already running.
if self._running:
return
def run():
while self._running:
time.sleep(sec)
callback()
self._running = True
self._thread = Thread(target=run)
self._thread.setDaemon(True)
self._thread.start()
def _deactivate_main_callback(self):
""""""
# Stop the thread's main loop and wait until it finishes, timing out
# after 5 seconds.
should_join = (self._thread and self._thread.is_alive() and
self._thread is not current_thread())
self._running = False
if should_join:
timeout = 5
self._thread.join(timeout=timeout)
if self._thread.is_alive():
raise RuntimeError("failed to deactivate main callback "
"after %d seconds" % timeout)
[docs]
class DelegateTimerManagerInterface(object):
"""
DelegateTimerManager interface.
"""
def __init__(self):
self._timer_callback = None
self._timer_interval = None
self._timer_next_time = 0
[docs]
def set_timer_callback(self, callback, sec):
"""
Method to set the timer manager's callback.
:param callback: function to call every N seconds
:type callback: callable | None
:param sec: number of seconds between calls to the callback function
:type sec: float | int
"""
self._timer_callback = callback
self._timer_interval = sec
self._timer_next_time = time.time()
def call_timer_callback(self):
""""""
if not (self._timer_callback and self._timer_interval):
return
now = time.time()
if self._timer_next_time < now:
self._timer_next_time = now + self._timer_interval
self._timer_callback()
[docs]
class DelegateTimerManager(TimerManagerBase):
"""
Timer manager class that calls :meth:`main_callback` through an
engine-specific callback function.
Engines using this class should implement the methods in
:class:`DelegateManagerInterface`.
"""
def __init__(self, interval, engine):
TimerManagerBase.__init__(self, interval, engine)
def _activate_main_callback(self, callback, sec):
""""""
self.engine.set_timer_callback(callback, sec)
def _deactivate_main_callback(self):
""""""
self.engine.set_timer_callback(None, 0)