Package cherrypy :: Package process :: Module plugins
[hide private]
[frames] | no frames]

Source Code for Module cherrypy.process.plugins

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