Robert Brewer avatar Robert Brewer committed 2ff3788

Trunk fix for #752 (Return cherrypy.server to a single-server model):

1. Change restsrv.servers.ServerManager (multiple httpservers) to ServerAdapter (one httpserver).
2. cherrypy.server is now a subclass of ServerAdapter, and is subscribed by default.
3. Made several plugin methods idempotent that weren't before.
4. Added names to win32 bus state events. Also fixed a buglet in win32 block().
5. Added repr to wspbus.states.State objects.
6. Did ''not'' change any callers of cherrypy.server other than what was necessary, to help prove the fixes work without breaking compatibility. Future changesets will be used to modify docs and tutorials n such.

Comments (0)

Files changed (15)

cherrypy/__init__.py

 # Add an autoreloader (the 'engine' config namespace may detach/attach it).
 engine.autoreload = restsrv.plugins.Autoreloader(engine)
 engine.autoreload.subscribe()
+
 restsrv.plugins.ThreadManager(engine).subscribe()
 
+signal_handler = restsrv.plugins.SignalHandler(engine)
+
 from cherrypy import _cpserver
 server = _cpserver.Server()
+server.subscribe()
 
 
 def quickstart(root, script_name="", config=None):
         _global_conf_alias.update(config)
     tree.mount(root, script_name, config)
     
-    engine.subscribe('start', server.quickstart)
-    restsrv.plugins.SignalHandler(engine).subscribe()
+    signal_handler.subscribe()
     engine.start()
     engine.block()
 
 log.error_file = ''
 # Using an access file makes CP about 10% slower. Leave off by default.
 log.access_file = ''
-engine.subscribe('log', lambda msg: log.error(msg, 'ENGINE'))
+
+def _buslog(msg):
+    log.error(msg, 'ENGINE')
+engine.subscribe('log', _buslog)
 
 #                       Helper functions for CP apps                       #
 

cherrypy/_cpmodpy.py

                             "tools.ignore_headers.headers": ['Range'],
                             })
     
+    cherrypy.server.unsubscribe()
     cherrypy.engine.start()
     
     def cherrypy_cleanup(data):

cherrypy/_cpserver.py

 
 import cherrypy
 from cherrypy.lib import attributes
+
+# We import * because we want to export check_port
+# et al as attributes of this module.
 from cherrypy.restsrv.servers import *
 
 
-class Server(object):
-    """Manager for a set of HTTP servers.
+class Server(ServerAdapter):
+    """An adapter for an HTTP server.
     
-    This is both a container and controller for "HTTP server" objects,
-    which are kept in Server.httpservers, a dictionary of the form:
-    {httpserver: bind_addr} where 'bind_addr' is usually a (host, port)
-    tuple.
-    
-    Most often, you will only be starting a single HTTP server. In this
-    common case, you can set attributes (like socket_host and socket_port)
+    You can set attributes (like socket_host and socket_port)
     on *this* object (which is probably cherrypy.server), and call
     quickstart. For example:
     
     
         s = MyCustomWSGIServer(wsgiapp, port=8080)
         cherrypy.server.quickstart(s)
