OLD | NEW |
(Empty) | |
| 1 """Site services for use with a Web Site Process Bus.""" |
| 2 |
| 3 import os |
| 4 import re |
| 5 import signal as _signal |
| 6 import sys |
| 7 import time |
| 8 import threading |
| 9 |
| 10 from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident, ntob, s
et |
| 11 |
| 12 # _module__file__base is used by Autoreload to make |
| 13 # absolute any filenames retrieved from sys.modules which are not |
| 14 # already absolute paths. This is to work around Python's quirk |
| 15 # of importing the startup script and using a relative filename |
| 16 # for it in sys.modules. |
| 17 # |
| 18 # Autoreload examines sys.modules afresh every time it runs. If an application |
| 19 # changes the current directory by executing os.chdir(), then the next time |
| 20 # Autoreload runs, it will not be able to find any filenames which are |
| 21 # not absolute paths, because the current directory is not the same as when the |
| 22 # module was first imported. Autoreload will then wrongly conclude the file has |
| 23 # "changed", and initiate the shutdown/re-exec sequence. |
| 24 # See ticket #917. |
| 25 # For this workaround to have a decent probability of success, this module |
| 26 # needs to be imported as early as possible, before the app has much chance |
| 27 # to change the working directory. |
| 28 _module__file__base = os.getcwd() |
| 29 |
| 30 |
| 31 class SimplePlugin(object): |
| 32 """Plugin base class which auto-subscribes methods for known channels.""" |
| 33 |
| 34 bus = None |
| 35 """A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine.""" |
| 36 |
| 37 def __init__(self, bus): |
| 38 self.bus = bus |
| 39 |
| 40 def subscribe(self): |
| 41 """Register this object as a (multi-channel) listener on the bus.""" |
| 42 for channel in self.bus.listeners: |
| 43 # Subscribe self.start, self.exit, etc. if present. |
| 44 method = getattr(self, channel, None) |
| 45 if method is not None: |
| 46 self.bus.subscribe(channel, method) |
| 47 |
| 48 def unsubscribe(self): |
| 49 """Unregister this object as a listener on the bus.""" |
| 50 for channel in self.bus.listeners: |
| 51 # Unsubscribe self.start, self.exit, etc. if present. |
| 52 method = getattr(self, channel, None) |
| 53 if method is not None: |
| 54 self.bus.unsubscribe(channel, method) |
| 55 |
| 56 |
| 57 |
| 58 class SignalHandler(object): |
| 59 """Register bus channels (and listeners) for system signals. |
| 60 |
| 61 You can modify what signals your application listens for, and what it does |
| 62 when it receives signals, by modifying :attr:`SignalHandler.handlers`, |
| 63 a dict of {signal name: callback} pairs. The default set is:: |
| 64 |
| 65 handlers = {'SIGTERM': self.bus.exit, |
| 66 'SIGHUP': self.handle_SIGHUP, |
| 67 'SIGUSR1': self.bus.graceful, |
| 68 } |
| 69 |
| 70 The :func:`SignalHandler.handle_SIGHUP`` method calls |
| 71 :func:`bus.restart()<cherrypy.process.wspbus.Bus.restart>` |
| 72 if the process is daemonized, but |
| 73 :func:`bus.exit()<cherrypy.process.wspbus.Bus.exit>` |
| 74 if the process is attached to a TTY. This is because Unix window |
| 75 managers tend to send SIGHUP to terminal windows when the user closes them. |
| 76 |
| 77 Feel free to add signals which are not available on every platform. The |
| 78 :class:`SignalHandler` will ignore errors raised from attempting to register |
| 79 handlers for unknown signals. |
| 80 """ |
| 81 |
| 82 handlers = {} |
| 83 """A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit).""" |
| 84 |
| 85 signals = {} |
| 86 """A map from signal numbers to names.""" |
| 87 |
| 88 for k, v in vars(_signal).items(): |
| 89 if k.startswith('SIG') and not k.startswith('SIG_'): |
| 90 signals[v] = k |
| 91 del k, v |
| 92 |
| 93 def __init__(self, bus): |
| 94 self.bus = bus |
| 95 # Set default handlers |
| 96 self.handlers = {'SIGTERM': self.bus.exit, |
| 97 'SIGHUP': self.handle_SIGHUP, |
| 98 'SIGUSR1': self.bus.graceful, |
| 99 } |
| 100 |
| 101 if sys.platform[:4] == 'java': |
| 102 del self.handlers['SIGUSR1'] |
| 103 self.handlers['SIGUSR2'] = self.bus.graceful |
| 104 self.bus.log("SIGUSR1 cannot be set on the JVM platform. " |
| 105 "Using SIGUSR2 instead.") |
| 106 self.handlers['SIGINT'] = self._jython_SIGINT_handler |
| 107 |
| 108 self._previous_handlers = {} |
| 109 |
| 110 def _jython_SIGINT_handler(self, signum=None, frame=None): |
| 111 # See http://bugs.jython.org/issue1313 |
| 112 self.bus.log('Keyboard Interrupt: shutting down bus') |
| 113 self.bus.exit() |
| 114 |
| 115 def subscribe(self): |
| 116 """Subscribe self.handlers to signals.""" |
| 117 for sig, func in self.handlers.items(): |
| 118 try: |
| 119 self.set_handler(sig, func) |
| 120 except ValueError: |
| 121 pass |
| 122 |
| 123 def unsubscribe(self): |
| 124 """Unsubscribe self.handlers from signals.""" |
| 125 for signum, handler in self._previous_handlers.items(): |
| 126 signame = self.signals[signum] |
| 127 |
| 128 if handler is None: |
| 129 self.bus.log("Restoring %s handler to SIG_DFL." % signame) |
| 130 handler = _signal.SIG_DFL |
| 131 else: |
| 132 self.bus.log("Restoring %s handler %r." % (signame, handler)) |
| 133 |
| 134 try: |
| 135 our_handler = _signal.signal(signum, handler) |
| 136 if our_handler is None: |
| 137 self.bus.log("Restored old %s handler %r, but our " |
| 138 "handler was not registered." % |
| 139 (signame, handler), level=30) |
| 140 except ValueError: |
| 141 self.bus.log("Unable to restore %s handler %r." % |
| 142 (signame, handler), level=40, traceback=True) |
| 143 |
| 144 def set_handler(self, signal, listener=None): |
| 145 """Subscribe a handler for the given signal (number or name). |
| 146 |
| 147 If the optional 'listener' argument is provided, it will be |
| 148 subscribed as a listener for the given signal's channel. |
| 149 |
| 150 If the given signal name or number is not available on the current |
| 151 platform, ValueError is raised. |
| 152 """ |
| 153 if isinstance(signal, basestring): |
| 154 signum = getattr(_signal, signal, None) |
| 155 if signum is None: |
| 156 raise ValueError("No such signal: %r" % signal) |
| 157 signame = signal |
| 158 else: |
| 159 try: |
| 160 signame = self.signals[signal] |
| 161 except KeyError: |
| 162 raise ValueError("No such signal: %r" % signal) |
| 163 signum = signal |
| 164 |
| 165 prev = _signal.signal(signum, self._handle_signal) |
| 166 self._previous_handlers[signum] = prev |
| 167 |
| 168 if listener is not None: |
| 169 self.bus.log("Listening for %s." % signame) |
| 170 self.bus.subscribe(signame, listener) |
| 171 |
| 172 def _handle_signal(self, signum=None, frame=None): |
| 173 """Python signal handler (self.set_handler subscribes it for you).""" |
| 174 signame = self.signals[signum] |
| 175 self.bus.log("Caught signal %s." % signame) |
| 176 self.bus.publish(signame) |
| 177 |
| 178 def handle_SIGHUP(self): |
| 179 """Restart if daemonized, else exit.""" |
| 180 if os.isatty(sys.stdin.fileno()): |
| 181 # not daemonized (may be foreground or background) |
| 182 self.bus.log("SIGHUP caught but not daemonized. Exiting.") |
| 183 self.bus.exit() |
| 184 else: |
| 185 self.bus.log("SIGHUP caught while daemonized. Restarting.") |
| 186 self.bus.restart() |
| 187 |
| 188 |
| 189 try: |
| 190 import pwd, grp |
| 191 except ImportError: |
| 192 pwd, grp = None, None |
| 193 |
| 194 |
| 195 class DropPrivileges(SimplePlugin): |
| 196 """Drop privileges. uid/gid arguments not available on Windows. |
| 197 |
| 198 Special thanks to Gavin Baker: http://antonym.org/node/100. |
| 199 """ |
| 200 |
| 201 def __init__(self, bus, umask=None, uid=None, gid=None): |
| 202 SimplePlugin.__init__(self, bus) |
| 203 self.finalized = False |
| 204 self.uid = uid |
| 205 self.gid = gid |
| 206 self.umask = umask |
| 207 |
| 208 def _get_uid(self): |
| 209 return self._uid |
| 210 def _set_uid(self, val): |
| 211 if val is not None: |
| 212 if pwd is None: |
| 213 self.bus.log("pwd module not available; ignoring uid.", |
| 214 level=30) |
| 215 val = None |
| 216 elif isinstance(val, basestring): |
| 217 val = pwd.getpwnam(val)[2] |
| 218 self._uid = val |
| 219 uid = property(_get_uid, _set_uid, |
| 220 doc="The uid under which to run. Availability: Unix.") |
| 221 |
| 222 def _get_gid(self): |
| 223 return self._gid |
| 224 def _set_gid(self, val): |
| 225 if val is not None: |
| 226 if grp is None: |
| 227 self.bus.log("grp module not available; ignoring gid.", |
| 228 level=30) |
| 229 val = None |
| 230 elif isinstance(val, basestring): |
| 231 val = grp.getgrnam(val)[2] |
| 232 self._gid = val |
| 233 gid = property(_get_gid, _set_gid, |
| 234 doc="The gid under which to run. Availability: Unix.") |
| 235 |
| 236 def _get_umask(self): |
| 237 return self._umask |
| 238 def _set_umask(self, val): |
| 239 if val is not None: |
| 240 try: |
| 241 os.umask |
| 242 except AttributeError: |
| 243 self.bus.log("umask function not available; ignoring umask.", |
| 244 level=30) |
| 245 val = None |
| 246 self._umask = val |
| 247 umask = property(_get_umask, _set_umask, |
| 248 doc="""The default permission mode for newly created files and directori
es. |
| 249 |
| 250 Usually expressed in octal format, for example, ``0644``. |
| 251 Availability: Unix, Windows. |
| 252 """) |
| 253 |
| 254 def start(self): |
| 255 # uid/gid |
| 256 def current_ids(): |
| 257 """Return the current (uid, gid) if available.""" |
| 258 name, group = None, None |
| 259 if pwd: |
| 260 name = pwd.getpwuid(os.getuid())[0] |
| 261 if grp: |
| 262 group = grp.getgrgid(os.getgid())[0] |
| 263 return name, group |
| 264 |
| 265 if self.finalized: |
| 266 if not (self.uid is None and self.gid is None): |
| 267 self.bus.log('Already running as uid: %r gid: %r' % |
| 268 current_ids()) |
| 269 else: |
| 270 if self.uid is None and self.gid is None: |
| 271 if pwd or grp: |
| 272 self.bus.log('uid/gid not set', level=30) |
| 273 else: |
| 274 self.bus.log('Started as uid: %r gid: %r' % current_ids()) |
| 275 if self.gid is not None: |
| 276 os.setgid(self.gid) |
| 277 os.setgroups([]) |
| 278 if self.uid is not None: |
| 279 os.setuid(self.uid) |
| 280 self.bus.log('Running as uid: %r gid: %r' % current_ids()) |
| 281 |
| 282 # umask |
| 283 if self.finalized: |
| 284 if self.umask is not None: |
| 285 self.bus.log('umask already set to: %03o' % self.umask) |
| 286 else: |
| 287 if self.umask is None: |
| 288 self.bus.log('umask not set', level=30) |
| 289 else: |
| 290 old_umask = os.umask(self.umask) |
| 291 self.bus.log('umask old: %03o, new: %03o' % |
| 292 (old_umask, self.umask)) |
| 293 |
| 294 self.finalized = True |
| 295 # This is slightly higher than the priority for server.start |
| 296 # in order to facilitate the most common use: starting on a low |
| 297 # port (which requires root) and then dropping to another user. |
| 298 start.priority = 77 |
| 299 |
| 300 |
| 301 class Daemonizer(SimplePlugin): |
| 302 """Daemonize the running script. |
| 303 |
| 304 Use this with a Web Site Process Bus via:: |
| 305 |
| 306 Daemonizer(bus).subscribe() |
| 307 |
| 308 When this component finishes, the process is completely decoupled from |
| 309 the parent environment. Please note that when this component is used, |
| 310 the return code from the parent process will still be 0 if a startup |
| 311 error occurs in the forked children. Errors in the initial daemonizing |
| 312 process still return proper exit codes. Therefore, if you use this |
| 313 plugin to daemonize, don't use the return code as an accurate indicator |
| 314 of whether the process fully started. In fact, that return code only |
| 315 indicates if the process succesfully finished the first fork. |
| 316 """ |
| 317 |
| 318 def __init__(self, bus, stdin='/dev/null', stdout='/dev/null', |
| 319 stderr='/dev/null'): |
| 320 SimplePlugin.__init__(self, bus) |
| 321 self.stdin = stdin |
| 322 self.stdout = stdout |
| 323 self.stderr = stderr |
| 324 self.finalized = False |
| 325 |
| 326 def start(self): |
| 327 if self.finalized: |
| 328 self.bus.log('Already deamonized.') |
| 329 |
| 330 # forking has issues with threads: |
| 331 # http://www.opengroup.org/onlinepubs/000095399/functions/fork.html |
| 332 # "The general problem with making fork() work in a multi-threaded |
| 333 # world is what to do with all of the threads..." |
| 334 # So we check for active threads: |
| 335 if threading.activeCount() != 1: |
| 336 self.bus.log('There are %r active threads. ' |
| 337 'Daemonizing now may cause strange failures.' % |
| 338 threading.enumerate(), level=30) |
| 339 |
| 340 # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 |
| 341 # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7) |
| 342 # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012 |
| 343 |
| 344 # Finish up with the current stdout/stderr |
| 345 sys.stdout.flush() |
| 346 sys.stderr.flush() |
| 347 |
| 348 # Do first fork. |
| 349 try: |
| 350 pid = os.fork() |
| 351 if pid == 0: |
| 352 # This is the child process. Continue. |
| 353 pass |
| 354 else: |
| 355 # This is the first parent. Exit, now that we've forked. |
| 356 self.bus.log('Forking once.') |
| 357 os._exit(0) |
| 358 except OSError: |
| 359 # Python raises OSError rather than returning negative numbers. |
| 360 exc = sys.exc_info()[1] |
| 361 sys.exit("%s: fork #1 failed: (%d) %s\n" |
| 362 % (sys.argv[0], exc.errno, exc.strerror)) |
| 363 |
| 364 os.setsid() |
| 365 |
| 366 # Do second fork |
| 367 try: |
| 368 pid = os.fork() |
| 369 if pid > 0: |
| 370 self.bus.log('Forking twice.') |
| 371 os._exit(0) # Exit second parent |
| 372 except OSError: |
| 373 exc = sys.exc_info()[1] |
| 374 sys.exit("%s: fork #2 failed: (%d) %s\n" |
| 375 % (sys.argv[0], exc.errno, exc.strerror)) |
| 376 |
| 377 os.chdir("/") |
| 378 os.umask(0) |
| 379 |
| 380 si = open(self.stdin, "r") |
| 381 so = open(self.stdout, "a+") |
| 382 se = open(self.stderr, "a+") |
| 383 |
| 384 # os.dup2(fd, fd2) will close fd2 if necessary, |
| 385 # so we don't explicitly close stdin/out/err. |
| 386 # See http://docs.python.org/lib/os-fd-ops.html |
| 387 os.dup2(si.fileno(), sys.stdin.fileno()) |
| 388 os.dup2(so.fileno(), sys.stdout.fileno()) |
| 389 os.dup2(se.fileno(), sys.stderr.fileno()) |
| 390 |
| 391 self.bus.log('Daemonized to PID: %s' % os.getpid()) |
| 392 self.finalized = True |
| 393 start.priority = 65 |
| 394 |
| 395 |
| 396 class PIDFile(SimplePlugin): |
| 397 """Maintain a PID file via a WSPBus.""" |
| 398 |
| 399 def __init__(self, bus, pidfile): |
| 400 SimplePlugin.__init__(self, bus) |
| 401 self.pidfile = pidfile |
| 402 self.finalized = False |
| 403 |
| 404 def start(self): |
| 405 pid = os.getpid() |
| 406 if self.finalized: |
| 407 self.bus.log('PID %r already written to %r.' % (pid, self.pidfile)) |
| 408 else: |
| 409 open(self.pidfile, "wb").write(ntob("%s" % pid, 'utf8')) |
| 410 self.bus.log('PID %r written to %r.' % (pid, self.pidfile)) |
| 411 self.finalized = True |
| 412 start.priority = 70 |
| 413 |
| 414 def exit(self): |
| 415 try: |
| 416 os.remove(self.pidfile) |
| 417 self.bus.log('PID file removed: %r.' % self.pidfile) |
| 418 except (KeyboardInterrupt, SystemExit): |
| 419 raise |
| 420 except: |
| 421 pass |
| 422 |
| 423 |
| 424 class PerpetualTimer(threading._Timer): |
| 425 """A responsive subclass of threading._Timer whose run() method repeats. |
| 426 |
| 427 Use this timer only when you really need a very interruptible timer; |
| 428 this checks its 'finished' condition up to 20 times a second, which can |
| 429 results in pretty high CPU usage |
| 430 """ |
| 431 |
| 432 def run(self): |
| 433 while True: |
| 434 self.finished.wait(self.interval) |
| 435 if self.finished.isSet(): |
| 436 return |
| 437 try: |
| 438 self.function(*self.args, **self.kwargs) |
| 439 except Exception: |
| 440 self.bus.log("Error in perpetual timer thread function %r." % |
| 441 self.function, level=40, traceback=True) |
| 442 # Quit on first error to avoid massive logs. |
| 443 raise |
| 444 |
| 445 |
| 446 class BackgroundTask(threading.Thread): |
| 447 """A subclass of threading.Thread whose run() method repeats. |
| 448 |
| 449 Use this class for most repeating tasks. It uses time.sleep() to wait |
| 450 for each interval, which isn't very responsive; that is, even if you call |
| 451 self.cancel(), you'll have to wait until the sleep() call finishes before |
| 452 the thread stops. To compensate, it defaults to being daemonic, which means |
| 453 it won't delay stopping the whole process. |
| 454 """ |
| 455 |
| 456 def __init__(self, interval, function, args=[], kwargs={}, bus=None): |
| 457 threading.Thread.__init__(self) |
| 458 self.interval = interval |
| 459 self.function = function |
| 460 self.args = args |
| 461 self.kwargs = kwargs |
| 462 self.running = False |
| 463 self.bus = bus |
| 464 |
| 465 def cancel(self): |
| 466 self.running = False |
| 467 |
| 468 def run(self): |
| 469 self.running = True |
| 470 while self.running: |
| 471 time.sleep(self.interval) |
| 472 if not self.running: |
| 473 return |
| 474 try: |
| 475 self.function(*self.args, **self.kwargs) |
| 476 except Exception: |
| 477 if self.bus: |
| 478 self.bus.log("Error in background task thread function %r." |
| 479 % self.function, level=40, traceback=True) |
| 480 # Quit on first error to avoid massive logs. |
| 481 raise |
| 482 |
| 483 def _set_daemon(self): |
| 484 return True |
| 485 |
| 486 |
| 487 class Monitor(SimplePlugin): |
| 488 """WSPBus listener to periodically run a callback in its own thread.""" |
| 489 |
| 490 callback = None |
| 491 """The function to call at intervals.""" |
| 492 |
| 493 frequency = 60 |
| 494 """The time in seconds between callback runs.""" |
| 495 |
| 496 thread = None |
| 497 """A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>` thread
.""" |
| 498 |
| 499 def __init__(self, bus, callback, frequency=60, name=None): |
| 500 SimplePlugin.__init__(self, bus) |
| 501 self.callback = callback |
| 502 self.frequency = frequency |
| 503 self.thread = None |
| 504 self.name = name |
| 505 |
| 506 def start(self): |
| 507 """Start our callback in its own background thread.""" |
| 508 if self.frequency > 0: |
| 509 threadname = self.name or self.__class__.__name__ |
| 510 if self.thread is None: |
| 511 self.thread = BackgroundTask(self.frequency, self.callback, |
| 512 bus = self.bus) |
| 513 self.thread.setName(threadname) |
| 514 self.thread.start() |
| 515 self.bus.log("Started monitor thread %r." % threadname) |
| 516 else: |
| 517 self.bus.log("Monitor thread %r already started." % threadname) |
| 518 start.priority = 70 |
| 519 |
| 520 def stop(self): |
| 521 """Stop our callback's background task thread.""" |
| 522 if self.thread is None: |
| 523 self.bus.log("No thread running for %s." % self.name or self.__class
__.__name__) |
| 524 else: |
| 525 if self.thread is not threading.currentThread(): |
| 526 name = self.thread.getName() |
| 527 self.thread.cancel() |
| 528 if not get_daemon(self.thread): |
| 529 self.bus.log("Joining %r" % name) |
| 530 self.thread.join() |
| 531 self.bus.log("Stopped thread %r." % name) |
| 532 self.thread = None |
| 533 |
| 534 def graceful(self): |
| 535 """Stop the callback's background task thread and restart it.""" |
| 536 self.stop() |
| 537 self.start() |
| 538 |
| 539 |
| 540 class Autoreloader(Monitor): |
| 541 """Monitor which re-executes the process when files change. |
| 542 |
| 543 This :ref:`plugin<plugins>` restarts the process (via :func:`os.execv`) |
| 544 if any of the files it monitors change (or is deleted). By default, the |
| 545 autoreloader monitors all imported modules; you can add to the |
| 546 set by adding to ``autoreload.files``:: |
| 547 |
| 548 cherrypy.engine.autoreload.files.add(myFile) |
| 549 |
| 550 If there are imported files you do *not* wish to monitor, you can adjust the |
| 551 ``match`` attribute, a regular expression. For example, to stop monitoring |
| 552 cherrypy itself:: |
| 553 |
| 554 cherrypy.engine.autoreload.match = r'^(?!cherrypy).+' |
| 555 |
| 556 Like all :class:`Monitor<cherrypy.process.plugins.Monitor>` plugins, |
| 557 the autoreload plugin takes a ``frequency`` argument. The default is |
| 558 1 second; that is, the autoreloader will examine files once each second. |
| 559 """ |
| 560 |
| 561 files = None |
| 562 """The set of files to poll for modifications.""" |
| 563 |
| 564 frequency = 1 |
| 565 """The interval in seconds at which to poll for modified files.""" |
| 566 |
| 567 match = '.*' |
| 568 """A regular expression by which to match filenames.""" |
| 569 |
| 570 def __init__(self, bus, frequency=1, match='.*'): |
| 571 self.mtimes = {} |
| 572 self.files = set() |
| 573 self.match = match |
| 574 Monitor.__init__(self, bus, self.run, frequency) |
| 575 |
| 576 def start(self): |
| 577 """Start our own background task thread for self.run.""" |
| 578 if self.thread is None: |
| 579 self.mtimes = {} |
| 580 Monitor.start(self) |
| 581 start.priority = 70 |
| 582 |
| 583 def sysfiles(self): |
| 584 """Return a Set of sys.modules filenames to monitor.""" |
| 585 files = set() |
| 586 for k, m in sys.modules.items(): |
| 587 if re.match(self.match, k): |
| 588 if hasattr(m, '__loader__') and hasattr(m.__loader__, 'archive')
: |
| 589 f = m.__loader__.archive |
| 590 else: |
| 591 f = getattr(m, '__file__', None) |
| 592 if f is not None and not os.path.isabs(f): |
| 593 # ensure absolute paths so a os.chdir() in the app doesn
't break me |
| 594 f = os.path.normpath(os.path.join(_module__file__base, f
)) |
| 595 files.add(f) |
| 596 return files |
| 597 |
| 598 def run(self): |
| 599 """Reload the process if registered files have been modified.""" |
| 600 for filename in self.sysfiles() | self.files: |
| 601 if filename: |
| 602 if filename.endswith('.pyc'): |
| 603 filename = filename[:-1] |
| 604 |
| 605 oldtime = self.mtimes.get(filename, 0) |
| 606 if oldtime is None: |
| 607 # Module with no .py file. Skip it. |
| 608 continue |
| 609 |
| 610 try: |
| 611 mtime = os.stat(filename).st_mtime |
| 612 except OSError: |
| 613 # Either a module with no .py file, or it's been deleted. |
| 614 mtime = None |
| 615 |
| 616 if filename not in self.mtimes: |
| 617 # If a module has no .py file, this will be None. |
| 618 self.mtimes[filename] = mtime |
| 619 else: |
| 620 if mtime is None or mtime > oldtime: |
| 621 # The file has been deleted or modified. |
| 622 self.bus.log("Restarting because %s changed." % filename
) |
| 623 self.thread.cancel() |
| 624 self.bus.log("Stopped thread %r." % self.thread.getName(
)) |
| 625 self.bus.restart() |
| 626 return |
| 627 |
| 628 |
| 629 class ThreadManager(SimplePlugin): |
| 630 """Manager for HTTP request threads. |
| 631 |
| 632 If you have control over thread creation and destruction, publish to |
| 633 the 'acquire_thread' and 'release_thread' channels (for each thread). |
| 634 This will register/unregister the current thread and publish to |
| 635 'start_thread' and 'stop_thread' listeners in the bus as needed. |
| 636 |
| 637 If threads are created and destroyed by code you do not control |
| 638 (e.g., Apache), then, at the beginning of every HTTP request, |
| 639 publish to 'acquire_thread' only. You should not publish to |
| 640 'release_thread' in this case, since you do not know whether |
| 641 the thread will be re-used or not. The bus will call |
| 642 'stop_thread' listeners for you when it stops. |
| 643 """ |
| 644 |
| 645 threads = None |
| 646 """A map of {thread ident: index number} pairs.""" |
| 647 |
| 648 def __init__(self, bus): |
| 649 self.threads = {} |
| 650 SimplePlugin.__init__(self, bus) |
| 651 self.bus.listeners.setdefault('acquire_thread', set()) |
| 652 self.bus.listeners.setdefault('start_thread', set()) |
| 653 self.bus.listeners.setdefault('release_thread', set()) |
| 654 self.bus.listeners.setdefault('stop_thread', set()) |
| 655 |
| 656 def acquire_thread(self): |
| 657 """Run 'start_thread' listeners for the current thread. |
| 658 |
| 659 If the current thread has already been seen, any 'start_thread' |
| 660 listeners will not be run again. |
| 661 """ |
| 662 thread_ident = get_thread_ident() |
| 663 if thread_ident not in self.threads: |
| 664 # We can't just use get_ident as the thread ID |
| 665 # because some platforms reuse thread ID's. |
| 666 i = len(self.threads) + 1 |
| 667 self.threads[thread_ident] = i |
| 668 self.bus.publish('start_thread', i) |
| 669 |
| 670 def release_thread(self): |
| 671 """Release the current thread and run 'stop_thread' listeners.""" |
| 672 thread_ident = get_thread_ident() |
| 673 i = self.threads.pop(thread_ident, None) |
| 674 if i is not None: |
| 675 self.bus.publish('stop_thread', i) |
| 676 |
| 677 def stop(self): |
| 678 """Release all threads and run all 'stop_thread' listeners.""" |
| 679 for thread_ident, i in self.threads.items(): |
| 680 self.bus.publish('stop_thread', i) |
| 681 self.threads.clear() |
| 682 graceful = stop |
| 683 |
OLD | NEW |