1 27
import signal
2 27
from contextlib import contextmanager
3 27
from collections import OrderedDict
4

5 27
import trio
6 27
from ._util import signal_raise, is_main_thread, ConflictDetector
7

8
# Discussion of signal handling strategies:
9
#
10
# - On Windows signals barely exist. There are no options; signal handlers are
11
#   the only available API.
12
#
13
# - On Linux signalfd is arguably the natural way. Semantics: signalfd acts as
14
#   an *alternative* signal delivery mechanism. The way you use it is to mask
15
#   out the relevant signals process-wide (so that they don't get delivered
16
#   the normal way), and then when you read from signalfd that actually counts
17
#   as delivering it (despite the mask). The problem with this is that we
18
#   don't have any reliable way to mask out signals process-wide -- the only
19
#   way to do that in Python is to call pthread_sigmask from the main thread
20
#   *before starting any other threads*, and as a library we can't really
21
#   impose that, and the failure mode is annoying (signals get delivered via
22
#   signal handlers whether we want them to or not).
23
#
24
# - on macOS/*BSD, kqueue is the natural way. Semantics: kqueue acts as an
25
#   *extra* signal delivery mechanism. Signals are delivered the normal
26
#   way, *and* are delivered to kqueue. So you want to set them to SIG_IGN so
27
#   that they don't end up pending forever (I guess?). I can't find any actual
28
#   docs on how masking and EVFILT_SIGNAL interact. I did see someone note
29
#   that if a signal is pending when the kqueue filter is added then you
30
#   *don't* get notified of that, which makes sense. But still, we have to
31
#   manipulate signal state (e.g. setting SIG_IGN) which as far as Python is
32
#   concerned means we have to do this from the main thread.
33
#
34
# So in summary, there don't seem to be any compelling advantages to using the
35
# platform-native signal notification systems; they're kinda nice, but it's
36
# simpler to implement the naive signal-handler-based system once and be
37
# done. (The big advantage would be if there were a reliable way to monitor
38
# for SIGCHLD from outside the main thread and without interfering with other
39
# libraries that also want to monitor for SIGCHLD. But there isn't. I guess
40
# kqueue might give us that, but in kqueue we don't need it, because kqueue
41
# can directly monitor for child process state changes.)
42

43

44 27
@contextmanager
45 13
def _signal_handler(signals, handler):
46 27
    original_handlers = {}
47 27
    try:
48 27
        for signum in set(signals):
49 27
            original_handlers[signum] = signal.signal(signum, handler)
50 27
        yield
51
    finally:
52 27
        for signum, original_handler in original_handlers.items():
53 27
            signal.signal(signum, original_handler)
54

55

56 27
class SignalReceiver:
57 27
    def __init__(self):
58
        # {signal num: None}
59 27
        self._pending = OrderedDict()
60 27
        self._lot = trio.lowlevel.ParkingLot()
61 27
        self._conflict_detector = ConflictDetector(
62
            "only one task can iterate on a signal receiver at a time"
63
        )
64 27
        self._closed = False
65

66 27
    def _add(self, signum):
67 27
        if self._closed:
68 27
            signal_raise(signum)
69
        else:
70 27
            self._pending[signum] = None
71 27
            self._lot.unpark()
72

73 27
    def _redeliver_remaining(self):
74
        # First make sure that any signals still in the delivery pipeline will
75
        # get redelivered
76 27
        self._closed = True
77

78
        # And then redeliver any that are sitting in pending. This is done
79
        # using a weird recursive construct to make sure we process everything
80
        # even if some of the handlers raise exceptions.
81 27
        def deliver_next():
82 27
            if self._pending:
83 27
                signum, _ = self._pending.popitem(last=False)
84 27
                try:
85 27
                    signal_raise(signum)
86
                finally:
87 27
                    deliver_next()
88

89 27
        deliver_next()
90

91
    # Helper for tests, not public or otherwise used
92 27
    def _pending_signal_count(self):
93 27
        return len(self._pending)
94

95 27
    def __aiter__(self):
96 27
        return self
97

98 27
    async def __anext__(self):
99 27
        if self._closed:
100 27
            raise RuntimeError("open_signal_receiver block already exited")
101
        # In principle it would be possible to support multiple concurrent
102
        # calls to __anext__, but doing it without race conditions is quite
103
        # tricky, and there doesn't seem to be any point in trying.
104 27
        with self._conflict_detector:
105 27
            if not self._pending:
106 27
                await self._lot.park()
107
            else:
108 27
                await trio.lowlevel.checkpoint()
109 27
            signum, _ = self._pending.popitem(last=False)
110 27
            return signum
111

112

113 27
@contextmanager
114 13
def open_signal_receiver(*signals):
115
    """A context manager for catching signals.
116

117
    Entering this context manager starts listening for the given signals and
118
    returns an async iterator; exiting the context manager stops listening.
119

120
    The async iterator blocks until a signal arrives, and then yields it.
121

122
    Note that if you leave the ``with`` block while the iterator has
123
    unextracted signals still pending inside it, then they will be
124
    re-delivered using Python's regular signal handling logic. This avoids a
125
    race condition when signals arrives just before we exit the ``with``
126
    block.
127

128
    Args:
129
      signals: the signals to listen for.
130

131
    Raises:
132
      TypeError: if no signals were provided.
133

134
      RuntimeError: if you try to use this anywhere except Python's main
135
          thread. (This is a Python limitation.)
136

137
    Example:
138

139
      A common convention for Unix daemons is that they should reload their
140
      configuration when they receive a ``SIGHUP``. Here's a sketch of what
141
      that might look like using :func:`open_signal_receiver`::
142

143
         with trio.open_signal_receiver(signal.SIGHUP) as signal_aiter:
144
             async for signum in signal_aiter:
145
                 assert signum == signal.SIGHUP
146
                 reload_configuration()
147

148
    """
149 27
    if not signals:
150 27
        raise TypeError("No signals were provided")
151

152 27
    if not is_main_thread():
153 27
        raise RuntimeError(
154
            "Sorry, open_signal_receiver is only possible when running in "
155
            "Python interpreter's main thread"
156
        )
157 27
    token = trio.lowlevel.current_trio_token()
158 27
    queue = SignalReceiver()
159

160 27
    def handler(signum, _):
161 27
        token.run_sync_soon(queue._add, signum, idempotent=True)
162

163 27
    try:
164 27
        with _signal_handler(signals, handler):
165 27
            yield queue
166
    finally:
167 27
        queue._redeliver_remaining()

Read our documentation on viewing source code .

Loading