-    
-    But if you need to start more than one HTTP server (to serve on multiple
-    ports, or protocols, etc.), you can manually register each one and then
-    control them all:
-    
-        s1 = MyWSGIServer(host='0.0.0.0', port=80)
-        s2 = another.HTTPServer(host='127.0.0.1', SSL=True)
-        cherrypy.server.httpservers = {s1: ('0.0.0.0', 80),
-                                       s2: ('127.0.0.1', 443)}
-        # Note we do not use quickstart when we define our own httpservers
-        cherrypy.server.start()
-    
-    Whether you use quickstart(), or define your own httpserver entries and
-    use start(), you'll find that the start, wait, restart, and stop methods
-    work the same way, controlling all registered httpserver objects at once.
     """
     
     socket_port = 8080
     ssl_private_key = None
     
     def __init__(self):
-        self.mgr = ServerManager(cherrypy.engine)
-    
-    def _get_httpservers(self):
-        return self.mgr.httpservers
-    def _set_httpservers(self, value):
-        self.mgr.httpservers = value
-    httpservers = property(_get_httpservers, _set_httpservers)
+        ServerAdapter.__init__(self, cherrypy.engine)
     
     def quickstart(self, server=None):
         """Start from defaults. MUST be called from the main thread.
         starts an httpserver based on the given server object (if provided)
         and attributes of self.
         """
-        httpserver, bind_addr = self.httpserver_from_self(server)
-        self.mgr.httpservers[httpserver] = bind_addr
-        self.mgr.start()
-        cherrypy.engine.subscribe('stop', self.mgr.stop)
+        self.httpserver, self.bind_addr = self.httpserver_from_self(server)
+        self.start()
     
     def httpserver_from_self(self, httpserver=None):
         """Return a (httpserver, bind_addr) pair based on self attributes."""
         return httpserver, (host, port)
     
     def start(self):
-        """Start all registered HTTP servers."""
-        self.mgr.start()
-    
-    def wait(self, httpserver=None):
-        """Wait until the HTTP server is ready to receive requests.
-        
-        If no httpserver is specified, wait for all registered httpservers.
-        """
-        self.mgr.wait(httpserver)
-    
-    def stop(self):
-        """Stop all HTTP servers."""
-        self.mgr.stop()
-    
-    def restart(self):
-        """Restart all HTTP servers."""
-        self.mgr.restart()
+        """Start the HTTP server."""
+        if not self.httpserver:
+            self.httpserver, self.bind_addr = self.httpserver_from_self()
+        ServerAdapter.start(self)
+    start.priority = 75
     
     def base(self):
-        """Return the base (scheme://host) for this server manager."""
+        """Return the base (scheme://host) for this server."""
         if self.socket_file:
             return self.socket_file
         

cherrypy/restsrv/plugins.py

     """Plugin base class which auto-subscribes methods for known channels."""
     
     def subscribe(self):
-        """Register this monitor as a (multi-channel) listener on the bus."""
+        """Register this object as a (multi-channel) listener on the bus."""
         for channel in self.bus.listeners:
             method = getattr(self, channel, None)
             if method is not None:
                 self.bus.subscribe(channel, method)
     
     def unsubscribe(self):
-        """Unregister this monitor as a listener on the bus."""
+        """Unregister this object as a listener on the bus."""
         for channel in self.bus.listeners:
             method = getattr(self, channel, None)
             if method is not None:
     def start(self):
         # forking has issues with threads:
         # http://www.opengroup.org/onlinepubs/000095399/functions/fork.html
-        # " ... The general problem with making fork() work in a multi-threaded world
-        #  is what to do with all of the threads. ... "
+        # "The general problem with making fork() work in a multi-threaded
+        #  world is what to do with all of the threads..."
         # So we check for active threads:
         if threading.activeCount() != 1:
-            self.bus.log('There are more than one active threads. Daemonizing now may cause strange failures.')
+            self.bus.log('There are more than one active threads. '
+                         'Daemonizing now may cause strange failures.')
             self.bus.log(str(threading.enumerate()))
-
+        
         # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
         # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7)
         # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
     def start(self):
         """Start our callback in its own perpetual timer thread."""
         if self.frequency > 0:
-            threadname = "restsrv %s" % self.__class__.__name__
-            self.thread = PerpetualTimer(self.frequency, self.callback)
-            self.thread.setName(threadname)
-            self.thread.start()
-            self.bus.log("Started thread %r." % threadname)
+            if self.thread is None:
+                threadname = "restsrv %s" % self.__class__.__name__
+                self.thread = PerpetualTimer(self.frequency, self.callback)
+                self.thread.setName(threadname)
+                self.thread.start()
+                self.bus.log("Started thread %r." % threadname)
+            else:
+                self.bus.log("Thread %r already started." % threadname)
     start.priority = 70
     
     def stop(self):
         """Stop our callback's perpetual timer thread."""
-        if self.thread:
+        if self.thread is None:
+            self.bus.log("No thread running for %s." % self.__class__.__name__)
+        else:
             if self.thread is not threading.currentThread():
                 self.thread.cancel()
                 self.thread.join()
     
     def start(self):
         """Start our own perpetual timer thread for self.run."""
-        self.mtimes = {}
+        if self.thread is None:
+            self.mtimes = {}
         Monitor.start(self)
     start.priority = 70 
     

cherrypy/restsrv/servers.py

-"""Manage a set of HTTP servers."""
+"""Adapt an HTTP server."""
 
 import socket
 import threading
 import time
 
 
-class ServerManager(object):
-    """Manager for a set of HTTP servers.
-    
-    This is both a container and controller for HTTP servers and gateways,
-    which are kept in Server.httpservers, a dictionary of the form:
-    {httpserver: bind_addr} where 'bind_addr' is usually a (host, port)
-    tuple.
+class ServerAdapter(object):
+    """Adapter for an HTTP server.
     
     If you need to start more than one HTTP server (to serve on multiple
     ports, or protocols, etc.), you can manually register each one and then
-    control them all:
+    start them all with bus.start:
     
-        s1 = MyWSGIServer(host='0.0.0.0', port=80)
-        s2 = another.HTTPServer(host='127.0.0.1', SSL=True)
-        server.httpservers = {s1: ('0.0.0.0', 80),
-                              s2: ('127.0.0.1', 443)}
-        server.start()
-    
-    The start, wait, restart, and stop methods control all registered
-    httpserver objects at once.
+        s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80))
+        s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True))
+        s1.subscribe()
+        s2.subscribe()
+        bus.start()
     """
     
-    
-    def __init__(self, bus):
+    def __init__(self, bus, httpserver=None, bind_addr=None):
         self.bus = bus
-        self.httpservers = {}
+        self.httpserver = httpserver
+        self.bind_addr = bind_addr
         self.interrupt = None
+        self.running = False
     
     def subscribe(self):
         self.bus.subscribe('start', self.start)
         self.bus.subscribe('stop', self.stop)
     
+    def unsubscribe(self):
+        self.bus.unsubscribe('start', self.start)
+        self.bus.unsubscribe('stop', self.stop)
+    
     def start(self):
-        """Start all registered HTTP servers."""
-        self.interrupt = None
-        if not self.httpservers:
-            raise ValueError("No HTTP servers have been created.")
-        for httpserver in self.httpservers:
-            self._start_http(httpserver)
-    start.priority = 75
-    
-    def _start_http(self, httpserver):
-        """Start the given httpserver in a new thread."""
-        bind_addr = self.httpservers[httpserver]
-        if isinstance(bind_addr, tuple):
-            wait_for_free_port(*bind_addr)
-            host, port = bind_addr
+        """Start the HTTP server."""
+        if isinstance(self.bind_addr, tuple):
+            host, port = self.bind_addr
             on_what = "%s:%s" % (host, port)
         else:
-            on_what = "socket file: %s" % bind_addr
+            on_what = "socket file: %s" % self.bind_addr
         
-        t = threading.Thread(target=self._start_http_thread, args=(httpserver,))
+        if self.running:
+            self.bus.log("Already serving on %s" % on_what)
+            return
+        
+        self.interrupt = None
+        if not self.httpserver:
+            raise ValueError("No HTTP server has been created.")
+        
+        # Start the httpserver in a new thread.
+        if isinstance(self.bind_addr, tuple):
+            wait_for_free_port(*self.bind_addr)
+        
+        t = threading.Thread(target=self._start_http_thread)
         t.setName("HTTPServer " + t.getName())
         t.start()
         
-        self.wait(httpserver)
+        self.wait()
+        self.running = True
         self.bus.log("Serving on %s" % on_what)
+    start.priority = 75
     
-    def _start_http_thread(self, httpserver):
-        """HTTP servers MUST be started in new threads, so that the
+    def _start_http_thread(self):
+        """HTTP servers MUST be running in new threads, so that the
         main thread persists to receive KeyboardInterrupt's. If an
         exception is raised in the httpserver's thread then it's
-        trapped here, and the bus (and therefore our httpservers)
+        trapped here, and the bus (and therefore our httpserver)
         are shut down.
         """
         try:
