| 1 | # -*- test-case-name: twisted.internet.test -*- |
| 2 | # Copyright (c) Twisted Matrix Laboratories. |
| 3 | # Copyright (c) 2011 Canonical Ltd. |
| 4 | # See LICENSE for details. |
| 5 | |
| 6 | """ |
| 7 | This module provides support for Twisted to interact with the glib/gtk2/gtk3 |
| 8 | mainloop. |
| 9 | |
| 10 | In order to use this support, simply do the following:: |
| 11 | |
| 12 | | from twisted.internet import gireactor |
| 13 | | gireactor.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 | import signal |
| 24 | import sys |
| 25 | |
| 26 | if 'gobject' in sys.modules: |
| 27 | import glib as GLib |
| 28 | else: |
| 29 | from gi.repository import GLib |
| 30 | # This is some nasty junk right here. But it should stop SEGFAULTs |
| 31 | sys.modules['glib'] = None |
| 32 | sys.modules['gobject'] = None |
| 33 | sys.modules['gio'] = None |
| 34 | sys.modules['gtk'] = None |
| 35 | |
| 36 | from twisted.internet import base, posixbase, selectreactor |
| 37 | from twisted.internet.interfaces import IReactorFDSet |
| 38 | from twisted.python import log, runtime |
| 39 | from twisted.python.compat import set |
| 40 | from zope.interface import implements |
| 41 | |
| 42 | |
| 43 | GLib.threads_init() |
| 44 | |
| 45 | POLL_DISCONNECTED = (GLib.IOCondition.HUP | GLib.IOCondition.ERR | |
| 46 | GLib.IOCondition.NVAL) |
| 47 | |
| 48 | # glib's iochannel sources won't tell us about any events that we haven't |
| 49 | # asked for, even if those events aren't sensible inputs to the poll() |
| 50 | # call. |
| 51 | INFLAGS = GLib.IOCondition.IN | POLL_DISCONNECTED |
| 52 | OUTFLAGS = GLib.IOCondition.OUT | POLL_DISCONNECTED |
| 53 | |
| 54 | |
| 55 | class _GISignalMixin(object): |
| 56 | |
| 57 | if runtime.platformType == 'posix': |
| 58 | |
| 59 | def _handleSignals(self): |
| 60 | # Let the base class do its thing, but pygtk is probably |
| 61 | # going to stomp on us so go beyond that and set up some |
| 62 | # signal handling which pygtk won't mess with. This would |
| 63 | # be better done by letting this reactor select a |
| 64 | # different implementation of installHandler for |
| 65 | # _SIGCHLDWaker to use. Then, at least, we could fall |
| 66 | # back to our extension module. See #4286. |
| 67 | from twisted.internet.process import ( |
| 68 | reapAllProcesses as _reapAllProcesses) |
| 69 | base._SignalReactorMixin._handleSignals(self) |
| 70 | signal.signal(signal.SIGCHLD, |
| 71 | lambda *a: self.callFromThread(_reapAllProcesses)) |
| 72 | if getattr(signal, "siginterrupt", None) is not None: |
| 73 | signal.siginterrupt(signal.SIGCHLD, False) |
| 74 | # Like the base, reap processes now in case a process |
| 75 | # exited before the handlers above were installed. |
| 76 | _reapAllProcesses() |
| 77 | |
| 78 | |
| 79 | class _GIWaker(posixbase._UnixWaker): |
| 80 | """ |
| 81 | Run scheduled events after waking up. |
| 82 | """ |
| 83 | |
| 84 | def doRead(self): |
| 85 | posixbase._UnixWaker.doRead(self) |
| 86 | self.reactor._simulate() |
| 87 | |
| 88 | |
| 89 | class GIReactor(_GISignalMixin, |
| 90 | posixbase.PosixReactorBase, posixbase._PollLikeMixin): |
| 91 | """ |
| 92 | GObject event loop reactor. |
| 93 | |
| 94 | @ivar _sources: A dictionary mapping L{FileDescriptor} instances to |
| 95 | GSource handles. |
| 96 | |
| 97 | @ivar _reads: A set of L{FileDescriptor} instances currently monitored for |
| 98 | reading. |
| 99 | |
| 100 | @ivar _writes: A set of L{FileDescriptor} instances currently monitored for |
| 101 | writing. |
| 102 | |
| 103 | @ivar _simtag: A GSource handle for the next L{simulate} call. |
| 104 | """ |
| 105 | implements(IReactorFDSet) |
| 106 | |
| 107 | _POLL_DISCONNECTED = POLL_DISCONNECTED |
| 108 | _POLL_IN = GLib.IOCondition.IN |
| 109 | _POLL_OUT = GLib.IOCondition.OUT |
| 110 | |
| 111 | # Install a waker that knows it needs to call C{_simulate} in order to run |
| 112 | # callbacks queued from a thread: |
| 113 | _wakerFactory = _GIWaker |
| 114 | |
| 115 | def __init__(self, useGtk=False): |
| 116 | self._simtag = None |
| 117 | self._reads = set() |
| 118 | self._writes = set() |
| 119 | self._sources = {} |
| 120 | posixbase.PosixReactorBase.__init__(self) |
| 121 | |
| 122 | if useGtk: |
| 123 | if 'gobject' in sys.modules: |
| 124 | import gtk as Gtk |
| 125 | else: |
| 126 | from gi.repository import Gtk |
| 127 | |
| 128 | def mainquit(): |
| 129 | if Gtk.main_level(): |
| 130 | Gtk.main_quit() |
| 131 | |
| 132 | self.__pending = Gtk.events_pending |
| 133 | self.__iteration = Gtk.main_iteration |
| 134 | self.__crash = mainquit |
| 135 | self.__run = Gtk.main |
| 136 | else: |
| 137 | self.context = GLib.main_context_default() |
| 138 | self.__pending = self.context.pending |
| 139 | self.__iteration = self.context.iteration |
| 140 | self.loop = GLib.MainLoop() |
| 141 | self.__crash = lambda: GLib.idle_add(self.loop.quit) |
| 142 | self.__run = self.loop.run |
| 143 | |
| 144 | # The input_add function in pygtk1 checks for objects with a |
| 145 | # 'fileno' method and, if present, uses the result of that method |
| 146 | # as the input source. The pygtk2 input_add does not do this. The |
| 147 | # function below replicates the pygtk1 functionality. |
| 148 | |
| 149 | # In addition, pygtk maps gtk.input_add to _gobject.io_add_watch, and |
| 150 | # g_io_add_watch() takes different condition bitfields than |
| 151 | # gtk_input_add(). We use g_io_add_watch() here in case pygtk fixes this |
| 152 | # bug. |
| 153 | def input_add(self, source, condition, callback): |
| 154 | if hasattr(source, 'fileno'): |
| 155 | # handle python objects |
| 156 | def wrapper(source, condition, real_s=source, real_cb=callback): |
| 157 | return real_cb(real_s, condition) |
| 158 | return GLib.io_add_watch(source.fileno(), condition, wrapper) |
| 159 | else: |
| 160 | return GLib.io_add_watch(source, condition, callback) |
| 161 | |
| 162 | def _ioEventCallback(self, source, condition): |
| 163 | """ |
| 164 | Called by event loop when an I/O event occurs. |
| 165 | """ |
| 166 | log.callWithLogger( |
| 167 | source, self._doReadOrWrite, source, source, condition) |
| 168 | return True # True = don't auto-remove the source |
| 169 | |
| 170 | def _add(self, source, primary, other, primaryFlag, otherFlag): |
| 171 | """ |
| 172 | Add the given L{FileDescriptor} for monitoring either for reading or |
| 173 | writing. If the file is already monitored for the other operation, we |
| 174 | delete the previous registration and re-register it for both reading |
| 175 | and writing. |
| 176 | """ |
| 177 | if source in primary: |
| 178 | return |
| 179 | flags = primaryFlag |
| 180 | if source in other: |
| 181 | GLib.source_remove(self._sources[source]) |
| 182 | flags |= otherFlag |
| 183 | self._sources[source] = self.input_add( |
| 184 | source, flags, self._ioEventCallback) |
| 185 | primary.add(source) |
| 186 | |
| 187 | def addReader(self, reader): |
| 188 | """ |
| 189 | Add a L{FileDescriptor} for monitoring of data available to read. |
| 190 | """ |
| 191 | self._add(reader, self._reads, self._writes, INFLAGS, OUTFLAGS) |
| 192 | |
| 193 | def addWriter(self, writer): |
| 194 | """ |
| 195 | Add a L{FileDescriptor} for monitoring ability to write data. |
| 196 | """ |
| 197 | self._add(writer, self._writes, self._reads, OUTFLAGS, INFLAGS) |
| 198 | |
| 199 | def getReaders(self): |
| 200 | """ |
| 201 | Retrieve the list of current L{FileDescriptor} monitored for reading. |
| 202 | """ |
| 203 | return list(self._reads) |
| 204 | |
| 205 | def getWriters(self): |
| 206 | """ |
| 207 | Retrieve the list of current L{FileDescriptor} monitored for writing. |
| 208 | """ |
| 209 | return list(self._writes) |
| 210 | |
| 211 | def removeAll(self): |
| 212 | """ |
| 213 | Remove monitoring for all registered L{FileDescriptor}s. |
| 214 | """ |
| 215 | return self._removeAll(self._reads, self._writes) |
| 216 | |
| 217 | def _remove(self, source, primary, other, flags): |
| 218 | """ |
| 219 | Remove monitoring the given L{FileDescriptor} for either reading or |
| 220 | writing. If it's still monitored for the other operation, we |
| 221 | re-register the L{FileDescriptor} for only that operation. |
| 222 | """ |
| 223 | if source not in primary: |
| 224 | return |
| 225 | GLib.source_remove(self._sources[source]) |
| 226 | primary.remove(source) |
| 227 | if source in other: |
| 228 | self._sources[source] = self.input_add( |
| 229 | source, flags, self._ioEventCallback) |
| 230 | else: |
| 231 | self._sources.pop(source) |
| 232 | |
| 233 | def removeReader(self, reader): |
| 234 | """ |
| 235 | Stop monitoring the given L{FileDescriptor} for reading. |
| 236 | """ |
| 237 | self._remove(reader, self._reads, self._writes, OUTFLAGS) |
| 238 | |
| 239 | def removeWriter(self, writer): |
| 240 | """ |
| 241 | Stop monitoring the given L{FileDescriptor} for writing. |
| 242 | """ |
| 243 | self._remove(writer, self._writes, self._reads, INFLAGS) |
| 244 | |
| 245 | def iterate(self, delay=0): |
| 246 | """ |
| 247 | One iteration of the event loop, for trial's use. |
| 248 | |
| 249 | This is not used for actual reactor runs. |
| 250 | """ |
| 251 | self.runUntilCurrent() |
| 252 | while self.__pending(): |
| 253 | self.__iteration(0) |
| 254 | |
| 255 | def crash(self): |
| 256 | """ |
| 257 | Crash the reactor. |
| 258 | """ |
| 259 | posixbase.PosixReactorBase.crash(self) |
| 260 | self.__crash() |
| 261 | |
| 262 | def stop(self): |
| 263 | """ |
| 264 | Stop the reactor. |
| 265 | """ |
| 266 | posixbase.PosixReactorBase.stop(self) |
| 267 | # The base implementation only sets a flag, to ensure shutting down is |
| 268 | # not reentrant. Unfortunately, this flag is not meaningful to the |
| 269 | # gobject event loop. We therefore call wakeUp() to ensure the event |
| 270 | # loop will call back into Twisted once this iteration is done. This |
| 271 | # will result in self.runUntilCurrent() being called, where the stop |
| 272 | # flag will trigger the actual shutdown process, eventually calling |
| 273 | # crash() which will do the actual gobject event loop shutdown. |
| 274 | self.wakeUp() |
| 275 | |
| 276 | def run(self, installSignalHandlers=True): |
| 277 | """ |
| 278 | Run the reactor. |
| 279 | """ |
| 280 | self.callWhenRunning(self._reschedule) |
| 281 | self.startRunning(installSignalHandlers=installSignalHandlers) |
| 282 | if self._started: |
| 283 | self.__run() |
| 284 | |
| 285 | def callLater(self, *args, **kwargs): |
| 286 | """ |
| 287 | Schedule a C{DelayedCall}. |
| 288 | """ |
| 289 | result = posixbase.PosixReactorBase.callLater(self, *args, **kwargs) |
| 290 | # Make sure we'll get woken up at correct time to handle this new |
| 291 | # scheduled call: |
| 292 | self._reschedule() |
| 293 | return result |
| 294 | |
| 295 | def _reschedule(self): |
| 296 | """ |
| 297 | Schedule a glib timeout for C{_simulate}. |
| 298 | """ |
| 299 | if self._simtag is not None: |
| 300 | GLib.source_remove(self._simtag) |
| 301 | self._simtag = None |
| 302 | timeout = self.timeout() |
| 303 | if timeout is not None: |
| 304 | self._simtag = GLib.timeout_add(int(timeout * 1000), |
| 305 | self._simulate) |
| 306 | |
| 307 | def _simulate(self): |
| 308 | """ |
| 309 | Run timers, and then reschedule glib timeout for next scheduled event. |
| 310 | """ |
| 311 | self.runUntilCurrent() |
| 312 | self._reschedule() |
| 313 | |
| 314 | |
| 315 | class PortableGIReactor(_GISignalMixin, selectreactor.SelectReactor): |
| 316 | """ |
| 317 | Portable GObject Introspection event loop reactor. |
| 318 | """ |
| 319 | def __init__(self, useGtk=False): |
| 320 | self._simtag = None |
| 321 | selectreactor.SelectReactor.__init__(self) |
| 322 | |
| 323 | if useGtk: |
| 324 | if 'gobject' in sys.modules: |
| 325 | import gtk as Gtk |
| 326 | else: |
| 327 | from gi.repository import Gtk |
| 328 | |
| 329 | def mainquit(): |
| 330 | if Gtk.main_level(): |
| 331 | Gtk.main_quit() |
| 332 | |
| 333 | self.__crash = mainquit |
| 334 | self.__run = Gtk.main |
| 335 | else: |
| 336 | self.loop = GLib.MainLoop() |
| 337 | self.__crash = lambda: GLib.idle_add(self.loop.quit) |
| 338 | self.__run = self.loop.run |
| 339 | |
| 340 | def crash(self): |
| 341 | selectreactor.SelectReactor.crash(self) |
| 342 | self.__crash() |
| 343 | |
| 344 | def run(self, installSignalHandlers=True): |
| 345 | self.startRunning(installSignalHandlers=installSignalHandlers) |
| 346 | GLib.idle_add(self.simulate) |
| 347 | self.__run() |
| 348 | |
| 349 | def simulate(self): |
| 350 | """ |
| 351 | Run simulation loops and reschedule callbacks. |
| 352 | """ |
| 353 | if self._simtag is not None: |
| 354 | GLib.source_remove(self._simtag) |
| 355 | self.iterate() |
| 356 | timeout = min(self.timeout(), 0.1) |
| 357 | if timeout is None: |
| 358 | timeout = 0.1 |
| 359 | self._simtag = GLib.timeout_add(int(timeout * 1000), self.simulate) |
| 360 | |
| 361 | |
| 362 | def install(useGtk=False): |
| 363 | """ |
| 364 | Configure the twisted mainloop to be run inside the glib mainloop. |
| 365 | |
| 366 | @param useGtk: should GTK+ rather than glib event loop be |
| 367 | used (this will be slightly slower but does support GUI). |
| 368 | """ |
| 369 | if runtime.platform.getType() == 'posix': |
| 370 | reactor = GIReactor(useGtk=useGtk) |
| 371 | else: |
| 372 | reactor = PortableGIReactor(useGtk=useGtk) |
| 373 | |
| 374 | from twisted.internet.main import installReactor |
| 375 | installReactor(reactor) |
| 376 | return reactor |
| 377 | |
| 378 | |
| 379 | __all__ = ['install'] |