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