]> git.llucax.com Git - software/pymin.git/blob - pymin/eventloop.py
Merge commit 'llucax/logging' into logging
[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 timer was expired
42 timeout = False
43
44 # Alarm Signal handler
45 def alarm_handler(signum, stack_frame):
46     global timeout
47     timeout = True
48
49 class EventLoop:
50     r"""EventLoop(file[, timer[, handler[, timer_handler]]]) -> 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     If a 'timer' is supplied, then the timer_handler() function object
59     (or the handle_timer() method) is called every 'timer' seconds.
60
61     This is a really simple example of usage using a hanlder callable:
62
63     >>> import os
64     >>> def handle(event_loop):
65             data = os.read(event_loop.fileno, 100)
66             os.write(1, 'Received message: %r\n' % data)
67     >>> p = EventLoop(0, handle)
68     >>> p.loop(once=True)
69
70     In this example only one event is handled (see the 'once' argument
71     of loop).
72
73     A more complex example, making a subclass and explicitly stopping
74     the loop, looks something like this:
75
76     >>> class Test(EventLoop):
77     >>>     def handle(self):
78     >>>         data = os.read(self.fileno, 100)
79     >>>         if data == 'q\n':
80     >>>             self.stop()
81     >>>         else:
82     >>>             os.write(1, 'Received message: %r\n' % data)
83     >>>     def handle_timer(self):
84     >>>         print time.strftime('%c')
85     >>> p = Test(0, timer=5)
86     >>> p.loop()
87
88     This example loops until the user enters a single "q", when stop()
89     is called and the event loop is exited.
90     """
91
92     def __init__(self, file, handler=None, timer=None, timer_handler=None):
93         r"""Initialize the EventLoop object.
94
95         See EventLoop class documentation for more info.
96         """
97         log.debug(u'EventLoop(%r, %r, %r, %r)', file, handler,
98                     timer, timer_handler)
99         self.poll = select.poll()
100         self._stop = False
101         self.__register(file)
102         self.timer = timer
103         self.handler = handler
104         self.timer_handler = timer_handler
105
106     def __register(self, file):
107         r"__register(file) -> None :: Register a new file for polling."
108         self._file = file
109         self.poll.register(self.fileno, POLLIN | POLLPRI | POLLERR)
110
111     def set_file(self, file):
112         r"""set_file(file) -> None :: New file object to be monitored
113
114         Unregister the previous file object being monitored and register
115         a new one.
116         """
117         self.poll.unregister(self.fileno)
118         self.__register(file)
119
120     def get_file(self):
121         r"get_file() -> file object/int :: Get the current file object/fd."
122         return self._file
123
124     file = property(get_file, set_file, doc='File object (or descriptor)')
125
126     def get_fileno(self):
127         r"get_fileno() -> int :: Get the current file descriptor"
128         if hasattr(self.file, 'fileno'):
129             return self.file.fileno()
130         return self.file
131
132     fileno = property(get_fileno, doc='File descriptor (never a file object)')
133
134     def stop(self):
135         r"""stop() -> None :: Stop the event loop.
136
137         The event loop will be interrupted as soon as the current handler
138         finishes.
139         """
140         log.debug(u'EventLoop.stop()')
141         self._stop = True
142
143     def loop(self, once=False):
144         r"""loop([once]) -> None :: Wait for events.
145
146         Wait for events and handle then when they arrive. If once is True,
147         then only 1 event is processed and then this method returns.
148         """
149         log.debug(u'EventLoop.loop(%s)', once)
150         # Flag modified by the signal handler
151         global timeout
152         # If we use a timer, we set up the signal
153         if self.timer is not None:
154             signal.signal(signal.SIGALRM, alarm_handler)
155             self.handle_timer()
156             signal.alarm(self.timer)
157         while True:
158             try:
159                 log.debug(u'EventLoop.loop: polling')
160                 res = self.poll.poll()
161             except select.error, e:
162                 # The error is not an interrupt caused by the alarm, then raise
163                 if e.args[0] != errno.EINTR or not timeout:
164                     raise LoopInterruptedError(e)
165             # There was a timeout, so execute the timer handler
166             if timeout:
167                 log.debug(u'EventLoop.loop: timer catched, handling...')
168                 timeout = False
169                 self.handle_timer()
170                 signal.alarm(self.timer)
171             # Not a timeout, execute the regular handler
172             else:
173                 log.debug(u'EventLoop.loop: no timeout, handle event')
174                 self.handle()
175             # Look if we have to stop
176             if self._stop or once:
177                 log.debug(u'EventLoop.loop: stopped')
178                 self._stop = False
179                 break
180
181     def handle(self):
182         r"handle() -> None :: Abstract method to be overriden to handle events."
183         self.handler(self)
184
185     def handle_timer(self):
186         r"handle() -> None :: Abstract method to be overriden to handle events."
187         self.timer_handler(self)
188
189 if __name__ == '__main__':
190
191     logging.basicConfig(
192         level   = logging.DEBUG,
193         format  = '%(asctime)s %(levelname)-8s %(message)s',
194         datefmt = '%H:%M:%S',
195     )
196
197     import os
198     import time
199
200     def handle(event_loop):
201         data = os.read(event_loop.fileno, 100)
202         os.write(1, 'Received message: %r\n' % data)
203
204     p = EventLoop(0, handle)
205
206     os.write(1, 'Say something once: ')
207     p.loop(once=True)
208     os.write(1, 'Great!\n')
209
210     class Test(EventLoop):
211         def handle(self):
212             data = os.read(self.fileno, 100)
213             if data == 'q\n':
214                 self.stop()
215             else:
216                 os.write(1, 'Received message: %r\n' % data)
217         def handle_timer(self):
218             print time.strftime('%c')
219
220     p = Test(0, timer=5)
221
222     os.write(1, 'Say a lot of things, then press write just "q" to stop: ')
223     p.loop()
224     os.write(1, 'Ok, bye!\n')
225