-            httpserver.start()
+            self.httpserver.start()
         except KeyboardInterrupt, exc:
-            self.bus.log("<Ctrl-C> hit: shutting down HTTP servers")
+            self.bus.log("<Ctrl-C> hit: shutting down HTTP server")
             self.interrupt = exc
             self.bus.stop()
         except SystemExit, exc:
-            self.bus.log("SystemExit raised: shutting down HTTP servers")
+            self.bus.log("SystemExit raised: shutting down HTTP server")
             self.interrupt = exc
             self.bus.stop()
             raise
             import sys
             self.interrupt = sys.exc_info()[1]
             self.bus.log("Error in HTTP server: shutting down",
-                            traceback=True)
+                         traceback=True)
             self.bus.stop()
             raise
     
-    def wait(self, httpserver=None):
-        """Wait until the HTTP server is ready to receive requests.
+    def wait(self):
+        """Wait until the HTTP server is ready to receive requests."""
+        while not getattr(self.httpserver, "ready", False):
+            if self.interrupt:
+                raise self.interrupt
+            time.sleep(.1)
         
-        If no httpserver is specified, wait for all registered httpservers.
-        """
-        if httpserver is None:
-            httpservers = self.httpservers.items()
-        else:
-            httpservers = [(httpserver, self.httpservers[httpserver])]
-        
-        for httpserver, bind_addr in httpservers:
-            while not getattr(httpserver, "ready", False):
-                if self.interrupt:
-                    raise self.interrupt
-                time.sleep(.1)
-            
-            # Wait for port to be occupied
-            if isinstance(bind_addr, tuple):
-                host, port = bind_addr
-                wait_for_occupied_port(host, port)
+        # Wait for port to be occupied
+        if isinstance(self.bind_addr, tuple):
+            host, port = self.bind_addr
+            wait_for_occupied_port(host, port)
     
     def stop(self):
