Commits

Robert Brewer committed 7f808ef

Ugly fix for #321. cherrypy.server could really use some encapsulation now.

1. server.start now MUST be called from the main thread, or restart and interrupts won't work. You can stop and restart CherryPy safely now with the server.stop and server.restart methods. However, stop() only suspends the process; if you want to shut down the CP process, raise SystemExit or KeyboardInterrupt. If you need to do so in your own threads, set cherrypy._interrupt to an instance of one of those exceptions.

2. New cherrypy._httpserverclass attribute, so that threads can wait for the HTTP server to truly start.

3. server.start() now defaults serverClass to _missing, so if you were using None to get the WSGIServer, switch to _missing.

4. server has some new methods: start_app_server, start_http_server, stop_http_server, stop_app_server.

5. There's also a new start_with_callback function, so you don't have to code the threading yourself if you want to start the server but run another task in a new thread.

6. test/helper.py doesn't have startServer/stopServer methods anymore. Just call server.start/stop instead.

  • Participants
  • Parent commits 2e7786f

Comments (0)

Files changed (8)

File cherrypy/__init__.py

 # 1 = Started, ready to receive requests
 _appserver_state = 0
 _httpserver = None
+_httpserverclass = None
+_interrupt = None
 
 codecoverage = False
 

File cherrypy/_cphttpserver.py

         while self.ready:
             self.handle_request()
             if self.interrupt:
-                i = self.interrupt
-                self.interrupt = None
-                raise i
+                raise self.interrupt
     start = serve_forever
     
     def shutdown(self):
         self.ready = True
         while self.ready:
             if self.interrupt:
-                i = self.interrupt
-                self.interrupt = None
-                raise i
+                raise self.interrupt
             if not self.handle_request():
                 break
         self.server_close()

File cherrypy/_cpwsgiserver.py

             self.rfile = SizeCheckWrapper(self.rfile, mhs)
         self.wfile = self.socket.makefile("w", self.server.bufsize)
         self.sent_headers = False
+    
     def parse_request(self):
         self.sent_headers = False
         self.environ = {}
         while self.ready:
             self.tick()
             if self.interrupt:
-                i = self.interrupt
-                self.interrupt = None
-                raise i
+                raise self.interrupt
     
     def tick(self):
         try:
         # Don't join currentThread (when stop is called inside a request).
         current = threading.currentThread()
         for worker in self._workerThreads:
-            if worker is not current:
+            if worker is not current and worker.isAlive:
                 worker.join()
         
         self._workerThreads = []

