| | 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'] |