diff --git a/sanic/app.py b/sanic/app.py index 05d99c08..b0f51dc1 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1,3 +1,4 @@ +import os import logging import logging.config import re @@ -22,6 +23,7 @@ from sanic.static import register as static_register from sanic.testing import SanicTestClient from sanic.views import CompositionView from sanic.websocket import WebSocketProtocol, ConnectionClosed +import sanic.reloader_helpers as reloader_helpers class Sanic: @@ -604,7 +606,7 @@ class Sanic: def run(self, host=None, port=None, debug=False, ssl=None, sock=None, workers=1, protocol=None, backlog=100, stop_event=None, register_sys_signals=True, - access_log=True): + access_log=True, auto_reload=False): """Run the HTTP Server and listen until keyboard interrupt or term signal. On termination, drain connections before closing. @@ -638,12 +640,16 @@ class Sanic: host=host, port=port, debug=debug, ssl=ssl, sock=sock, workers=workers, protocol=protocol, backlog=backlog, register_sys_signals=register_sys_signals, - access_log=access_log) + access_log=access_log, auto_reload=auto_reload) try: self.is_running = True if workers == 1: - serve(**server_settings) + if os.name == 'posix' and auto_reload and \ + os.environ.get('MAIN_PROCESS_RUNNED') != 'true': + reloader_helpers.watchdog(2) + else: + serve(**server_settings) else: serve_multiple(server_settings, workers) except BaseException: @@ -733,7 +739,8 @@ class Sanic: def _helper(self, host=None, port=None, debug=False, ssl=None, sock=None, workers=1, loop=None, protocol=HttpProtocol, backlog=100, stop_event=None, - register_sys_signals=True, run_async=False, access_log=True): + register_sys_signals=True, run_async=False, access_log=True, + auto_reload=False): """Helper function used by `run` and `create_server`.""" if isinstance(ssl, dict): # try common aliaseses @@ -799,14 +806,16 @@ class Sanic: if self.configure_logging and debug: logger.setLevel(logging.DEBUG) - if self.config.LOGO is not None: + + if self.config.LOGO is not None and \ + os.environ.get('MAIN_PROCESS_RUNNED') != 'true': logger.debug(self.config.LOGO) if run_async: server_settings['run_async'] = True # Serve - if host and port: + if host and port and os.environ.get('MAIN_PROCESS_RUNNED') != 'true': proto = "http" if ssl is not None: proto = "https" diff --git a/sanic/reloader_helpers.py b/sanic/reloader_helpers.py new file mode 100644 index 00000000..24c6fca9 --- /dev/null +++ b/sanic/reloader_helpers.py @@ -0,0 +1,120 @@ +import os +import sys +import signal +import subprocess +from time import sleep +from multiprocessing import Process + + +def _iter_module_files(): + """This iterates over all relevant Python files. It goes through all + loaded files from modules, all files in folders of already loaded modules + as well as all files reachable through a package. + """ + # The list call is necessary on Python 3 in case the module + # dictionary modifies during iteration. + for module in list(sys.modules.values()): + if module is None: + continue + filename = getattr(module, '__file__', None) + if filename: + old = None + while not os.path.isfile(filename): + old = filename + filename = os.path.dirname(filename) + if filename == old: + break + else: + if filename[-4:] in ('.pyc', '.pyo'): + filename = filename[:-1] + yield filename + + +def _get_args_for_reloading(): + """Returns the executable.""" + rv = [sys.executable] + rv.extend(sys.argv) + return rv + + +def restart_with_reloader(): + """Create a new process and a subprocess in it + with the same arguments as this one. + """ + args = _get_args_for_reloading() + new_environ = os.environ.copy() + new_environ['MAIN_PROCESS_RUNNED'] = 'true' + cmd = ' '.join(args) + worker_process = Process( + target=subprocess.call, args=(cmd,), + kwargs=dict(shell=True, env=new_environ)) + worker_process.start() + return worker_process + + +def kill_process_children_unix(pid): + """Find and kill child process of a process (maximum two level). + + :param pid: PID of process (process ID) + :return: Nothing + """ + root_proc_path = "/proc/%s/task/%s/children" % (pid, pid) + if not os.path.isfile(root_proc_path): + return + with open(root_proc_path) as children_list_file: + children_list_pid = children_list_file.read().split() + + for child_pid in children_list_pid: + children_proc_path = "/proc/%s/task/%s/children" % \ + (child_pid, child_pid) + if not os.path.isfile(children_proc_path): + continue + with open(children_proc_path) as children_list_file_2: + children_list_pid_2 = children_list_file_2.read().split() + for _pid in children_list_pid_2: + os.kill(int(_pid), signal.SIGTERM) + + +def kill_program_completly(proc): + """Kill worker and it's child processes and exit. + + :param proc: worker process (process ID) + :return: Nothing + """ + kill_process_children_unix(proc.pid) + proc.terminate() + os._exit(0) + + +def watchdog(sleep_interval): + """Whatch project files, restart worker process if a change happened. + + :param sleep_interval: interval in second. + :return: Nothing + """ + + mtimes = {} + worker_process = restart_with_reloader() + signal.signal( + signal.SIGTERM, lambda *args: kill_program_completly(worker_process)) + signal.signal( + signal.SIGINT, lambda *args: kill_program_completly(worker_process)) + while True: + for filename in _iter_module_files(): + try: + mtime = os.stat(filename).st_mtime + except OSError: + continue + + old_time = mtimes.get(filename) + if old_time is None: + mtimes[filename] = mtime + continue + elif mtime > old_time: + kill_process_children_unix(worker_process.pid) + worker_process = restart_with_reloader() + + mtimes[filename] = mtime + break + + sleep(sleep_interval)