"I'm not proud of being a congenital pain in the ass. But I will take money for it."

POSIX signals in Python

Thu 28 September 2017 | -- (permalink)

I mostly try to avoid programming with POSIX signals, in part because I was spoiled in my youth: even after decades of incremental improvements, POSIX signals have not really caught up to what ITS had back in the 1970s. But sometimes they're, if not the right tool, the least bad tool for a particular job. Case in point would be any program which supervises enough child processes that it needs to interleave process handling and I/O.

In C, this is tedious, but straightforward. In Python it gets more interesting, both because of Python's limited support for POSIX signals and because in Python it's easier to wrap a sane abstraction layer around the whole untidy mess.

YYMV, but to me, the obvious model for how POSIX signals behave is the Event abstraction, as used in threading.Event, tornado.locks.Event, et cetera. This is a very simple abstraction which one can use as part of the control structure for a simple event loop, and its semantics are a good fit for POSIX signals.

So here's what a basic implementation might look like:

    import signal, os

    class Event(set):

        def __init__(self):
            super(Event, self).__init__()
            self.throw = False
            self.breakout = type("EventBreakout", (Exception,), {})

        def handler(self, sig, ignored = None):
            self.add(sig)
            if self.throw:
                self.throw = False
                raise self.breakout

        def wait(self, timeout = None):
            if not self.is_set():
                try:
                    self.throw = True
                    signal.alarm(0 if timeout is None else timeout)
                    while not self.is_set():
                        signal.pause()
                except self.breakout:
                    pass
                finally:
                    self.throw = False
                    signal.alarm(0)

        # set.clear() already does what we want, no need to override it.

        def set(self):
            self.throw = False
            self.handler(None)

        def is_set(self):
            return bool(self)

The critical features are:

  • The use of raise with a private exception class to effect non-local exits, and

  • The use of a flag to restrict raising of the exception to situations where it does something helpful.

Paint and racing stripes have been left as an exercise for the reader.