]> git.llucax.com Git - software/pymin.git/blobdiff - pymin/eventloop.py
Add configuration and command-line option framework.
[software/pymin.git] / pymin / eventloop.py
index 86b89324eae7cfeff644cf1623d02e827185325a..5e4bec3b50be0b26026c3a2634ccc2064e6199ef 100644 (file)
@@ -10,8 +10,9 @@ import select
 import errno
 import signal
 from select import POLLIN, POLLPRI, POLLERR
 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"""
 
 class LoopInterruptedError(RuntimeError):
     r"""
@@ -37,16 +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 timer was expired
-timeout = False
+# Flag to know if a signal was caught
+signals = list()
 
 # Alarm Signal handler
 
 # 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:
 
 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,
 
     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).
 
     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:
 
 
     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)
     >>> 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()
 
     >>> 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.
         """
         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.poll = select.poll()
         self._stop = False
         self.__register(file)
-        self.timer = timer
         self.handler = handler
         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 __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
 
@@ -134,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):
@@ -142,43 +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.
         """
         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)
-            self.handle_timer()
-            signal.alarm(self.timer)
+        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:
-                # 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)
                     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:
                 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):
                 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)
 
         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__':
 
 
 if __name__ == '__main__':
 
+    logging.basicConfig(
+        level   = logging.DEBUG,
+        format  = '%(asctime)s %(levelname)-8s %(message)s',
+        datefmt = '%H:%M:%S',
+    )
+
     import os
     import time
 
     import os
     import time
 
@@ -188,23 +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)
-        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')
 
     p.loop()
     os.write(1, 'Ok, bye!\n')