xiio

really simple async runtime
git clone https://git.ce9e.org/xiio.git

commit
d241b8a53cab1f54563072f953803756c32cbf02
parent
37911ea6ed293cd987fad904b07ab4b027857604
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2026-04-12 16:45
add subscribe_signals()

Diffstat

A tests/test_signals.py 21 +++++++++++++++++++++
M xiio/__init__.py 1 +
A xiio/signals.py 41 +++++++++++++++++++++++++++++++++++++++++

3 files changed, 63 insertions, 0 deletions


diff --git a/tests/test_signals.py b/tests/test_signals.py

@@ -0,0 +1,21 @@
   -1     1 import os
   -1     2 import signal
   -1     3 
   -1     4 import xiio
   -1     5 from tests.utils import XiioTestCase
   -1     6 
   -1     7 
   -1     8 class TestSubscribeSignals(XiioTestCase):
   -1     9     async def test_subscribe_signals(self):
   -1    10         result = []
   -1    11 
   -1    12         async def send_signals():
   -1    13             await xiio.sleep(0.1)
   -1    14             os.kill(os.getpid(), signal.Signals.SIGUSR1)
   -1    15 
   -1    16         async with xiio.TaskGroup() as tg:
   -1    17             async with xiio.subscribe_signals(signal.Signals.SIGUSR1) as signal_fd:
   -1    18                 tg.add_task(send_signals())
   -1    19                 with self.assert_duration(0.1):
   -1    20                     result = await xiio.read(signal_fd, 1)
   -1    21                 self.assertEqual(result[0], signal.Signals.SIGUSR1)

diff --git a/xiio/__init__.py b/xiio/__init__.py

@@ -9,3 +9,4 @@ from .multiplex import TaskGroup  # noqa
    9     9 from .multiplex import gather  # noqa
   10    10 from .multiplex import timeout  # noqa
   11    11 from .subprocess import run_process  # noqa
   -1    12 from .signals import subscribe_signals  # noqa

diff --git a/xiio/signals.py b/xiio/signals.py

@@ -0,0 +1,41 @@
   -1     1 """
   -1     2 Signals can only be registered in the main thread and there can only be a
   -1     3 single wakeup FD. As a consequence, `subscribe_signals()` should only be used
   -1     4 once, early on in the application.
   -1     5 """
   -1     6 
   -1     7 import contextlib
   -1     8 import signal
   -1     9 import socket
   -1    10 from collections.abc import AsyncGenerator
   -1    11 from collections.abc import Generator
   -1    12 
   -1    13 
   -1    14 def noop(*args):
   -1    15     pass
   -1    16 
   -1    17 
   -1    18 @contextlib.contextmanager
   -1    19 def signal_fd() -> Generator[int, None, None]:
   -1    20     r, w = socket.socketpair()
   -1    21     r.setblocking(False)  # noqa
   -1    22     w.setblocking(False)  # noqa
   -1    23 
   -1    24     old = signal.set_wakeup_fd(w.fileno())
   -1    25     try:
   -1    26         yield r.fileno()
   -1    27     finally:
   -1    28         signal.set_wakeup_fd(old)
   -1    29         r.close()
   -1    30         w.close()
   -1    31 
   -1    32 
   -1    33 @contextlib.asynccontextmanager
   -1    34 async def subscribe_signals(*sigs: int) -> AsyncGenerator[int, None]:
   -1    35     with signal_fd() as fd:
   -1    36         old_sigs = {sig: signal.signal(sig, noop) for sig in sigs}
   -1    37         try:
   -1    38             yield fd
   -1    39         finally:
   -1    40             for sig, old in old_sigs.items():
   -1    41                 signal.signal(sig, old)