Ticket #4558: gtk3reactor.py

File gtk3reactor.py, 11.6 KB (added by kkszysiu, 4 years ago)

reactor

Line 
1# -*- test-case-name: twisted.internet.test.test_gtk3reactor -*-
2# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5
6"""
7This module provides support for Twisted to interact with the glib/gtk3
8mainloop.
9
10In order to use this support, simply do the following::
11
12    |  from twisted.internet import gtk3reactor
13    |  gtk3reactor.install()
14
15Then use twisted.internet APIs as usual.  The other methods here are not
16intended to be called directly.
17
18When installing the reactor, you can choose whether to use the glib
19event loop or the GTK+ event loop which is based on it but adds GUI
20integration.
21"""
22
23# System Imports
24import sys
25from zope.interface import implements
26try:
27    if not hasattr(sys, 'frozen'):
28        # Don't want to check this for py2exe
29        from gi.repository import Gtk as gtk
30except (ImportError, AttributeError):
31    pass # maybe we're using pygtk before this hack existed.
32from gi.repository import GObject as gobject
33if hasattr(gobject, "threads_init"):
34    gobject.threads_init()
35
36# Twisted Imports
37from twisted.python import log, runtime, failure
38from twisted.python.compat import set
39from twisted.internet.interfaces import IReactorFDSet
40from twisted.internet import main, posixbase, error, selectreactor
41
42POLL_DISCONNECTED = gobject.IO_HUP | gobject.IO_ERR | gobject.IO_NVAL
43
44# glib's iochannel sources won't tell us about any events that we haven't
45# asked for, even if those events aren't sensible inputs to the poll()
46# call.
47INFLAGS = gobject.IO_IN | POLL_DISCONNECTED
48OUTFLAGS = gobject.IO_OUT | POLL_DISCONNECTED
49
50
51
52def _our_mainquit():
53    # XXX: gtk.main_quit() (which is used for crash()) raises an exception if
54    # gtk.main_level() == 0; however, all the tests freeze if we use this
55    # function to stop the reactor.  what gives?  (I believe this may have been
56    # a stupid mistake where I forgot to import gtk here... I will remove this
57    # comment if the tests pass)
58    from gi.repository import Gtk as gtk
59    if gtk.main_level():
60        gtk.main_quit()
61
62
63
64class Gtk3Reactor(posixbase.PosixReactorBase):
65    """
66    GTK+-3 event loop reactor.
67
68    @ivar _sources: A dictionary mapping L{FileDescriptor} instances to gtk
69        watch handles.
70
71    @ivar _reads: A set of L{FileDescriptor} instances currently monitored for
72        reading.
73
74    @ivar _writes: A set of L{FileDescriptor} instances currently monitored for
75        writing.
76
77    @ivar _simtag: A gtk timeout handle for the next L{simulate} call.
78    """
79    implements(IReactorFDSet)
80
81    def __init__(self, useGtk=True):
82        self._simtag = None
83        self._reads = set()
84        self._writes = set()
85        self._sources = {}
86        posixbase.PosixReactorBase.__init__(self)
87
88        self.context = gobject.main_context_default()
89        self.__pending = self.context.pending
90        self.__iteration = self.context.iteration
91        self.loop = gobject.MainLoop()
92        self.__crash = self.loop.quit
93        self.__run = self.loop.run
94
95    # The input_add function in pygtk1 checks for objects with a
96    # 'fileno' method and, if present, uses the result of that method
97    # as the input source. The gobject-instrospected GTK3 input_add does not do this. The
98    # function below replicates the pygtk1 functionality.
99
100    # In addition, pygtk maps gtk.input_add to _gobject.io_add_watch, and
101    # g_io_add_watch() takes different condition bitfields than
102    # gtk_input_add(). We use g_io_add_watch() here in case pygtk fixes this
103    # bug.
104    def input_add(self, source, condition, callback):
105        if hasattr(source, 'fileno'):
106            # handle python objects
107            def wrapper(source, condition, real_s=source, real_cb=callback):
108                return real_cb(real_s, condition)
109            return gobject.io_add_watch(source.fileno(), condition, wrapper)
110        else:
111            return gobject.io_add_watch(source, condition, callback)
112
113
114    def _add(self, source, primary, other, primaryFlag, otherFlag):
115        """
116        Add the given L{FileDescriptor} for monitoring either for reading or
117        writing. If the file is already monitored for the other operation, we
118        delete the previous registration and re-register it for both reading
119        and writing.
120        """
121        if source in primary:
122            return
123        flags = primaryFlag
124        if source in other:
125            gobject.source_remove(self._sources[source])
126            flags |= otherFlag
127        self._sources[source] = self.input_add(source, flags, self.callback)
128        primary.add(source)
129
130
131    def addReader(self, reader):
132        """
133        Add a L{FileDescriptor} for monitoring of data available to read.
134        """
135        self._add(reader, self._reads, self._writes, INFLAGS, OUTFLAGS)
136
137
138    def addWriter(self, writer):
139        """
140        Add a L{FileDescriptor} for monitoring ability to write data.
141        """
142        self._add(writer, self._writes, self._reads, OUTFLAGS, INFLAGS)
143
144
145    def getReaders(self):
146        """
147        Retrieve the list of current L{FileDescriptor} monitored for reading.
148        """
149        return list(self._reads)
150
151
152    def getWriters(self):
153        """
154        Retrieve the list of current L{FileDescriptor} monitored for writing.
155        """
156        return list(self._writes)
157
158
159    def removeAll(self):
160        """
161        Remove monitoring for all registered L{FileDescriptor}s.
162        """
163        return self._removeAll(self._reads, self._writes)
164
165
166    def _remove(self, source, primary, other, flags):
167        """
168        Remove monitoring the given L{FileDescriptor} for either reading or
169        writing. If it's still monitored for the other operation, we
170        re-register the L{FileDescriptor} for only that operation.
171        """
172        if source not in primary:
173            return
174        gobject.source_remove(self._sources[source])
175        primary.remove(source)
176        if source in other:
177            self._sources[source] = self.input_add(
178                source, flags, self.callback)
179        else:
180            self._sources.pop(source)
181
182
183    def removeReader(self, reader):
184        """
185        Stop monitoring the given L{FileDescriptor} for reading.
186        """
187        self._remove(reader, self._reads, self._writes, OUTFLAGS)
188
189
190    def removeWriter(self, writer):
191        """
192        Stop monitoring the given L{FileDescriptor} for writing.
193        """
194        self._remove(writer, self._writes, self._reads, INFLAGS)
195
196
197    doIterationTimer = None
198
199    def doIterationTimeout(self, *args):
200        self.doIterationTimer = None
201        return 0 # auto-remove
202
203
204    def doIteration(self, delay):
205        # flush some pending events, return if there was something to do
206        # don't use the usual "while self.context.pending(): self.context.iteration()"
207        # idiom because lots of IO (in particular test_tcp's
208        # ProperlyCloseFilesTestCase) can keep us from ever exiting.
209        log.msg(channel='system', event='iteration', reactor=self)
210        if self.__pending():
211            self.__iteration(0)
212            return
213        # nothing to do, must delay
214        if delay == 0:
215            return # shouldn't delay, so just return
216        self.doIterationTimer = gobject.timeout_add(int(delay * 1000),
217                                                self.doIterationTimeout)
218        # This will either wake up from IO or from a timeout.
219        self.__iteration(1) # block
220        # note: with the .simulate timer below, delays > 0.1 will always be
221        # woken up by the .simulate timer
222        if self.doIterationTimer:
223            # if woken by IO, need to cancel the timer
224            gobject.source_remove(self.doIterationTimer)
225            self.doIterationTimer = None
226
227
228    def crash(self):
229        posixbase.PosixReactorBase.crash(self)
230        self.__crash()
231
232
233    def run(self, installSignalHandlers=1):
234        self.startRunning(installSignalHandlers=installSignalHandlers)
235        gobject.timeout_add(0, self.simulate)
236        if self._started:
237            self.__run()
238
239
240    def _doReadOrWrite(self, source, condition, faildict={
241        error.ConnectionDone: failure.Failure(error.ConnectionDone()),
242        error.ConnectionLost: failure.Failure(error.ConnectionLost()),
243        }):
244        why = None
245        didRead = None
246        if condition & POLL_DISCONNECTED and \
247               not (condition & gobject.IO_IN):
248            why = main.CONNECTION_LOST
249        else:
250            try:
251                if condition & gobject.IO_IN:
252                    why = source.doRead()
253                    didRead = source.doRead
254                if not why and condition & gobject.IO_OUT:
255                    # if doRead caused connectionLost, don't call doWrite
256                    # if doRead is doWrite, don't call it again.
257                    if not source.disconnected and source.doWrite != didRead:
258                        why = source.doWrite()
259                        didRead = source.doWrite # if failed it was in write
260            except:
261                why = sys.exc_info()[1]
262                log.msg('Error In %s' % source)
263                log.deferr()
264
265        if why:
266            self._disconnectSelectable(source, why, didRead == source.doRead)
267
268
269    def callback(self, source, condition):
270        log.callWithLogger(source, self._doReadOrWrite, source, condition)
271        self.simulate() # fire Twisted timers
272        return 1 # 1=don't auto-remove the source
273
274
275    def simulate(self):
276        """
277        Run simulation loops and reschedule callbacks.
278        """
279        if self._simtag is not None:
280            gobject.source_remove(self._simtag)
281        self.runUntilCurrent()
282        timeout = min(self.timeout(), 0.1)
283        if timeout is None:
284            timeout = 0.1
285        # grumble
286        self._simtag = gobject.timeout_add(int(timeout * 1010), self.simulate)
287
288
289
290class PortableGtkReactor(selectreactor.SelectReactor):
291    """
292    Reactor that works on Windows.
293
294    Sockets aren't supported by GTK+'s input_add on Win32.
295    """
296    _simtag = None
297
298    def crash(self):
299        selectreactor.SelectReactor.crash(self)
300        from gi.repository import Gtk as gtk
301        # mainquit is deprecated in newer versions
302        if gtk.main_level():
303            if hasattr(gtk, 'main_quit'):
304                gtk.main_quit()
305            else:
306                gtk.mainquit()
307
308
309    def run(self, installSignalHandlers=1):
310        from gi.repository import Gtk as gtk
311        self.startRunning(installSignalHandlers=installSignalHandlers)
312        gobject.timeout_add(0, self.simulate)
313        # mainloop is deprecated in newer versions
314        if hasattr(gtk, 'main'):
315            gtk.main()
316        else:
317            gtk.mainloop()
318
319
320    def simulate(self):
321        """
322        Run simulation loops and reschedule callbacks.
323        """
324        if self._simtag is not None:
325            gobject.source_remove(self._simtag)
326        self.iterate()
327        timeout = min(self.timeout(), 0.1)
328        if timeout is None:
329            timeout = 0.1
330        # grumble
331        self._simtag = gobject.timeout_add(int(timeout * 1010), self.simulate)
332
333
334
335def install(useGtk=True):
336    """
337    Configure the twisted mainloop to be run inside the gtk mainloop.
338
339    @param useGtk: should glib rather than GTK+ event loop be
340        used (this will be slightly faster but does not support GUI).
341    """
342    reactor = Gtk3Reactor(useGtk)
343    from twisted.internet.main import installReactor
344    installReactor(reactor)
345    return reactor
346
347
348
349def portableInstall(useGtk=True):
350    """
351    Configure the twisted mainloop to be run inside the gtk mainloop.
352    """
353    reactor = PortableGtkReactor()
354    from twisted.internet.main import installReactor
355    installReactor(reactor)
356    return reactor
357
358
359
360if runtime.platform.getType() != 'posix':
361    install = portableInstall
362
363
364
365__all__ = ['install']