| 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 | """ |
|---|
| 7 | This module provides support for Twisted to interact with the glib/gtk3 |
|---|
| 8 | mainloop. |
|---|
| 9 | |
|---|
| 10 | In order to use this support, simply do the following:: |
|---|
| 11 | |
|---|
| 12 | | from twisted.internet import gtk3reactor |
|---|
| 13 | | gtk3reactor.install() |
|---|
| 14 | |
|---|
| 15 | Then use twisted.internet APIs as usual. The other methods here are not |
|---|
| 16 | intended to be called directly. |
|---|
| 17 | |
|---|
| 18 | When installing the reactor, you can choose whether to use the glib |
|---|
| 19 | event loop or the GTK+ event loop which is based on it but adds GUI |
|---|
| 20 | integration. |
|---|
| 21 | """ |
|---|
| 22 | |
|---|
| 23 | # System Imports |
|---|
| 24 | import sys |
|---|
| 25 | from zope.interface import implements |
|---|
| 26 | try: |
|---|
| 27 | if not hasattr(sys, 'frozen'): |
|---|
| 28 | # Don't want to check this for py2exe |
|---|
| 29 | from gi.repository import Gtk as gtk |
|---|
| 30 | except (ImportError, AttributeError): |
|---|
| 31 | pass # maybe we're using pygtk before this hack existed. |
|---|
| 32 | from gi.repository import GObject as gobject |
|---|
| 33 | if hasattr(gobject, "threads_init"): |
|---|
| 34 | gobject.threads_init() |
|---|
| 35 | |
|---|
| 36 | # Twisted Imports |
|---|
| 37 | from twisted.python import log, runtime, failure |
|---|
| 38 | from twisted.python.compat import set |
|---|
| 39 | from twisted.internet.interfaces import IReactorFDSet |
|---|
| 40 | from twisted.internet import main, posixbase, error, selectreactor |
|---|
| 41 | |
|---|
| 42 | POLL_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. |
|---|
| 47 | INFLAGS = gobject.IO_IN | POLL_DISCONNECTED |
|---|
| 48 | OUTFLAGS = gobject.IO_OUT | POLL_DISCONNECTED |
|---|
| 49 | |
|---|
| 50 | |
|---|
| 51 | |
|---|
| 52 | def _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 | |
|---|
| 64 | class 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 | |
|---|
| 290 | class 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 | |
|---|
| 335 | def 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 | |
|---|
| 349 | def 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 | |
|---|
| 360 | if runtime.platform.getType() != 'posix': |
|---|
| 361 | install = portableInstall |
|---|
| 362 | |
|---|
| 363 | |
|---|
| 364 | |
|---|
| 365 | __all__ = ['install'] |
|---|