-        """Stop all HTTP servers."""
-        for httpserver, bind_addr in self.httpservers.items():
-            # httpstop() MUST block until the server is *truly* stopped.
-            httpserver.stop()
+        """Stop the HTTP server."""
+        if self.running:
+            # stop() MUST block until the server is *truly* stopped.
+            self.httpserver.stop()
             # Wait for the socket to be truly freed.
-            if isinstance(bind_addr, tuple):
-                wait_for_free_port(*bind_addr)
-            self.bus.log("HTTP Server %s shut down" % httpserver)
+            if isinstance(self.bind_addr, tuple):
+                wait_for_free_port(*self.bind_addr)
+            self.running = False
+            self.bus.log("HTTP Server %s shut down" % self.httpserver)
+        else:
+            self.bus.log("HTTP Server %s already shut down" % self.httpserver)
+    stop.priority = 25
     
     def restart(self):
-        """Restart all HTTP servers."""
+        """Restart the HTTP server."""
         self.stop()
         self.start()
 

cherrypy/restsrv/win32.py

 """Windows service for restsrv. Requires pywin32."""
 
+import os
 import thread
 import win32api
 import win32con
         try:
             return self.events[state]
         except KeyError:
-            event = win32event.CreateEvent(None, 0, 0, None)
+            event = win32event.CreateEvent(None, 0, 0,
+                                           u"WSPBus %s Event (pid=%r)" %
+                                           (state.name, os.getpid()))
             self.events[state] = event
             return event
     
         Since this class uses native win32event objects, the interval
         argument is ignored.
         """
+        # Don't wait for an event that beat us to the punch ;)
+        if self.state == state:
+            return
+        
         event = self._get_state_event(state)
         try:
             win32event.WaitForSingleObject(event, win32event.INFINITE)

cherrypy/restsrv/wspbus.py

 # Use a flag to indicate the state of the bus.
 class _StateEnum(object):
     class State(object):
-        pass
+        name = None
+        def __repr__(self):
+            return "states.%s" % self.name
+    
+    def __setattr__(self, key, value):
+        if isinstance(value, self.State):
+            value.name = key
+        object.__setattr__(self, key, value)
 states = _StateEnum()
 states.STOPPED = states.State()
 states.STARTING = states.State()

cherrypy/test/helper.py

     """
     cherrypy.config.reset()
     setConfig(conf)