File cherrypy/server.py

     - Creates a server
 """
 
-import warnings
+import cgi
 import threading
 import time
+import warnings
 
 import cherrypy
 from cherrypy import _cphttptools
 onStopThreadList = []
 
 
-def start(initOnly=False, serverClass=None):
-    defaultOn = (cherrypy.config.get("server.environment") == "development")
-    if cherrypy.config.get('autoreload.on', defaultOn):
-        # Check initOnly. If True, we're probably not starting
-        # our own webserver, and therefore could do Very Bad Things
-        # when autoreload calls sys.exit.
-        if not initOnly:
+_missing = object()
+
+def start(initOnly=False, serverClass=_missing):
+    """Main function. MUST be called from the main thread.
+    
+    Set initOnly to True to keep this function from blocking.
+    Set serverClass to None to skip starting any HTTP server.
+    """
+    # This duplicates the line in start_app_server on purpose.
+    cherrypy._appserver_state = None
+    cherrypy._interrupt = None
+    
+    conf = cherrypy.config.get
+    
+    if serverClass is _missing:
+        serverClass = conf("server.class", _missing)
+    if serverClass is _missing:
+        import _cpwsgi
+        serverClass = _cpwsgi.WSGIServer
+    elif serverClass and isinstance(serverClass, basestring):
+        # Dynamically load the class from the given string
+        serverClass = cherrypy._cputil.attributes(serverClass)
+    
+    # Autoreload, but check serverClass. If None, we're not starting
+    # our own webserver, and therefore could do Very Bad Things when
+    # autoreload calls sys.exit.
+    if serverClass is not None:
+        defaultOn = (conf("server.environment") == "development")
+        if conf('autoreload.on', defaultOn):
             try:
                 autoreload.main(_start, (initOnly, serverClass))
             except KeyboardInterrupt:
     
     _start(initOnly, serverClass)
 
-def _start(initOnly=False, serverClass=None):
-    """Main function."""
+def _start(initOnly, serverClass):
+    # This duplicates the line in start_app_server on purpose.
+    cherrypy._appserver_state = None
+    
+    cherrypy._httpserverclass = serverClass
+    conf = cherrypy.config.get
+    
     try:
         if cherrypy.codecoverage:
             from cherrypy.lib import covercp
             covercp.start()
         
-        # Use a flag to indicate the state of the cherrypy application server.
-        # 0 = Not started
-        # None = In process of starting
-        # 1 = Started, ready to receive requests
-        cherrypy._appserver_state = None
-        
         # Output config options to log
-        if cherrypy.config.get("server.logConfigOptions", True):
+        if conf("server.logConfigOptions", True):
             cherrypy.config.outputConfigMap()
-        
-        # Check the config options
-        # TODO
-        # config.checkConfigOptions()
+        # TODO: config.checkConfigOptions()
         
         # If sessions are stored in files and we
         # use threading, we need a lock on the file
-        if (cherrypy.config.get('server.threadPool') > 1
-            and cherrypy.config.get('session.storageType') == 'file'):
+        if (conf('server.threadPool') > 1
+            and conf('session.storageType') == 'file'):
             cherrypy._sessionFileLock = threading.RLock()
         
         # set cgi.maxlen which will limit the size of POST request bodies
-        import cgi
-        cgi.maxlen = cherrypy.config.get('server.maxRequestSize')
-        
-        # Call the functions from cherrypy.server.onStartServerList
-        for func in cherrypy.server.onStartServerList:
-            func()
+        cgi.maxlen = conf('server.maxRequestSize')
         
         # Set up the profiler if requested.
-        if cherrypy.config.get("profiling.on", False):
-            ppath = cherrypy.config.get("profiling.path", "")
+        if conf("profiling.on", False):
+            ppath = conf("profiling.path", "")
             cherrypy.profiler = profiler.Profiler(ppath)
         else:
             cherrypy.profiler = None
-
-        # Initilize the built in filters
+        
+        # Initialize the built in filters
         cherrypy._cputil._cpInitDefaultFilters()
         cherrypy._cputil._cpInitUserDefinedFilters()
         
-        if initOnly:
-            cherrypy._appserver_state = 1
-        else:
-            run_server(serverClass)
-    except:
-        # _start may be called as the target of a Thread, in which case
-        # any errors would pass silently. Log them at least.
-        cherrypy.log(cherrypy._cputil.formatExc())
-        raise
+        start_app_server()
+        start_http_server()
+        wait_until_ready()
+        
+        if not initOnly:
+            # Block forever (wait for KeyboardInterrupt or SystemExit).
+            while True:
+                time.sleep(.1)
+                if cherrypy._interrupt:
+                    raise cherrypy._interrupt
+    except KeyboardInterrupt:
+        cherrypy.log("<Ctrl-C> hit: shutting down server", "HTTP")
+        stop()
+    except SystemExit:
+        cherrypy.log("SystemExit raised: shutting down server", "HTTP")
+        stop()
 
+def start_app_server():
+    """Start the CherryPy core."""
+    # Use a flag to indicate the state of the cherrypy application server.
+    # 0 = Not started
+    # None = In process of starting
+    # 1 = Started, ready to receive requests
+    cherrypy._appserver_state = None
+    
+    # Call the functions from cherrypy.server.onStartServerList
+    for func in cherrypy.server.onStartServerList:
+        func()
+    
+    cherrypy._appserver_state = 1
 
-def run_server(serverClass=None):
-    """Prepare the requested server and then run it."""
+def start_http_server(serverClass=None):
+    """Start the requested HTTP server."""
+    if serverClass is None:
+        serverClass = cherrypy._httpserverclass
+    if serverClass is None:
+        return
+    
     if cherrypy._httpserver is not None:
-        warnings.warn("You seem to have an HTTP server still running."
-                      "Please call cherrypy.server.stop() before continuing.")
+        msg = ("You seem to have an HTTP server still running."
+               "Please call cherrypy.server.stop_http_server() "
+               "before continuing.")
+        warnings.warn(msg)
     
     if cherrypy.config.get('server.socketPort'):
         host = cherrypy.config.get('server.socketHost')
     cherrypy.log("Serving HTTP on %s" % onWhat, 'HTTP')
     
     # Instantiate the server.
-    if serverClass is None:
-        serverClass = cherrypy.config.get("server.class", None)
-    if serverClass and isinstance(serverClass, basestring):
-        serverClass = cherrypy._cputil.attributes(serverClass)
-    if serverClass is None:
-        import _cpwsgi
-        serverClass = _cpwsgi.WSGIServer
     cherrypy._httpserver = serverClass()
     
-    # Start the http server. Must be done after wait_for_free_port (above).
-    # Note that _httpserver.start() will block this thread, so there
-    # isn't any notification in this thread that the HTTP server is
-    # truly ready. See wait_until_ready() for all the things that
-    # other threads should wait for before proceeding with requests.
-    try:
-        cherrypy._appserver_state = 1
-        # This should block until the http server stops.
-        cherrypy._httpserver.start()
-    except KeyboardInterrupt:
-        cherrypy.log("<Ctrl-C> hit: shutting down server", "HTTP")
-        stop()
-    except SystemExit:
-        cherrypy.log("SystemExit raised: shutting down server", "HTTP")
-        stop()
+    # HTTP servers MUST be started in a new thread, so that the
+    # main thread persists to receive KeyboardInterrupt's. This
+    # wrapper traps an interrupt in the http server's main thread
+    # and shutdowns CherryPy.
+    def _start_http():
+        try:
+            cherrypy._httpserver.start()
+        except (KeyboardInterrupt, SystemExit), exc:
+            cherrypy._interrupt = exc
+    threading.Thread(target=_start_http).start()
 
 
 seen_threads = {}
                              requestLine, headers, rfile, scheme)
 
 def stop():
-    """Shutdown CherryPy (and any HTTP servers it started)."""
+    """Stop CherryPy and any HTTP servers it started."""
+    stop_http_server()
+    stop_app_server()
+
+def stop_app_server():
+    """Stop CherryPy."""
+    cherrypy._appserver_state = 0
+    cherrypy.log("CherryPy shut down", "HTTP")
+
+def stop_http_server():
+    """Stop the HTTP server."""
     try:
         httpstop = cherrypy._httpserver.stop
     except AttributeError:
         pass
     else:
-        # httpstop() should block until the server is *truly* stopped.
+        # httpstop() MUST block until the server is *truly* stopped.
         httpstop()
         cherrypy.log("HTTP Server shut down", "HTTP")
     
         func()
     
     cherrypy._httpserver = None
-    cherrypy._appserver_state = 0
-    cherrypy.log("CherryPy shut down", "HTTP")
 
 def restart():
-    """Stop and start CherryPy."""
-    http = getattr(cherrypy, '_httpserver', None)
+    """Restart CherryPy (and any HTTP servers it started)."""
     stop()
-    if http:
-        # Start the server in a new thread
-        thread_args = {"serverClass": http.__class__}
-        t = threading.Thread(target=_start, kwargs=thread_args)
-        t.start()
-    else:
-        _start(initOnly=True)
+    start_app_server()
+    start_http_server()
     wait_until_ready()
 
 def wait_until_ready():
     """Block the caller until CherryPy is ready to receive requests."""
     
+    # Wait for app to start up
     while cherrypy._appserver_state != 1:
         time.sleep(.1)
     
-    http = getattr(cherrypy, '_httpserver', None)
-    if http:
-        # Wait for HTTP server to start up
-        while not http.ready:
+    # Wait for HTTP server to start up
+    if cherrypy._httpserverclass is not None:
+        while not getattr(cherrypy._httpserver, "ready", None):
             time.sleep(.1)
         
         # Wait for port to be occupied
     
     cherrypy.log("Port %s not bound" % port, 'HTTP')
     raise cherrypy.NotReady("Port not bound.")
+
+def start_with_callback(func, args=None, kwargs=None, serverClass=_missing):
+    """Start CherryPy, then callback the given func in a new thread."""
+    if args is None:
+        args = ()
+    if kwargs is None:
+        kwargs = {}
+    args = (func,) + args
+    threading.Thread(target=_callback_intermediary, args=args, kwargs=kwargs).start()
+    cherrypy.server.start(serverClass=serverClass)
+
+def _callback_intermediary(func, *args, **kwargs):
+    wait_until_ready()
+    func(*args, **kwargs)

File cherrypy/test/helper.py

 
 
 import os, os.path
-import sys
-import time
+import re
 import socket
 import StringIO
+import sys
+import thread
 import threading
+import time
+import types
 
 import cherrypy
 import webtest
-import types
-import re
 
 for _x in dir(cherrypy):
     y = getattr(cherrypy, _x)
         webtest.ignored_exceptions.append(y)
 
 
-def startServer(serverClass=None):
-    """Start server in a new thread (same thread if serverClass is None)."""
-    if serverClass is None:
-        cherrypy.server.start(initOnly=True)
-    else:
-        t = threading.Thread(target=cherrypy.server.start,
-                             args=(False, serverClass))
-        t.start()
-    cherrypy.server.wait_until_ready()
-
-
-def stopServer():
-    """Stop the current CP server."""
-    cherrypy.server.stop()
-
-
 def onerror():
     """Assign to _cpOnError to enable webtest server-side debugging."""
     handled = webtest.server_error()
 
 class CPWebCase(webtest.WebCase):
     
+    def exit(self):
+        sys.exit()
+    
     def _getRequest(self, url, headers, method, body):
         # Like getPage, but for serverless requests.
         webtest.ServerError.on = False
     of test modules. The config, however, is reset for each module.
     """
     setConfig(conf)
