mirror of
https://github.com/avitoras/telegram-tui.git
synced 2026-02-04 11:56:25 +00:00
278 lines
8.9 KiB
Python
278 lines
8.9 KiB
Python
# Urwid main loop code using ZeroMQ queues
|
|
# Copyright (C) 2019 Dave Jones
|
|
#
|
|
# This library 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 2.1 of the License, or (at your option) any later version.
|
|
#
|
|
# This library 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 this library; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
# Urwid web site: https://urwid.org/
|
|
|
|
"""ZeroMQ based urwid EventLoop implementation.
|
|
|
|
`ZeroMQ <https://zeromq.org>`_ library is required.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import errno
|
|
import heapq
|
|
import logging
|
|
import os
|
|
import time
|
|
import typing
|
|
from itertools import count
|
|
|
|
import zmq
|
|
|
|
from .abstract_loop import EventLoop, ExitMainLoop
|
|
|
|
if typing.TYPE_CHECKING:
|
|
import io
|
|
from collections.abc import Callable
|
|
from concurrent.futures import Executor, Future
|
|
|
|
from typing_extensions import ParamSpec
|
|
|
|
ZMQAlarmHandle = typing.TypeVar("ZMQAlarmHandle")
|
|
_T = typing.TypeVar("_T")
|
|
_Spec = ParamSpec("_Spec")
|
|
|
|
|
|
class ZMQEventLoop(EventLoop):
|
|
"""
|
|
This class is an urwid event loop for `ZeroMQ`_ applications. It is very
|
|
similar to :class:`SelectEventLoop`, supporting the usual :meth:`alarm`
|
|
events and file watching (:meth:`watch_file`) capabilities, but also
|
|
incorporates the ability to watch zmq queues for events
|
|
(:meth:`watch_queue`).
|
|
|
|
.. _ZeroMQ: https://zeromq.org/
|
|
"""
|
|
|
|
_alarm_break = count()
|
|
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
|
|
self._did_something = True
|
|
self._alarms: list[tuple[float, int, Callable[[], typing.Any]]] = []
|
|
self._poller = zmq.Poller()
|
|
self._queue_callbacks: dict[int, Callable[[], typing.Any]] = {}
|
|
self._idle_handle = 0
|
|
self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
|
|
|
|
def run_in_executor(
|
|
self,
|
|
executor: Executor,
|
|
func: Callable[_Spec, _T],
|
|
*args: _Spec.args,
|
|
**kwargs: _Spec.kwargs,
|
|
) -> Future[_T]:
|
|
"""Run callable in executor.
|
|
|
|
:param executor: Executor to use for running the function
|
|
:type executor: concurrent.futures.Executor
|
|
:param func: function to call
|
|
:type func: Callable
|
|
:param args: positional arguments to function
|
|
:type args: object
|
|
:param kwargs: keyword arguments to function
|
|
:type kwargs: object
|
|
:return: future object for the function call outcome.
|
|
:rtype: concurrent.futures.Future
|
|
"""
|
|
return executor.submit(func, *args, **kwargs)
|
|
|
|
def alarm(self, seconds: float, callback: Callable[[], typing.Any]) -> ZMQAlarmHandle:
|
|
"""
|
|
Call *callback* a given time from now. No parameters are passed to
|
|
callback. Returns a handle that may be passed to :meth:`remove_alarm`.
|
|
|
|
:param float seconds:
|
|
floating point time to wait before calling callback.
|
|
|
|
:param callback:
|
|
function to call from event loop.
|
|
"""
|
|
handle = (time.time() + seconds, next(self._alarm_break), callback)
|
|
heapq.heappush(self._alarms, handle)
|
|
return handle
|
|
|
|
def remove_alarm(self, handle: ZMQAlarmHandle) -> bool:
|
|
"""
|
|
Remove an alarm. Returns ``True`` if the alarm exists, ``False``
|
|
otherwise.
|
|
"""
|
|
try:
|
|
self._alarms.remove(handle)
|
|
heapq.heapify(self._alarms)
|
|
|
|
except ValueError:
|
|
return False
|
|
|
|
return True
|
|
|
|
def watch_queue(
|
|
self,
|
|
queue: zmq.Socket,
|
|
callback: Callable[[], typing.Any],
|
|
flags: int = zmq.POLLIN,
|
|
) -> zmq.Socket:
|
|
"""
|
|
Call *callback* when zmq *queue* has something to read (when *flags* is
|
|
set to ``POLLIN``, the default) or is available to write (when *flags*
|
|
is set to ``POLLOUT``). No parameters are passed to the callback.
|
|
Returns a handle that may be passed to :meth:`remove_watch_queue`.
|
|
|
|
:param queue:
|
|
The zmq queue to poll.
|
|
|
|
:param callback:
|
|
The function to call when the poll is successful.
|
|
|
|
:param int flags:
|
|
The condition to monitor on the queue (defaults to ``POLLIN``).
|
|
"""
|
|
if queue in self._queue_callbacks:
|
|
raise ValueError(f"already watching {queue!r}")
|
|
self._poller.register(queue, flags)
|
|
self._queue_callbacks[queue] = callback
|
|
return queue
|
|
|
|
def watch_file(
|
|
self,
|
|
fd: int | io.TextIOWrapper,
|
|
callback: Callable[[], typing.Any],
|
|
flags: int = zmq.POLLIN,
|
|
) -> io.TextIOWrapper:
|
|
"""
|
|
Call *callback* when *fd* has some data to read. No parameters are
|
|
passed to the callback. The *flags* are as for :meth:`watch_queue`.
|
|
Returns a handle that may be passed to :meth:`remove_watch_file`.
|
|
|
|
:param fd:
|
|
The file-like object, or fileno to monitor.
|
|
|
|
:param callback:
|
|
The function to call when the file has data available.
|
|
|
|
:param int flags:
|
|
The condition to monitor on the file (defaults to ``POLLIN``).
|
|
"""
|
|
if isinstance(fd, int):
|
|
fd = os.fdopen(fd)
|
|
self._poller.register(fd, flags)
|
|
self._queue_callbacks[fd.fileno()] = callback
|
|
return fd
|
|
|
|
def remove_watch_queue(self, handle: zmq.Socket) -> bool:
|
|
"""
|
|
Remove a queue from background polling. Returns ``True`` if the queue
|
|
was being monitored, ``False`` otherwise.
|
|
"""
|
|
try:
|
|
try:
|
|
self._poller.unregister(handle)
|
|
finally:
|
|
self._queue_callbacks.pop(handle, None)
|
|
|
|
except KeyError:
|
|
return False
|
|
|
|
return True
|
|
|
|
def remove_watch_file(self, handle: io.TextIOWrapper) -> bool:
|
|
"""
|
|
Remove a file from background polling. Returns ``True`` if the file was
|
|
being monitored, ``False`` otherwise.
|
|
"""
|
|
try:
|
|
try:
|
|
self._poller.unregister(handle)
|
|
finally:
|
|
self._queue_callbacks.pop(handle.fileno(), None)
|
|
|
|
except KeyError:
|
|
return False
|
|
|
|
return True
|
|
|
|
def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
|
|
"""
|
|
Add a *callback* to be executed when the event loop detects it is idle.
|
|
Returns a handle that may be passed to :meth:`remove_enter_idle`.
|
|
"""
|
|
self._idle_handle += 1
|
|
self._idle_callbacks[self._idle_handle] = callback
|
|
return self._idle_handle
|
|
|
|
def remove_enter_idle(self, handle: int) -> bool:
|
|
"""
|
|
Remove an idle callback. Returns ``True`` if *handle* was removed,
|
|
``False`` otherwise.
|
|
"""
|
|
try:
|
|
del self._idle_callbacks[handle]
|
|
except KeyError:
|
|
return False
|
|
|
|
return True
|
|
|
|
def _entering_idle(self) -> None:
|
|
for callback in list(self._idle_callbacks.values()):
|
|
callback()
|
|
|
|
def run(self) -> None:
|
|
"""
|
|
Start the event loop. Exit the loop when any callback raises an
|
|
exception. If :exc:`ExitMainLoop` is raised, exit cleanly.
|
|
"""
|
|
with contextlib.suppress(ExitMainLoop):
|
|
while True:
|
|
try:
|
|
self._loop()
|
|
except zmq.error.ZMQError as exc: # noqa: PERF203
|
|
if exc.errno != errno.EINTR:
|
|
raise
|
|
|
|
def _loop(self) -> None:
|
|
"""
|
|
A single iteration of the event loop.
|
|
"""
|
|
state = "wait" # default state not expecting any action
|
|
if self._alarms or self._did_something:
|
|
timeout = 0
|
|
if self._alarms:
|
|
state = "alarm"
|
|
timeout = max(0.0, self._alarms[0][0] - time.time())
|
|
if self._did_something and (not self._alarms or (self._alarms and timeout > 0)):
|
|
state = "idle"
|
|
timeout = 0
|
|
ready = dict(self._poller.poll(timeout * 1000))
|
|
else:
|
|
ready = dict(self._poller.poll())
|
|
|
|
if not ready:
|
|
if state == "idle":
|
|
self._entering_idle()
|
|
self._did_something = False
|
|
elif state == "alarm":
|
|
_due, _tie_break, callback = heapq.heappop(self._alarms)
|
|
callback()
|
|
self._did_something = True
|
|
|
|
for queue in ready:
|
|
self._queue_callbacks[queue]()
|
|
self._did_something = True
|