-    cherrypy.server.quickstart(server)
+    cherrypy.signal_handler.subscribe()
     # The Pybots automatic testing system needs the suite to exit
     # with a non-zero value if there were any problems.
     # Might as well stick it in the engine... :/
         apps.append((base, app))
     apps.sort()
     apps.reverse()
-    for s in cherrypy.server.httpservers:
-        s.wsgi_app.apps = apps
+    cherrypy.server.httpserver.wsgi_app.apps = apps
 
 def _run_test_suite_thread(moduleNames, conf):
     for testmod in moduleNames:

cherrypy/test/modpy.py

     return output
 
 
-APACHE_PATH = "apache"
+APACHE_PATH = "httpd"
 CONF_PATH = "test_mp.conf"
 
 conf_modpython_gateway = """
         mod = __import__(modname, globals(), locals(), [''])
         mod.setup_server()
         
+        cherrypy.server.unsubscribe()
         cherrypy.engine.start()
     from mod_python import apache
     return apache.OK

cherrypy/test/modwsgi.py

             "engine.SIGHUP": None,
             "engine.SIGTERM": None,
             })
+        cherrypy.server.unsubscribe()
         cherrypy.engine.start(blocking=False)
     return cherrypy.tree(environ, start_response)
 

cherrypy/test/test_conn.py

         
         old_timeout = None
         try:
-            httpserver = cherrypy.server.httpservers.keys()[0]
+            httpserver = cherrypy.server.httpserver
             old_timeout = httpserver.timeout
         except (AttributeError, IndexError):
             print "skipped ",

cherrypy/test/test_states.py

                 self.assertBody("Hello World")
                 self.assertNoHeader("Connection")
                 
-                cherrypy.server.httpservers.keys()[0].interrupt = KeyboardInterrupt
+                cherrypy.server.httpserver.interrupt = KeyboardInterrupt
                 engine.block()
                 
                 self.assertEqual(db_connection.running, False)
         if exit_code == 0:
             self.fail("Process failed to return nonzero exit code.")
 
-    def test_6_Start_Error_With_Daemonize(self):
-        if not self.server_class:
-            print "skipped (no server) ",
-            return
-        
-        # Start the demo script in a new process
-        demoscript = os.path.join(os.getcwd(), os.path.dirname(__file__),
-                                  "test_states_demo.py")
-        host = cherrypy.server.socket_host
-        port = cherrypy.server.socket_port
-        
-        # If a process errors during start, it should stop the engine
-        # and exit with a non-zero exit code, even if we daemonize soon
-        # thereafter.
-        args = [sys.executable, demoscript, host, str(port), '-starterror', '-daemonize']
-        if self.scheme == "https":
-            args.append('-ssl')
-        exit_code = os.spawnl(os.P_WAIT, sys.executable, *args)
-        if exit_code == 0:
-            self.fail("Process failed to return nonzero exit code.")
-        time.sleep(2) # Wait for the daemonized process to exit.
 
-
-class DaemonizeTest(helper.CPWebCase):
+class DaemonizeTests(helper.CPWebCase):
     
-    def test_Daemonize(self):
+    def test_1_Daemonize(self):
         if not self.server_class:
             print "skipped (no server) ",
             return
         # that we wait for the daemon to finish running before we fail.
         if exit_code != 0:
             self.fail("Daemonized process failed to exit cleanly")
+    
+    def test_2_Start_Error_With_Daemonize(self):
+        if not self.server_class:
+            print "skipped (no server) ",
+            return
+        if os.name not in ['posix']: 
+            print "skipped (not on posix) ",
+            return
+        
+        # Start the demo script in a new process
+        demoscript = os.path.join(os.getcwd(), os.path.dirname(__file__),
+                                  "test_states_demo.py")
+        host = cherrypy.server.socket_host
+        port = cherrypy.server.socket_port
+        
+        # If a process errors during start, it should stop the engine
+        # and exit with a non-zero exit code, even if we daemonize soon
+        # thereafter.
+        args = [sys.executable, demoscript, host, str(port), '-starterror', '-daemonize']
+        if self.scheme == "https":
+            args.append('-ssl')
+        exit_code = os.spawnl(os.P_WAIT, sys.executable, *args)
+        if exit_code == 0:
+            self.fail("Process failed to return nonzero exit code.")
+        time.sleep(2) # Wait for the daemonized process to exit.
 
 
 def run(server, conf):
     helper.setConfig(conf)
     ServerStateTests.server_class = server
-    DaemonizeTest.server_class = server
+    DaemonizeTests.server_class = server
     suite = helper.CPTestLoader.loadTestsFromTestCase(ServerStateTests)
-    daemon_suite = helper.CPTestLoader.loadTestsFromTestCase(DaemonizeTest)
+    daemon_suite = helper.CPTestLoader.loadTestsFromTestCase(DaemonizeTests)
     try:
         try:
             import pyconquer
             }
     
     if host:
-        DaemonizeTest.HOST = ServerStateTests.HOST = host
+        DaemonizeTests.HOST = ServerStateTests.HOST = host
     
     if port:
-        DaemonizeTest.PORT = ServerStateTests.PORT = port
+        DaemonizeTests.PORT = ServerStateTests.PORT = port
     
     if ssl:
         localDir = os.path.dirname(__file__)
         serverpem = os.path.join(os.getcwd(), localDir, 'test.pem')
         conf['server.ssl_certificate'] = serverpem
         conf['server.ssl_private_key'] = serverpem
-        DaemonizeTest.scheme = ServerStateTests.scheme = "https"
-        DaemonizeTest.HTTP_CONN = ServerStateTests.HTTP_CONN = httplib.HTTPSConnection
+        DaemonizeTests.scheme = ServerStateTests.scheme = "https"
+        DaemonizeTests.HTTP_CONN = ServerStateTests.HTTP_CONN = httplib.HTTPSConnection
     
     def _run(server):
         print

cherrypy/test/test_states_demo.py

     start.exposed = True
     
     def stop(self):
+        # This handler might be called before the engine is STARTED if an
+        # HTTP worker thread handles it before the HTTP server returns
+        # control to engine.start. We avoid that race condition here
+        # by waiting for the Bus to be STARTED.
+        cherrypy.engine.block(state=cherrypy.engine.states.STARTED)
         cherrypy.engine.stop()
     stop.exposed = True
 
             "server.socket_port": int(sys.argv[2]),
             "log.screen": False,
             "log.error_file": os.path.join(thisdir, 'test_states_demo.error.log'),
+            "log.access_file": os.path.join(thisdir, 'test_states_demo.access.log'),
             }
     
     if '-ssl' in sys.argv[3:]:
         plugins.Daemonizer(cherrypy.engine).subscribe()
         plugins.PIDFile(cherrypy.engine, PID_file_path).subscribe()
     
-    cherrypy.engine.subscribe('start', cherrypy.server.quickstart, priority=75)
-    
     if '-starterror' in sys.argv[3:]:
         cherrypy.engine.subscribe('start', lambda: 1/0, priority=6)
     

cherrypy/test/test_tools.py

     def testEndRequestOnDrop(self):
         old_timeout = None
         try:
-            httpserver = cherrypy.server.httpservers.keys()[0]
+            httpserver = cherrypy.server.httpserver
             old_timeout = httpserver.timeout
         except (AttributeError, IndexError):
             print "skipped ",

cherrypy/test/test_wsgi_ns.py

 class WSGI_Namespace_Test(helper.CPWebCase):
     
     def test_pipeline(self):
-        if not cherrypy.server.httpservers:
+        if not cherrypy.server.httpserver:
             print "skipped ",
             return
         
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.