-    startServer(server)
+    cherrypy.server.start_with_callback(_run_test_suite_thread,
+                                        args=(moduleNames, conf),
+                                        serverClass=server)
+
+def _run_test_suite_thread(moduleNames, conf):
+    cherrypy.server.wait_until_ready()
     for testmod in moduleNames:
         # Must run each module in a separate suite,
         # because each module uses/overwrites cherrypy globals.
         
         suite = CPTestLoader.loadTestsFromName(testmod)
         CPTestRunner.run(suite)
-    stopServer()
-
+    thread.interrupt_main()
 
 def testmain(server=None, conf=None):
     """Run __main__ as a test module, with webtest debugging."""
     if conf is None:
         conf = {}
     setConfig(conf)
-    
-    startServer(server)
+    cherrypy.server.start_with_callback(_test_main_thread, serverClass=server)
+
+def _test_main_thread():
+    cherrypy.server.wait_until_ready()
+    cherrypy._cputil._cpInitDefaultFilters()
     try:
-        cherrypy._cputil._cpInitDefaultFilters()
         webtest.main()
     finally:
-        # webtest.main == unittest.main, which raises SystemExit,
-        # so put stopServer in a finally clause
-        stopServer()
+        thread.interrupt_main()
 

File cherrypy/test/test_noserver.py

 cherrypy.root.test = HelloWorld()
 
 cherrypy.config.update({"server.environment": "production"})
