# HG changeset patch # User Ludovic Chabant # Date 1439612811 25200 # Node ID 9ab005db25922695446959f25c6395028ed04b6f # Parent 703ea5d76f33593733a438965d875b47f2f5016c serve: Improve reloading and shutdown of the preview server. See the huge comment block in the file for more infos. diff -r 703ea5d76f33 -r 9ab005db2592 piecrust/serving/wrappers.py --- 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,