]> git.llucax.com Git - software/pymin.git/blobdiff - pymin/eventloop.py
Move handler decorator help checking to avoid an extra level in the stack trace.
[software/pymin.git] / pymin / eventloop.py
index 06922a72fe9290bcfb5133700e49ae6187d0dc3c..0f720120730c1326c5ed852e35883ca96c18b344 100644 (file)
@@ -7,7 +7,10 @@ Please see EventLoop class documentation for more info.
 """
 
 import select
 """
 
 import select
+import errno
+import signal
 from select import POLLIN, POLLPRI, POLLERR
 from select import POLLIN, POLLPRI, POLLERR
+import logging ; log = logging.getLogger('pymin.eventloop')
 
 __ALL__ = ('EventLoop', 'LoopInterruptedError')
 
 
 __ALL__ = ('EventLoop', 'LoopInterruptedError')
 
@@ -35,8 +38,16 @@ class LoopInterruptedError(RuntimeError):
         r"str(obj) -> String representation."
         return 'Loop interrupted: %s' % self.select_error
 
         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:
 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,
 
     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).
 
     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:
 
     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)
     >>> 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)
     >>> 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()
 
     >>> 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.
         """
         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.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 __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
 
     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.
         """
         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):
         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.
         """
         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:
         while True:
             try:
+                log.debug(u'EventLoop.loop: polling')
                 res = self.poll.poll()
             except select.error, e:
                 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()
                 self.handle()
+            # Look if we have to stop
             if self._stop or once:
             if self._stop or once:
+                log.debug(u'EventLoop.loop: stopped')
                 self._stop = False
                 break
 
     def handle(self):
                 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__':
 
 
 if __name__ == '__main__':
 
+    logging.basicConfig(
+        level   = logging.DEBUG,
+        format  = '%(asctime)s %(levelname)-8s %(message)s',
+        datefmt = '%H:%M:%S',
+    )
+
     import os
     import os
+    import time
 
     def handle(event_loop):
         data = os.read(event_loop.fileno, 100)
 
     def handle(event_loop):
         data = os.read(event_loop.fileno, 100)
@@ -152,21 +219,21 @@ if __name__ == '__main__':
 
     p = EventLoop(0, handle)
 
 
     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)
     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')
 
     p.loop()
     os.write(1, 'Ok, bye!\n')