X-Git-Url: https://git.llucax.com/software/pymin.git/blobdiff_plain/c3862dc0f135a52327fbf9511c22392555e84f3f..816f9f77182018dd0b92b0298eb4d46845d81f61:/pymin/eventloop.py diff --git a/pymin/eventloop.py b/pymin/eventloop.py index b6be5a1..5e4bec3 100644 --- a/pymin/eventloop.py +++ b/pymin/eventloop.py @@ -10,8 +10,9 @@ import select import errno import signal from select import POLLIN, POLLPRI, POLLERR +import logging ; log = logging.getLogger('pymin.eventloop') -__ALL__ = ('EventLoop', 'LoopInterruptedError') +__all__ = ('EventLoop', 'LoopInterruptedError') class LoopInterruptedError(RuntimeError): r""" @@ -37,16 +38,16 @@ class LoopInterruptedError(RuntimeError): r"str(obj) -> String representation." return 'Loop interrupted: %s' % self.select_error -# Flag to know if a timer was expired -timeout = False +# Flag to know if a signal was caught +signals = list() # Alarm Signal handler -def alarm_handler(signum, stack_frame): - global timeout - timeout = True +def signal_handler(signum, stack_frame): + global signals + signals.append(signum) class EventLoop: - r"""EventLoop(file[, timer[, handler[, timer_handler]]]) -> EventLoop. + r"""EventLoop(file[, handler[, signals]]]) -> EventLoop. This class implements a simple event loop based on select module. It "listens" to activity a single 'file' object (a file, a pipe, @@ -54,8 +55,11 @@ class EventLoop: function (or the handle() method if you prefer subclassing) every time the file is ready for reading (or has an error). - If a 'timer' is supplied, then the timer_handler() function object - (or the handle_timer() method) is called every 'timer' seconds. + 'signals' is a dictionary with signals to be handled by the loop, + where keys are signal numbers and values are callbacks (which takes + 2 arguments, first the event loop that captured the signal, and then + the captured signal number). Callbacks can be None if all signals + are handled by the handle_signal() member function. This is a really simple example of usage using a hanlder callable: @@ -75,36 +79,56 @@ class EventLoop: >>> class Test(EventLoop): >>> def handle(self): >>> data = os.read(self.fileno, 100) - >>> if data == 'q\n': - >>> self.stop() - >>> else: - >>> os.write(1, 'Received message: %r\n' % data) - >>> def handle_timer(self): - >>> print time.strftime('%c') - >>> p = Test(0, timer=5) + >>> os.write(1, 'Received message: %r\n' % data) + >>> def handle_signal(self, signum): + >>> os.write(1, 'Signal %d received, stopping\n' % signum) + >>> self.stop() + >>> p = Test(0, signals={signal.SIGTERM: None, signal.SIGINT: None}) >>> p.loop() - This example loops until the user enters a single "q", when stop() - is called and the event loop is exited. + This example loops until the user enter interrupts the program (by + pressing Ctrl-C) or untile the program is terminated by a TERM signal + (kill) when stop() is called and the event loop is exited. """ - def __init__(self, file, handler=None, timer=None, timer_handler=None): + def __init__(self, file, handler=None, signals=None): r"""Initialize the EventLoop object. See EventLoop class documentation for more info. """ + log.debug(u'EventLoop(%r, %r, %r)', file, handler, signals) self.poll = select.poll() self._stop = False self.__register(file) - self.timer = timer self.handler = handler - self.timer_handler = timer_handler + self.signals = dict() + if signals is None: + signals = dict() + for (signum, sighandler) in signals.items(): + self.set_signal(signum, sighandler) def __register(self, file): r"__register(file) -> None :: Register a new file for polling." self._file = file self.poll.register(self.fileno, POLLIN | POLLPRI | POLLERR) + def set_signal(self, signum, sighandler): + prev = self.signals.get(signum, None) + # If the signal was not already handled, handle it + if signum not in self.signals: + signal.signal(signum, signal_handler) + self.signals[signum] = sighandler + return prev + + def get_signal_handler(self, signum): + return self.signals[signum] + + def unset_signal(self, signum): + prev = self.signals[signum] + # Restore the default handler + signal.signal(signum, signal.SIG_DFL) + return prev + def set_file(self, file): r"""set_file(file) -> None :: New file object to be monitored @@ -134,6 +158,7 @@ class EventLoop: The event loop will be interrupted as soon as the current handler finishes. """ + log.debug(u'EventLoop.stop()') self._stop = True def loop(self, once=False): @@ -142,42 +167,49 @@ class EventLoop: Wait for events and handle then when they arrive. If once is True, then only 1 event is processed and then this method returns. """ - # Flag modified by the signal handler - global timeout - # If we use a timer, we set up the signal - if self.timer is not None: - signal.signal(signal.SIGALRM, alarm_handler) - signal.alarm(self.timer) + log.debug(u'EventLoop.loop(%s)', once) + # List of pending signals + global signals while True: try: + log.debug(u'EventLoop.loop: polling') res = self.poll.poll() except select.error, e: - # The error is not an interrupt caused by the alarm, then raise - if e.args[0] != errno.EINTR or not timeout: + # The error is not an interrupt caused by a signal, then raise + if e.args[0] != errno.EINTR or not signals: raise LoopInterruptedError(e) - # There was a timeout, so execute the timer handler - if timeout: - timeout = False - self.handle_timer() - signal.alarm(self.timer) - # Not a timeout, execute the regular handler - else: + # If we have signals to process, we just do it + have_signals = bool(signals) + while signals: + signum = signals.pop(0) + log.debug(u'EventLoop.loop: processing signal %d...', signum) + self.handle_signal(signum) + # No signals to process, execute the regular handler + if not have_signals: + log.debug(u'EventLoop.loop: processing event...') self.handle() # Look if we have to stop if self._stop or once: + log.debug(u'EventLoop.loop: stopped') self._stop = False break def handle(self): - r"handle() -> None :: Abstract method to be overriden to handle events." + r"handle() -> None :: Handle file descriptor events." self.handler(self) - def handle_timer(self): - r"handle() -> None :: Abstract method to be overriden to handle events." - self.timer_handler(self) + def handle_signal(self, signum): + r"handle_signal(signum) -> None :: Handles signals." + self.signals[signum](self, signum) if __name__ == '__main__': + logging.basicConfig( + level = logging.DEBUG, + format = '%(asctime)s %(levelname)-8s %(message)s', + datefmt = '%H:%M:%S', + ) + import os import time @@ -187,23 +219,21 @@ if __name__ == '__main__': p = EventLoop(0, handle) - os.write(1, 'Say something once: ') + os.write(1, 'Say something once:\n') p.loop(once=True) os.write(1, 'Great!\n') class Test(EventLoop): def handle(self): data = os.read(self.fileno, 100) - if data == 'q\n': - self.stop() - else: - os.write(1, 'Received message: %r\n' % data) - def handle_timer(self): - print time.strftime('%c') + os.write(1, 'Received message: %r\n' % data) + def handle_signal(self, signum): + os.write(1, 'Signal %d received, stopping\n' % signum) + self.stop() - p = Test(0, timer=5) + p = Test(0, signals={signal.SIGTERM: None, signal.SIGINT: None}) - os.write(1, 'Say a lot of things, then press write just "q" to stop: ') + os.write(1, 'Say a lot of things, then press Ctrl-C or kill me to stop: ') p.loop() os.write(1, 'Ok, bye!\n')