2017-04-25 16:45:34 +01:00
|
|
|
"""
|
2013-08-29 15:21:13 +01:00
|
|
|
event.py: event loop implementation using a heap queue and threads.
|
2017-04-25 16:45:34 +01:00
|
|
|
"""
|
|
|
|
|
2013-08-29 15:21:13 +01:00
|
|
|
import heapq
|
2017-04-25 16:45:34 +01:00
|
|
|
import threading
|
|
|
|
import time
|
2019-10-17 22:52:31 +01:00
|
|
|
from functools import total_ordering
|
2020-01-14 06:15:44 +00:00
|
|
|
from typing import Any, Callable
|
2013-08-29 15:21:13 +01:00
|
|
|
|
2017-04-25 16:45:34 +01:00
|
|
|
|
|
|
|
class Timer(threading.Thread):
|
|
|
|
"""
|
|
|
|
Based on threading.Timer but cancel() returns if the timer was
|
|
|
|
already running.
|
|
|
|
"""
|
|
|
|
|
2020-01-14 06:15:44 +00:00
|
|
|
def __init__(
|
|
|
|
self, interval: float, function: Callable, args: Any = None, kwargs: Any = None
|
|
|
|
) -> None:
|
2017-05-04 18:36:13 +01:00
|
|
|
"""
|
|
|
|
Create a Timer instance.
|
|
|
|
|
|
|
|
:param interval: time interval
|
|
|
|
:param function: function to call when timer finishes
|
|
|
|
:param args: function arguments
|
|
|
|
:param kwargs: function keyword arguments
|
|
|
|
"""
|
2019-10-23 17:51:52 +01:00
|
|
|
super().__init__()
|
2017-04-25 16:45:34 +01:00
|
|
|
self.interval = interval
|
|
|
|
self.function = function
|
2017-05-04 18:36:13 +01:00
|
|
|
|
2017-04-25 16:45:34 +01:00
|
|
|
self.finished = threading.Event()
|
|
|
|
self._running = threading.Lock()
|
|
|
|
|
2017-05-04 18:36:13 +01:00
|
|
|
# validate arguments were provided
|
|
|
|
if args:
|
|
|
|
self.args = args
|
|
|
|
else:
|
|
|
|
self.args = []
|
|
|
|
|
|
|
|
# validate keyword arguments were provided
|
|
|
|
if kwargs:
|
|
|
|
self.kwargs = kwargs
|
|
|
|
else:
|
|
|
|
self.kwargs = {}
|
|
|
|
|
2020-01-14 06:15:44 +00:00
|
|
|
def cancel(self) -> bool:
|
2017-04-25 16:45:34 +01:00
|
|
|
"""
|
|
|
|
Stop the timer if it hasn't finished yet. Return False if
|
|
|
|
the timer was already running.
|
2017-05-04 18:36:13 +01:00
|
|
|
|
|
|
|
:return: True if canceled, False otherwise
|
2020-01-17 00:12:01 +00:00
|
|
|
"""
|
2017-04-25 16:45:34 +01:00
|
|
|
locked = self._running.acquire(False)
|
|
|
|
if locked:
|
|
|
|
self.finished.set()
|
|
|
|
self._running.release()
|
|
|
|
return locked
|
2013-08-29 15:21:13 +01:00
|
|
|
|
2020-01-14 06:15:44 +00:00
|
|
|
def run(self) -> None:
|
2017-05-04 18:36:13 +01:00
|
|
|
"""
|
|
|
|
Run the timer.
|
|
|
|
|
|
|
|
:return: nothing
|
|
|
|
"""
|
2017-04-25 16:45:34 +01:00
|
|
|
self.finished.wait(self.interval)
|
|
|
|
with self._running:
|
|
|
|
if not self.finished.is_set():
|
|
|
|
self.function(*self.args, **self.kwargs)
|
|
|
|
self.finished.set()
|
|
|
|
|
|
|
|
|
2019-10-17 22:52:31 +01:00
|
|
|
@total_ordering
|
2019-10-23 17:31:07 +01:00
|
|
|
class Event:
|
2017-05-04 18:36:13 +01:00
|
|
|
"""
|
|
|
|
Provides event objects that can be used within the EventLoop class.
|
|
|
|
"""
|
|
|
|
|
2020-01-14 06:15:44 +00:00
|
|
|
def __init__(
|
|
|
|
self, eventnum: int, event_time: float, func: Callable, *args: Any, **kwds: Any
|
|
|
|
) -> None:
|
2017-05-04 18:36:13 +01:00
|
|
|
"""
|
|
|
|
Create an Event instance.
|
|
|
|
|
|
|
|
:param eventnum: event number
|
|
|
|
:param event_time: event time
|
|
|
|
:param func: event function
|
|
|
|
:param args: function arguments
|
|
|
|
:param kwds: function keyword arguments
|
|
|
|
"""
|
2017-04-25 16:45:34 +01:00
|
|
|
self.eventnum = eventnum
|
|
|
|
self.time = event_time
|
|
|
|
self.func = func
|
|
|
|
self.args = args
|
|
|
|
self.kwds = kwds
|
|
|
|
self.canceled = False
|
|
|
|
|
2020-01-14 06:15:44 +00:00
|
|
|
def __lt__(self, other: "Event") -> bool:
|
2019-10-17 22:52:31 +01:00
|
|
|
result = self.time < other.time
|
|
|
|
if result:
|
|
|
|
result = self.eventnum < other.eventnum
|
|
|
|
return result
|
2017-04-25 16:45:34 +01:00
|
|
|
|
2020-01-14 06:15:44 +00:00
|
|
|
def run(self) -> None:
|
2017-05-04 18:36:13 +01:00
|
|
|
"""
|
|
|
|
Run an event.
|
|
|
|
|
|
|
|
:return: nothing
|
|
|
|
"""
|
2017-04-25 16:45:34 +01:00
|
|
|
if self.canceled:
|
|
|
|
return
|
|
|
|
self.func(*self.args, **self.kwds)
|
2013-08-29 15:21:13 +01:00
|
|
|
|
2020-01-14 06:15:44 +00:00
|
|
|
def cancel(self) -> None:
|
2017-05-04 18:36:13 +01:00
|
|
|
"""
|
|
|
|
Cancel event.
|
|
|
|
|
|
|
|
:return: nothing
|
|
|
|
"""
|
2017-04-25 16:45:34 +01:00
|
|
|
# XXX not thread-safe
|
|
|
|
self.canceled = True
|
2013-08-29 15:21:13 +01:00
|
|
|
|
2017-04-25 16:45:34 +01:00
|
|
|
|
2019-10-23 17:31:07 +01:00
|
|
|
class EventLoop:
|
2017-05-04 18:36:13 +01:00
|
|
|
"""
|
|
|
|
Provides an event loop for running events.
|
|
|
|
"""
|
|
|
|
|
2020-01-14 06:15:44 +00:00
|
|
|
def __init__(self) -> None:
|
2017-05-04 18:36:13 +01:00
|
|
|
"""
|
|
|
|
Creates a EventLoop instance.
|
|
|
|
"""
|
2013-08-29 15:21:13 +01:00
|
|
|
self.lock = threading.RLock()
|
|
|
|
self.queue = []
|
|
|
|
self.eventnum = 0
|
|
|
|
self.timer = None
|
|
|
|
self.running = False
|
|
|
|
self.start = None
|
|
|
|
|
2020-01-14 06:15:44 +00:00
|
|
|
def __run_events(self) -> None:
|
2017-05-04 18:36:13 +01:00
|
|
|
"""
|
|
|
|
Run events.
|
|
|
|
|
|
|
|
:return: nothing
|
|
|
|
"""
|
2013-08-29 15:21:13 +01:00
|
|
|
schedule = False
|
|
|
|
while True:
|
|
|
|
with self.lock:
|
|
|
|
if not self.running or not self.queue:
|
|
|
|
break
|
2019-12-06 17:42:41 +00:00
|
|
|
now = time.monotonic()
|
2013-08-29 15:21:13 +01:00
|
|
|
if self.queue[0].time > now:
|
|
|
|
schedule = True
|
|
|
|
break
|
|
|
|
event = heapq.heappop(self.queue)
|
2018-10-12 00:01:17 +01:00
|
|
|
if event.time > now:
|
|
|
|
raise ValueError("invalid event time: %s > %s", event.time, now)
|
2013-08-29 15:21:13 +01:00
|
|
|
event.run()
|
2017-05-04 18:36:13 +01:00
|
|
|
|
2013-08-29 15:21:13 +01:00
|
|
|
with self.lock:
|
|
|
|
self.timer = None
|
|
|
|
if schedule:
|
|
|
|
self.__schedule_event()
|
|
|
|
|
2020-01-14 06:15:44 +00:00
|
|
|
def __schedule_event(self) -> None:
|
2017-05-04 18:36:13 +01:00
|
|
|
"""
|
|
|
|
Schedule event.
|
|
|
|
|
|
|
|
:return: nothing
|
|
|
|
"""
|
2013-08-29 15:21:13 +01:00
|
|
|
with self.lock:
|
2018-10-12 00:01:17 +01:00
|
|
|
if not self.running:
|
|
|
|
raise ValueError("scheduling event while not running")
|
2013-08-29 15:21:13 +01:00
|
|
|
if not self.queue:
|
|
|
|
return
|
2019-12-06 17:42:41 +00:00
|
|
|
delay = self.queue[0].time - time.monotonic()
|
2018-10-12 00:01:17 +01:00
|
|
|
if self.timer:
|
|
|
|
raise ValueError("timer was already set")
|
2017-04-25 16:45:34 +01:00
|
|
|
self.timer = Timer(delay, self.__run_events)
|
2013-08-29 15:21:13 +01:00
|
|
|
self.timer.daemon = True
|
|
|
|
self.timer.start()
|
|
|
|
|
2020-01-14 06:15:44 +00:00
|
|
|
def run(self) -> None:
|
2017-05-04 18:36:13 +01:00
|
|
|
"""
|
|
|
|
Start event loop.
|
|
|
|
|
|
|
|
:return: nothing
|
|
|
|
"""
|
2013-08-29 15:21:13 +01:00
|
|
|
with self.lock:
|
|
|
|
if self.running:
|
|
|
|
return
|
|
|
|
self.running = True
|
2019-12-06 17:42:41 +00:00
|
|
|
self.start = time.monotonic()
|
2013-08-29 15:21:13 +01:00
|
|
|
for event in self.queue:
|
|
|
|
event.time += self.start
|
|
|
|
self.__schedule_event()
|
|
|
|
|
2020-01-14 06:15:44 +00:00
|
|
|
def stop(self) -> None:
|
2017-05-04 18:36:13 +01:00
|
|
|
"""
|
|
|
|
Stop event loop.
|
|
|
|
|
|
|
|
:return: nothing
|
|
|
|
"""
|
2013-08-29 15:21:13 +01:00
|
|
|
with self.lock:
|
|
|
|
if not self.running:
|
|
|
|
return
|
|
|
|
self.queue = []
|
|
|
|
self.eventnum = 0
|
|
|
|
if self.timer is not None:
|
|
|
|
self.timer.cancel()
|
|
|
|
self.timer = None
|
|
|
|
self.running = False
|
|
|
|
self.start = None
|
|
|
|
|
2020-01-14 06:15:44 +00:00
|
|
|
def add_event(self, delaysec: float, func: Callable, *args: Any, **kwds: Any):
|
2017-05-04 18:36:13 +01:00
|
|
|
"""
|
|
|
|
Add an event to the event loop.
|
|
|
|
|
2020-01-16 19:00:57 +00:00
|
|
|
:param delaysec: delay in seconds for event
|
2017-05-04 18:36:13 +01:00
|
|
|
:param func: event function
|
|
|
|
:param args: event arguments
|
|
|
|
:param kwds: event keyword arguments
|
|
|
|
:return: created event
|
2020-01-17 00:12:01 +00:00
|
|
|
"""
|
2013-08-29 15:21:13 +01:00
|
|
|
with self.lock:
|
|
|
|
eventnum = self.eventnum
|
|
|
|
self.eventnum += 1
|
|
|
|
evtime = float(delaysec)
|
|
|
|
if self.running:
|
2019-12-06 17:42:41 +00:00
|
|
|
evtime += time.monotonic()
|
2017-04-25 16:45:34 +01:00
|
|
|
event = Event(eventnum, evtime, func, *args, **kwds)
|
2013-08-29 15:21:13 +01:00
|
|
|
|
|
|
|
if self.queue:
|
|
|
|
prevhead = self.queue[0]
|
|
|
|
else:
|
|
|
|
prevhead = None
|
|
|
|
|
|
|
|
heapq.heappush(self.queue, event)
|
|
|
|
head = self.queue[0]
|
|
|
|
if prevhead is not None and prevhead != head:
|
2015-11-05 17:36:44 +00:00
|
|
|
if self.timer is not None and self.timer.cancel():
|
2013-08-29 15:21:13 +01:00
|
|
|
self.timer = None
|
|
|
|
if self.running and self.timer is None:
|
|
|
|
self.__schedule_event()
|
|
|
|
return event
|