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