Add auto reloader.

This commit is contained in:
Yaser Amiri 2017-12-07 16:30:54 +03:30
parent 1b0ad2c3cd
commit 52c2a8484e
2 changed files with 135 additions and 6 deletions

View File

@ -1,3 +1,4 @@
import os
import logging import logging
import logging.config import logging.config
import re import re
@ -22,6 +23,7 @@ from sanic.static import register as static_register
from sanic.testing import SanicTestClient from sanic.testing import SanicTestClient
from sanic.views import CompositionView from sanic.views import CompositionView
from sanic.websocket import WebSocketProtocol, ConnectionClosed from sanic.websocket import WebSocketProtocol, ConnectionClosed
import sanic.reloader_helpers as reloader_helpers
class Sanic: class Sanic:
@ -604,7 +606,7 @@ class Sanic:
def run(self, host=None, port=None, debug=False, ssl=None, def run(self, host=None, port=None, debug=False, ssl=None,
sock=None, workers=1, protocol=None, sock=None, workers=1, protocol=None,
backlog=100, stop_event=None, register_sys_signals=True, 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 """Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing. signal. On termination, drain connections before closing.
@ -638,11 +640,15 @@ class Sanic:
host=host, port=port, debug=debug, ssl=ssl, sock=sock, host=host, port=port, debug=debug, ssl=ssl, sock=sock,
workers=workers, protocol=protocol, backlog=backlog, workers=workers, protocol=protocol, backlog=backlog,
register_sys_signals=register_sys_signals, register_sys_signals=register_sys_signals,
access_log=access_log) access_log=access_log, auto_reload=auto_reload)
try: try:
self.is_running = True self.is_running = True
if workers == 1: if workers == 1:
if os.name == 'posix' and auto_reload and \
os.environ.get('MAIN_PROCESS_RUNNED') != 'true':
reloader_helpers.watchdog(2)
else:
serve(**server_settings) serve(**server_settings)
else: else:
serve_multiple(server_settings, workers) serve_multiple(server_settings, workers)
@ -733,7 +739,8 @@ class Sanic:
def _helper(self, host=None, port=None, debug=False, def _helper(self, host=None, port=None, debug=False,
ssl=None, sock=None, workers=1, loop=None, ssl=None, sock=None, workers=1, loop=None,
protocol=HttpProtocol, backlog=100, stop_event=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`.""" """Helper function used by `run` and `create_server`."""
if isinstance(ssl, dict): if isinstance(ssl, dict):
# try common aliaseses # try common aliaseses
@ -799,14 +806,16 @@ class Sanic:
if self.configure_logging and debug: if self.configure_logging and debug:
logger.setLevel(logging.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) logger.debug(self.config.LOGO)
if run_async: if run_async:
server_settings['run_async'] = True server_settings['run_async'] = True
# Serve # Serve
if host and port: if host and port and os.environ.get('MAIN_PROCESS_RUNNED') != 'true':
proto = "http" proto = "http"
if ssl is not None: if ssl is not None:
proto = "https" proto = "https"

120
sanic/reloader_helpers.py Normal file
View File

@ -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)