]> git.llucax.com Git - software/pymin.git/blob - pymin/eventloop.py
Add configuration and command-line option framework.
[software/pymin.git] / pymin / eventloop.py
1 # vim: set encoding=utf-8 et sw=4 sts=4 :
2
3 r"""
4 A simple event loop.
5
6 Please see EventLoop class documentation for more info.
7 """
8
9 import select
10 import errno
11 import signal
12 from select import POLLIN, POLLPRI, POLLERR
13 import logging ; log = logging.getLogger('pymin.eventloop')
14
15 __all__ = ('EventLoop', 'LoopInterruptedError')
16
17 class LoopInterruptedError(RuntimeError):
18     r"""
19     LoopInterruptedError(select_error) -> LoopInterruptedError instance.
20
21     This class is raised when the event loop is interrupted in an unexpected
22     way. It wraps a select error, which can be accessed using the 'select_error'
23     attribute.
24     """
25
26     def __init__(self, select_error):
27         r"""Initialize the object.
28
29         See the class documentation for more info.
30         """
31         self.select_error = select_error
32
33     def __repr__(self):
34         r"repr(obj) -> Object representation."
35         return 'LoopInterruptedError(select_error=%r)' % self.select_error
36
37     def __str__(self):
38         r"str(obj) -> String representation."
39         return 'Loop interrupted: %s' % self.select_error
40
41 # Flag to know if a signal was caught
42 signals = list()
43
44 # Alarm Signal handler
45 def signal_handler(signum, stack_frame):
46     global signals
47     signals.append(signum)
48
49 class EventLoop:
50     r"""EventLoop(file[, handler[, signals]]]) -> EventLoop.
51
52     This class implements a simple event loop based on select module.
53     It "listens" to activity a single 'file' object (a file, a pipe,
54     a socket, or even a simple file descriptor) and calls a 'handler'
55     function (or the handle() method if you prefer subclassing) every
56     time the file is ready for reading (or has an error).
57
58     'signals' is a dictionary with signals to be handled by the loop,
59     where keys are signal numbers and values are callbacks (which takes
60     2 arguments, first the event loop that captured the signal, and then
61     the captured signal number). Callbacks can be None if all signals
62     are handled by the handle_signal() member function.
63
64     This is a really simple example of usage using a hanlder callable:
65
66     >>> import os
67     >>> def handle(event_loop):
68             data = os.read(event_loop.fileno, 100)
69             os.write(1, 'Received message: %r\n' % data)
70     >>> p = EventLoop(0, handle)
71     >>> p.loop(once=True)
72
73     In this example only one event is handled (see the 'once' argument
74     of loop).
75
76     A more complex example, making a subclass and explicitly stopping
77     the loop, looks something like this:
78
79     >>> class Test(EventLoop):
80     >>>     def handle(self):
81     >>>         data = os.read(self.fileno, 100)
82     >>>         os.write(1, 'Received message: %r\n' % data)
83     >>>     def handle_signal(self, signum):
84     >>>         os.write(1, 'Signal %d received, stopping\n' % signum)
85     >>>         self.stop()
86     >>> p = Test(0, signals={signal.SIGTERM: None, signal.SIGINT: None})
87     >>> p.loop()
88
89     This example loops until the user enter interrupts the program (by
90     pressing Ctrl-C) or untile the program is terminated by a TERM signal
91     (kill) when stop() is called and the event loop is exited.
92     """
93
94     def __init__(self, file, handler=None, signals=None):
95         r"""Initialize the EventLoop object.
96
97         See EventLoop class documentation for more info.
98         """
99         log.debug(u'EventLoop(%r, %r, %r)', file, handler, signals)
100         self.poll = select.poll()
101         self._stop = False
102         self.__register(file)
103         self.handler = handler
104         self.signals = dict()
105         if signals is None:
106             signals = dict()
107         for (signum, sighandler) in signals.items():
108             self.set_signal(signum, sighandler)
109
110     def __register(self, file):
111         r"__register(file) -> None :: Register a new file for polling."
112         self._file = file
113         self.poll.register(self.fileno, POLLIN | POLLPRI | POLLERR)
114
115     def set_signal(self, signum, sighandler):
116         prev = self.signals.get(signum, None)
117         # If the signal was not already handled, handle it
118         if signum not in self.signals:
119             signal.signal(signum, signal_handler)
120         self.signals[signum] = sighandler
121         return prev
122
123     def get_signal_handler(self, signum):
124         return self.signals[signum]
125
126     def unset_signal(self, signum):
127         prev = self.signals[signum]
128         # Restore the default handler
129         signal.signal(signum, signal.SIG_DFL)
130         return prev
131
132     def set_file(self, file):
133         r"""set_file(file) -> None :: New file object to be monitored
134
135         Unregister the previous file object being monitored and register
136         a new one.
137         """
138         self.poll.unregister(self.fileno)
139         self.__register(file)
140
141     def get_file(self):
142         r"get_file() -> file object/int :: Get the current file object/fd."
143         return self._file
144
145     file = property(get_file, set_file, doc='File object (or descriptor)')
146
147     def get_fileno(self):
148         r"get_fileno() -> int :: Get the current file descriptor"
149         if hasattr(self.file, 'fileno'):
150             return self.file.fileno()
151         return self.file
152
153     fileno = property(get_fileno, doc='File descriptor (never a file object)')
154
155     def stop(self):
156         r"""stop() -> None :: Stop the event loop.
157
158         The event loop will be interrupted as soon as the current handler
159         finishes.
160         """
161         log.debug(u'EventLoop.stop()')
162         self._stop = True
163
164     def loop(self, once=False):
165         r"""loop([once]) -> None :: Wait for events.
166
167         Wait for events and handle then when they arrive. If once is True,
168         then only 1 event is processed and then this method returns.
169         """
170         log.debug(u'EventLoop.loop(%s)', once)
171         # List of pending signals
172         global signals
173         while True:
174             try:
175                 log.debug(u'EventLoop.loop: polling')
176                 res = self.poll.poll()
177             except select.error, e:
178                 # The error is not an interrupt caused by a signal, then raise
179                 if e.args[0] != errno.EINTR or not signals:
180                     raise LoopInterruptedError(e)
181             # If we have signals to process, we just do it
182             have_signals = bool(signals)
183             while signals:
184                 signum = signals.pop(0)
185                 log.debug(u'EventLoop.loop: processing signal %d...', signum)
186                 self.handle_signal(signum)
187             # No signals to process, execute the regular handler
188             if not have_signals:
189                 log.debug(u'EventLoop.loop: processing event...')
190                 self.handle()
191             # Look if we have to stop
192             if self._stop or once:
193                 log.debug(u'EventLoop.loop: stopped')
194                 self._stop = False
195                 break
196
197     def handle(self):
198         r"handle() -> None :: Handle file descriptor events."
199         self.handler(self)
200
201     def handle_signal(self, signum):
202         r"handle_signal(signum) -> None :: Handles signals."
203         self.signals[signum](self, signum)
204
205 if __name__ == '__main__':
206
207     logging.basicConfig(
208         level   = logging.DEBUG,
209         format  = '%(asctime)s %(levelname)-8s %(message)s',
210         datefmt = '%H:%M:%S',
211     )
212
213     import os
214     import time
215
216     def handle(event_loop):
217         data = os.read(event_loop.fileno, 100)
218         os.write(1, 'Received message: %r\n' % data)
219
220     p = EventLoop(0, handle)
221
222     os.write(1, 'Say something once:\n')
223     p.loop(once=True)
224     os.write(1, 'Great!\n')
225
226     class Test(EventLoop):
227         def handle(self):
228             data = os.read(self.fileno, 100)
229             os.write(1, 'Received message: %r\n' % data)
230         def handle_signal(self, signum):
231             os.write(1, 'Signal %d received, stopping\n' % signum)
232             self.stop()
233
234     p = Test(0, signals={signal.SIGTERM: None, signal.SIGINT: None})
235
236     os.write(1, 'Say a lot of things, then press Ctrl-C or kill me to stop: ')
237     p.loop()
238     os.write(1, 'Ok, bye!\n')
239