Source

CherryPy / cherrypy / _cpengine.py

The branch 'cherrypy-3.0.x' does not exist.
Full commit
"""Create and manage the CherryPy application engine."""

import cgi
import os
import re
import signal
import sys
import threading
import time

import cherrypy
from cherrypy import _cprequest

# Use a flag to indicate the state of the application engine.
STOPPED = 0
STARTING = None
STARTED = 1


class PerpetualTimer(threading._Timer):
    
    def run(self):
        while True:
            self.finished.wait(self.interval)
            if self.finished.isSet():
                return
            self.function(*self.args, **self.kwargs)


class Engine(object):
    """Interface for (HTTP) applications, plus process controls.
    
    Servers and gateways should not instantiate Request objects directly.
    Instead, they should ask an Engine object for a request via the
    Engine.request method.
    
    Blocking is completely optional! The Engine's blocking, signal and
    interrupt handling, privilege dropping, and autoreload features are
    not a good idea when driving CherryPy applications from another
    deployment tool (but an Engine is a great deployment tool itself).
    By calling start(blocking=False), you avoid blocking and interrupt-
    handling issues. By setting Engine.SIGHUP and Engine.SIGTERM to None,
    you can completely disable the signal handling (and therefore disable
    autoreloads triggered by SIGHUP). Set Engine.autoreload_on to False
    to disable autoreload entirely.
    """
    
    # Configurable attributes
    request_class = _cprequest.Request
    response_class = _cprequest.Response
    deadlock_poll_freq = 60
    autoreload_on = True
    autoreload_frequency = 1
    autoreload_match = ".*"
    
    def __init__(self):
        self.state = STOPPED
        
        # Startup/shutdown hooks
        self.on_start_engine_list = []
        self.on_stop_engine_list = []
        self.on_start_thread_list = []
        self.on_stop_thread_list = []
        self.seen_threads = {}
        
        self.servings = []
        
        self.mtimes = {}
        self.reload_files = []
        
        self.monitor_thread = None
    
    def start(self, blocking=True):
        """Start the application engine."""
        self.state = STARTING
        
        cherrypy.checker()
        
        for func in self.on_start_engine_list:
            func()
        
        self.state = STARTED
        
        self._set_signals()
        
        freq = self.deadlock_poll_freq
        if freq > 0:
            self.monitor_thread = PerpetualTimer(freq, self.monitor)
            self.monitor_thread.setName("CPEngine Monitor")
            self.monitor_thread.start()
        
        if blocking:
            self.block()
    
    def block(self):
        """Block forever (wait for stop(), KeyboardInterrupt or SystemExit)."""
        try:
            while self.state != STOPPED:
                # Note that autoreload_frequency controls
                # sleep timer even if autoreload is off.
                time.sleep(self.autoreload_frequency)
                if self.autoreload_on:
                    self.autoreload()
        except KeyboardInterrupt:
            cherrypy.log("<Ctrl-C> hit: shutting down app engine", "ENGINE")
            cherrypy.server.stop()
            self.stop()
        except SystemExit:
            cherrypy.log("SystemExit raised: shutting down app engine", "ENGINE")
            cherrypy.server.stop()
            self.stop()
            raise
        except:
            # Don't bother logging, since we're going to re-raise.
            # Note that we don't stop the HTTP server here.
            self.stop()
            raise
    
    def reexec(self):
        """Re-execute the current process."""
        cherrypy.server.stop()
        self.stop()
        
        args = sys.argv[:]
        cherrypy.log("Re-spawning %s" % " ".join(args), "ENGINE")
        args.insert(0, sys.executable)
        
        if sys.platform == "win32":
            args = ['"%s"' % arg for arg in args]
        
        # Some platforms (OS X) will error if all threads are not
        # ABSOLUTELY terminated. See http://www.cherrypy.org/ticket/581.
        for trial in xrange(self.reexec_retry * 10):
            try:
                os.execv(sys.executable, args)
                return
            except OSError, x:
                if x.errno != 45:
                    raise
                time.sleep(0.1)
        else:
            raise
    
    # Number of seconds to retry reexec if os.execv fails.
    reexec_retry = 2
    
    def autoreload(self):
        """Reload the process if registered files have been modified."""
        sysfiles = []
        for k, m in sys.modules.items():
            if re.match(self.autoreload_match, k):
                if hasattr(m, "__loader__"):
                    if hasattr(m.__loader__, "archive"):
                        k = m.__loader__.archive
                k = getattr(m, "__file__", None)
                sysfiles.append(k)
        
        for filename in sysfiles + self.reload_files:
            if filename:
                if filename.endswith(".pyc"):
                    filename = filename[:-1]
                
                oldtime = self.mtimes.get(filename, 0)
                if oldtime is None:
                    # Module with no .py file. Skip it.
                    continue
                
                try:
                    mtime = os.stat(filename).st_mtime
                except OSError:
                    # Either a module with no .py file, or it's been deleted.
                    mtime = None
                
                if filename not in self.mtimes:
                    # If a module has no .py file, this will be None.
                    self.mtimes[filename] = mtime
                else:
                    if mtime is None or mtime > oldtime:
                        # The file has been deleted or modified.
                        self.reexec()
    
    def stop(self):
        """Stop the application engine."""
        if self.state != STOPPED:
            for thread_ident, i in self.seen_threads.iteritems():
                for func in self.on_stop_thread_list:
                    func(i)
            self.seen_threads.clear()
            
            for func in self.on_stop_engine_list:
                func()
            
            if self.monitor_thread:
                self.monitor_thread.cancel()
                self.monitor_thread.join()
                self.monitor_thread = None
            
            self.state = STOPPED
            cherrypy.log("CherryPy shut down", "ENGINE")
    
    def restart(self):
        """Restart the application engine (does not block)."""
        self.stop()
        self.start(blocking=False)
    
    def wait(self):
        """Block the caller until ready to receive requests (or error)."""
        while not (self.state == STARTED):
            time.sleep(.1)
    
    def request(self, local_host, remote_host, scheme="http",
                server_protocol="HTTP/1.1"):
        """Obtain and return an HTTP Request object. (Core)
        
        local_host should be an http.Host object with the server info.
        remote_host should be an http.Host object with the client info.
        scheme: either "http" or "https"; defaults to "http"
        """
        if self.state == STOPPED:
            req = NotReadyRequest("The CherryPy engine has stopped.")
        elif self.state == STARTING:
            req = NotReadyRequest("The CherryPy engine could not start.")
        else:
            # Only run on_start_thread_list if the engine is running.
            threadID = threading._get_ident()
            if threadID not in self.seen_threads:
                i = len(self.seen_threads) + 1
                self.seen_threads[threadID] = i
                
                for func in self.on_start_thread_list:
                    func(i)
            req = self.request_class(local_host, remote_host, scheme,
                                     server_protocol)
        resp = self.response_class()
        cherrypy.serving.load(req, resp)
        self.servings.append((req, resp))
        return req
    
    def release(self):
        """Close and de-reference the current request and response. (Core)"""
        req = cherrypy.serving.request
        
        try:
            req.close()
        except:
            cherrypy.log(traceback=True)
        
        try:
            self.servings.remove((req, cherrypy.serving.response))
        except ValueError:
            pass
        
        cherrypy.serving.clear()
    
    def monitor(self):
        """Check timeout on all responses. (Internal)"""
        if self.state == STARTED:
            for req, resp in self.servings:
                resp.check_timeout()
    
    def start_with_callback(self, func, args=None, kwargs=None):
        """Start the given func in a new thread, then start self and block."""
        
        if args is None:
            args = ()
        if kwargs is None:
            kwargs = {}
        args = (func,) + args
        
        def _callback(func, *a, **kw):
            self.wait()
            func(*a, **kw)
        t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
        t.setName("CPEngine Callback " + t.getName())
        t.start()
        
        self.start()
    
    
    #                           Signal handling                           #
    
    SIGHUP = None
    SIGTERM = None
    
    if hasattr(signal, "SIGHUP"):
        def SIGHUP(self, signum=None, frame=None):
            self.reexec()
    
    if hasattr(signal, "SIGTERM"):
        def SIGTERM(self, signum=None, frame=None):
            cherrypy.server.stop()
            self.stop()
    
    def _set_signals(self):
        try:
            if self.SIGHUP:
                signal.signal(signal.SIGHUP, self.SIGHUP)
            if self.SIGTERM:
                signal.signal(signal.SIGTERM, self.SIGTERM)
        except ValueError, x:
            if x.args[0] != 'signal only works in main thread':
                raise
    
    #                           Drop privileges                           #
    
    # Special thanks to Gavin Baker: http://antonym.org/node/100.
    try:
        import pwd, grp
    except ImportError:
        try:
            os.umask
        except AttributeError:
            def drop_privileges(self):
                """Drop privileges. Not implemented on this platform."""
                raise NotImplementedError
        else:
            umask = None
            
            def drop_privileges(self):
                """Drop privileges. Windows version (umask only)."""
                if self.umask is not None:
                    old_umask = os.umask(self.umask)
                    cherrypy.log('umask old: %03o, new: %03o' %
                                 (old_umask, self.umask), "PRIV")
    else:
        uid = None
        gid = None
        umask = None
        
        def drop_privileges(self):
            """Drop privileges. UNIX version (uid, gid, and umask)."""
            if not (self.uid is None and self.gid is None):
                if self.uid is None:
                    uid = None
                elif isinstance(self.uid, basestring):
                    uid = self.pwd.getpwnam(self.uid)[2]
                else:
                    uid = self.uid
                
                if self.gid is None:
                    gid = None
                elif isinstance(self.gid, basestring):
                    gid = self.grp.getgrnam(self.gid)[2]
                else:
                    gid = self.gid
                
                def names():
                    name = self.pwd.getpwuid(os.getuid())[0]
                    group = self.grp.getgrgid(os.getgid())[0]
                    return name, group
                
                cherrypy.log('Started as %r/%r' % names(), "PRIV")
                if gid is not None:
                    os.setgid(gid)
                if uid is not None:
                    os.setuid(uid)
                cherrypy.log('Running as %r/%r' % names(), "PRIV")
            
            if self.umask is not None:
                old_umask = os.umask(self.umask)
                cherrypy.log('umask old: %03o, new: %03o' %
                             (old_umask, self.umask), "PRIV")


class NotReadyRequest:
    
    throw_errors = True
    show_tracebacks = True
    error_page = {}
    
    def __init__(self, msg):
        self.msg = msg
        self.protocol = (1,1)
    
    def close(self):
        pass
    
    def run(self, method, path, query_string, protocol, headers, rfile):
        self.method = "GET"
        cherrypy.HTTPError(503, self.msg).set_response()
        cherrypy.response.finalize()
        return cherrypy.response