changeset 558:9ab005db2592

serve: Improve reloading and shutdown of the preview server. See the huge comment block in the file for more infos.
author Ludovic Chabant <ludovic@chabant.com>
date Fri, 14 Aug 2015 21:26:51 -0700
parents 703ea5d76f33
children b95977d97652
files piecrust/serving/wrappers.py
diffstat 1 files changed, 44 insertions(+), 28 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/serving/wrappers.py	Wed Aug 12 23:20:16 2015 -0700
+++ b/piecrust/serving/wrappers.py	Fri Aug 14 21:26:51 2015 -0700
@@ -1,4 +1,5 @@
 import os
+import signal
 import logging
 import threading
 import urllib.request
@@ -27,38 +28,53 @@
                                sub_cache_dir=sub_cache_dir,
                                run_sse_check=_run_sse_check)
 
-    # We need to run Werkzeug in a background thread because we may have some
-    # SSE responses running. In theory we should be using a proper async
-    # server for this kind of stuff, but I'd rather avoid additional
-    # dependencies on stuff that's not necessarily super portable.
-    # Anyway we run the server in multi-threading mode, but the request
-    # threads are not set to `daemon` mode (and there's no way to set that
-    # flag without re-implementing `run_simple` apparently). So instead we
-    # run the server in a background thread so we keep the main thread to
-    # ourselves here, which means we can trap `KeyboardInterrupt`, and set
-    # a global flag that will kill all the long-running SSE threads and make
-    # this whole thing exit cleanly and properly (hopefully).
-    def _inner():
+    # We need to do a few things to get Werkzeug to properly shutdown or
+    # restart while SSE responses are running. This is because Werkzeug runs
+    # them in background threads (because we tell it to), but those threads
+    # are not marked as "daemon", so when the main thread tries to exit, it
+    # will wait on those SSE responses to end, which will pretty much never
+    # happen (except for a timeout or the user closing their browser).
+    #
+    # In theory we should be using a proper async server for this kind of
+    # stuff, but I'd rather avoid additional dependencies on stuff that's not
+    # necessarily super portable.
+    #
+    # Anyway, we run the server as usual, but we intercept the `SIGINT`
+    # signal for when the user presses `CTRL-C`. When that happens, we set a
+    # flag that will make all existing SSE loops return, which will make it
+    # possible for the main thread to end too.
+    #
+    # We also need to do a few thing for the "reloading" feature in Werkzeug,
+    # see the comment down there for more info.
+    def _shutdown_server():
+        from piecrust.serving import procloop
+        procloop.server_shutdown = True
+
+    def _shutdown_server_and_raise_sigint():
+        if not use_reloader or os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
+            # We only need to shutdown the SSE requests for the process
+            # that actually runs them.
+            print("")
+            print("Shutting server down...")
+            _shutdown_server()
+        raise KeyboardInterrupt()
+
+    signal.signal(signal.SIGINT,
+                  lambda *args: _shutdown_server_and_raise_sigint())
+
+    try:
         run_simple(host, port, app,
                    threaded=True,
                    use_debugger=use_debugger,
                    use_reloader=use_reloader)
-
-    t = threading.Thread(name='WerkzeugServer', target=_inner)
-    t.start()
-    try:
-        while t.is_alive():
-            t.join(0.5)
-    except KeyboardInterrupt:
-        shutdown_url = 'http://%s:%s/__piecrust_debug/werkzeug_shutdown' % (
-                host, port)
-        logger.info("")
-        logger.info("Shutting down server...")
-        urllib.request.urlopen(shutdown_url)
-    finally:
-        logger.debug("Terminating push notifications...")
-        from piecrust.serving import procloop
-        procloop.server_shutdown = True
+    except SystemExit:
+        if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
+            # When using the reloader, if code has changed, the child process
+            # will use `sys.exit` to end and let the master process restart
+            # it... we need to shutdown the SSE requests otherwise it will
+            # not exit.
+            _shutdown_server()
+        raise
 
 
 def run_gunicorn_server(root_dir,