-cherrypy.server.start(initOnly = True)
+cherrypy.server.start(serverClass=None)
 

File cherrypy/test/test_states.py

 """
 
 import time
+import threading
 
 import cherrypy
 
         self.assertRaises(cherrypy.NotReady, self.getPage, "/")
         
         # Test server start
-        helper.startServer(self.serverClass)
+        cherrypy.server.start(True, self.serverClass)
         self.assertEqual(cherrypy._appserver_state, 1)
         
         if self.serverClass:
         self.assertBody("Hello World")
         
         # Test server stop
-        helper.stopServer()
+        cherrypy.server.stop()
         self.assertEqual(cherrypy._appserver_state, 0)
         
         # Once the server has stopped, we should get a NotReady error again.
         self.assertRaises(cherrypy.NotReady, self.getPage, "/")
     
-    def test_1_KeyboardInterrupt(self):
-        if self.serverClass:
-            # Raise a keyboard interrupt in the HTTP server's main thread.
-            helper.startServer(self.serverClass)
-            cherrypy._httpserver.interrupt = KeyboardInterrupt
-            # Give the server time to shut down.
-            while cherrypy._appserver_state != 0:
-                time.sleep(.1)
-            self.assertEqual(cherrypy._httpserver, None)
-            self.assertEqual(cherrypy._appserver_state, 0)
-            self.assertRaises(cherrypy.NotReady, self.getPage, "/")
-            
-            # Raise a keyboard interrupt in a page handler; on multithreaded
-            # servers, this should occur in one of the worker threads.
-            # This should raise a BadStatusLine error, since the worker
-            # thread will just die without writing a response.
-            from httplib import BadStatusLine
-            helper.startServer(self.serverClass)
-            self.assertRaises(BadStatusLine, self.getPage, "/ctrlc")
-            # Give the server time to shut down.
-            while cherrypy._appserver_state != 0:
-                print ".",
-                time.sleep(.1)
-            self.assertEqual(cherrypy._httpserver, None)
-            self.assertRaises(cherrypy.NotReady, self.getPage, "/")
-    
-    def test_2_Restart(self):
+    def test_1_Restart(self):
         # Test server start
-        helper.startServer(self.serverClass)
+        cherrypy.server.start(True, self.serverClass)
         self.getPage("/")
         self.assertBody("Hello World")
         
         self.assertEqual(cherrypy._appserver_state, 1)
         self.assertBody("app was restarted succesfully")
         
-        # Now that we've restarted, test a KeyboardInterrupt (ticket 321).
+        cherrypy.server.stop()
+        self.assertEqual(cherrypy._appserver_state, 0)
+    
+    def test_2_KeyboardInterrupt(self):
         if self.serverClass:
-            cherrypy._httpserver.interrupt = KeyboardInterrupt
-            # Give the server time to shut down.
-            while cherrypy._appserver_state != 0:
-                time.sleep(.1)
+            
+            # Raise a keyboard interrupt in the HTTP server's main thread.
+            def interrupt():
+                cherrypy.server.wait_until_ready()
+                cherrypy._httpserver.interrupt = KeyboardInterrupt
+            threading.Thread(target=interrupt).start()
+            
+            # We must start the server in this, the main thread
+            cherrypy.server.start(False, self.serverClass)
+            # Time passes...
             self.assertEqual(cherrypy._httpserver, None)
+            self.assertEqual(cherrypy._appserver_state, 0)
+            self.assertRaises(cherrypy.NotReady, self.getPage, "/")
             
-            # Once the server has stopped, we should get a NotReady error again.
+            # Raise a keyboard interrupt in a page handler; on multithreaded
+            # servers, this should occur in one of the worker threads.
+            # This should raise a BadStatusLine error, since the worker
+            # thread will just die without writing a response.
+            def interrupt():
+                cherrypy.server.wait_until_ready()
+                from httplib import BadStatusLine
+                self.assertRaises(BadStatusLine, self.getPage, "/ctrlc")
+            threading.Thread(target=interrupt).start()
+            
+            cherrypy.server.start(False, self.serverClass)
+            # Time passes...
+            self.assertEqual(cherrypy._httpserver, None)
+            self.assertEqual(cherrypy._appserver_state, 0)
             self.assertRaises(cherrypy.NotReady, self.getPage, "/")
 
 
-
 def run(server, conf):
     helper.setConfig(conf)
-    cherrypy._cputil._cpInitDefaultFilters()
     ServerStateTests.serverClass = server
     suite = helper.CPTestLoader.loadTestsFromTestCase(ServerStateTests)
     try:
         helper.CPTestRunner.run(suite)
     finally:
-        helper.stopServer()
+        cherrypy.server.stop()
 
 
 if __name__ == "__main__":
     conf = {'server.socketHost': '127.0.0.1',
             'server.socketPort': 8000,
             'server.threadPool': 10,
-            'server.logToScreen': True,
+            'server.logToScreen': False,
             'server.logConfigOptions': False,
             'server.environment': "production",
             'server.showTracebacks': True,
         run(server, conf)
     _run(None)
     _run("cherrypy._cpwsgi.WSGIServer")
-##    _run("cherrypy._cphttpserver.PooledThreadServer")
-##    conf['server.threadPool'] = 1
-##    _run("cherrypy._cphttpserver.CherryHTTPServer")
-
+    _run("cherrypy._cphttpserver.PooledThreadServer")
+    conf['server.threadPool'] = 1
+    _run("cherrypy._cphttpserver.CherryHTTPServer")

File cherrypy/test/test_tutorials.py

             module = reload(sys.modules[target])
         else:
             module = __import__(target, globals(), locals(), [''])
-        
-        cherrypy.server.start(initOnly=True)
     
     def test01HelloWorld(self):
         self.load_tut_module("tut01_helloworld")