]> git.llucax.com Git - software/pymin.git/blobdiff - pymin/eventloop.py
Add die() function to pymind for fatal errors.
[software/pymin.git] / pymin / eventloop.py
index 06922a72fe9290bcfb5133700e49ae6187d0dc3c..5e4bec3b50be0b26026c3a2634ccc2064e6199ef 100644 (file)
@@ -7,9 +7,12 @@ Please see EventLoop class documentation for more info.
 """
 
 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"""
@@ -35,8 +38,16 @@ class LoopInterruptedError(RuntimeError):
         r"str(obj) -> String representation."
         return 'Loop interrupted: %s' % self.select_error
 
+# Flag to know if a signal was caught
+signals = list()
+
+# Alarm Signal handler
+def signal_handler(signum, stack_frame):
+    global signals
+    signals.append(signum)
+
 class EventLoop:
-    r"""EventLoop(file[, handler]) -> EventLoop instance
+    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,
@@ -44,9 +55,15 @@ class EventLoop:
     function (or the handle() method if you prefer subclassing) every
     time the file is ready for reading (or has an error).
 
+    '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:
 
-    >>> import os 
+    >>> import os
     >>> def handle(event_loop):
             data = os.read(event_loop.fileno, 100)
             os.write(1, 'Received message: %r\n' % data)
@@ -62,32 +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)
-    >>> p = Test(0)
+    >>>         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):
+    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.handler = 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
 
@@ -117,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):
@@ -125,26 +167,51 @@ 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.
         """
+        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:
-                raise LoopInterruptedError(e)
-            if self.handler is not None:
-                self.handler(self)
-            else:
+                # The error is not an interrupt caused by a signal, then raise
+                if e.args[0] != errno.EINTR or not signals:
+                    raise LoopInterruptedError(e)
+            # 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."
-        raise NotImplementedError
+        r"handle() -> None :: Handle file descriptor events."
+        self.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
 
     def handle(event_loop):
         data = os.read(event_loop.fileno, 100)
@@ -152,21 +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)
+            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)
+    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')