comparison piecrust/serving/wrappers.py @ 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 cc6f3dbe3048
children 3ceeca7bb71c
comparison
equal deleted inserted replaced
557:703ea5d76f33 558:9ab005db2592
1 import os 1 import os
2 import signal
2 import logging 3 import logging
3 import threading 4 import threading
4 import urllib.request 5 import urllib.request
5 6
6 7
25 app = _get_piecrust_server(root_dir, 26 app = _get_piecrust_server(root_dir,
26 debug=debug_piecrust, 27 debug=debug_piecrust,
27 sub_cache_dir=sub_cache_dir, 28 sub_cache_dir=sub_cache_dir,
28 run_sse_check=_run_sse_check) 29 run_sse_check=_run_sse_check)
29 30
30 # We need to run Werkzeug in a background thread because we may have some 31 # We need to do a few things to get Werkzeug to properly shutdown or
31 # SSE responses running. In theory we should be using a proper async 32 # restart while SSE responses are running. This is because Werkzeug runs
32 # server for this kind of stuff, but I'd rather avoid additional 33 # them in background threads (because we tell it to), but those threads
33 # dependencies on stuff that's not necessarily super portable. 34 # are not marked as "daemon", so when the main thread tries to exit, it
34 # Anyway we run the server in multi-threading mode, but the request 35 # will wait on those SSE responses to end, which will pretty much never
35 # threads are not set to `daemon` mode (and there's no way to set that 36 # happen (except for a timeout or the user closing their browser).
36 # flag without re-implementing `run_simple` apparently). So instead we 37 #
37 # run the server in a background thread so we keep the main thread to 38 # In theory we should be using a proper async server for this kind of
38 # ourselves here, which means we can trap `KeyboardInterrupt`, and set 39 # stuff, but I'd rather avoid additional dependencies on stuff that's not
39 # a global flag that will kill all the long-running SSE threads and make 40 # necessarily super portable.
40 # this whole thing exit cleanly and properly (hopefully). 41 #
41 def _inner(): 42 # Anyway, we run the server as usual, but we intercept the `SIGINT`
43 # signal for when the user presses `CTRL-C`. When that happens, we set a
44 # flag that will make all existing SSE loops return, which will make it
45 # possible for the main thread to end too.
46 #
47 # We also need to do a few thing for the "reloading" feature in Werkzeug,
48 # see the comment down there for more info.
49 def _shutdown_server():
50 from piecrust.serving import procloop
51 procloop.server_shutdown = True
52
53 def _shutdown_server_and_raise_sigint():
54 if not use_reloader or os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
55 # We only need to shutdown the SSE requests for the process
56 # that actually runs them.
57 print("")
58 print("Shutting server down...")
59 _shutdown_server()
60 raise KeyboardInterrupt()
61
62 signal.signal(signal.SIGINT,
63 lambda *args: _shutdown_server_and_raise_sigint())
64
65 try:
42 run_simple(host, port, app, 66 run_simple(host, port, app,
43 threaded=True, 67 threaded=True,
44 use_debugger=use_debugger, 68 use_debugger=use_debugger,
45 use_reloader=use_reloader) 69 use_reloader=use_reloader)
46 70 except SystemExit:
47 t = threading.Thread(name='WerkzeugServer', target=_inner) 71 if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
48 t.start() 72 # When using the reloader, if code has changed, the child process
49 try: 73 # will use `sys.exit` to end and let the master process restart
50 while t.is_alive(): 74 # it... we need to shutdown the SSE requests otherwise it will
51 t.join(0.5) 75 # not exit.
52 except KeyboardInterrupt: 76 _shutdown_server()
53 shutdown_url = 'http://%s:%s/__piecrust_debug/werkzeug_shutdown' % ( 77 raise
54 host, port)
55 logger.info("")
56 logger.info("Shutting down server...")
57 urllib.request.urlopen(shutdown_url)
58 finally:
59 logger.debug("Terminating push notifications...")
60 from piecrust.serving import procloop
61 procloop.server_shutdown = True
62 78
63 79
64 def run_gunicorn_server(root_dir, 80 def run_gunicorn_server(root_dir,
65 debug_piecrust=False, sub_cache_dir=None, 81 debug_piecrust=False, sub_cache_dir=None,
66 gunicorn_options=None): 82 gunicorn_options=None):