werkzeug中reloader的实现

时间:2023-03-08 21:53:04

在用flask开发时,如果把use_reloader设为True(debug设为True也能实现),那当你修改了app代码或调用环境发生改变时,服务器会自动重启,如下

 * Detected change in '/home/steinliber/flask-source-code/route/a.py', reloading
* Restarting with stat
* Debugger is active!
* Debugger pin code: --

可以看出服务器会自动检测是哪个文件发生了改变并自动重启,接下来就看看它是怎么实现的,这是当use_reloader开启时相关函数调用情况

/usr/lib/python2.7/threading.py(783)__bootstrap()
-> self.__bootstrap_inner()
/usr/lib/python2.7/threading.py(810)__bootstrap_inner()
-> self.run()
/usr/lib/python2.7/threading.py(763)run()
-> self.__target(*self.__args, **self.__kwargs)
/home/steinliber/flask-source-code/env/local/lib/python2.7/site-packages/werkzeug/serving.py(657)inner()

可以看出该进程创建了线程并实现服务器功能

在函数run_simple中,会对use_reloader进行判断

  if use_reloader:
# If we're not running already in the subprocess that is the
# reloader we want to open up a socket early to make sure the
# port is actually available.
if os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
if port == 0 and not can_open_by_fd:
raise ValueError('Cannot bind to a random port with enabled '
'reloader if the Python interpreter does '
'not support socket opening by fd.') # Create and destroy a socket so that any exceptions are
# raised before we spawn a separate Python interpreter and
# lose this ability.
address_family = select_ip_version(hostname, port)
s = socket.socket(address_family, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((hostname, port))
if hasattr(s, 'set_inheritable'):
s.set_inheritable(True) # If we can open the socket by file descriptor, then we can just
# reuse this one and our socket will survive the restarts.
if can_open_by_fd:
os.environ['WERKZEUG_SERVER_FD'] = str(s.fileno())
s.listen(LISTEN_QUEUE)
log_startup(s)
else:
s.close() from ._reloader import run_with_reloader
run_with_reloader(inner, extra_files, reloader_interval,
reloader_type)

WERKZEUG_RUN_MAIN用于判断是否开启了子进程,如果没有进入子进程,就创建socket,(注释说的是创建并关闭socket去产生所有异常在创建子进程之前)若socket可以用fd打开就保持socket打开方便重用。接着就是调用run_with_reloader了,这是相关测试代码

def run_with_reloader(main_func, extra_files=None, interval=1,
reloader_type='auto'):
"""Run the given function in an independent python interpreter."""
import signal
reloader = reloader_loops[reloader_type](extra_files, interval)
signal.signal(signal.SIGTERM, doit)
try:
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
t = threading.Thread(target=main_func, args=())
t.setDaemon(True)
t.start()
print reloader,t
reloader.run()
else:
print 'here'
sys.exit(reloader.restart_with_reloader())
except KeyboardInterrupt:
pass

def doit(*args):
    print 'here im'
    sys.exit(0)


class ReloaderLoop(object):
name = None # monkeypatched by testsuite. wrapping with `staticmethod` is required in
# case time.sleep has been replaced by a non-c function (e.g. by
# `eventlet.monkey_patch`) before we get here
_sleep = staticmethod(time.sleep) def __init__(self, extra_files=None, interval=1):
self.extra_files = set(os.path.abspath(x)
for x in extra_files or ())
self.interval = interval def run(self):
pass def restart_with_reloader(self):
"""Spawn a new Python interpreter with the same arguments as this one,
but running the reloader thread.
"""
while 1:
_log('info', ' * Restarting with %s' % self.name)
args = [sys.executable] + sys.argv
new_environ = os.environ.copy()
new_environ['WERKZEUG_RUN_MAIN'] = 'true' # a weird bug on windows. sometimes unicode strings end up in the
# environment and subprocess.call does not like this, encode them
# to latin1 and continue.
print new_environ
print args
if os.name == 'nt' and PY2:
for key, value in iteritems(new_environ):
if isinstance(value, text_type):
new_environ[key] = value.encode('iso-8859-1') exit_code = subprocess.call(args, env=new_environ,
close_fds=False)
if exit_code != 3:
return exit_code def trigger_reload(self, filename):
self.log_reload(filename)
sys.exit(3) def log_reload(self, filename):
filename = os.path.abspath(filename)
_log('info', ' * Detected change in %r, reloading' % filename)

里面我加入了一些测试的print函数,当重启服务器时可以看到

* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
here
* Restarting with stat
{'LC_NUMERIC': 'zh_CN.UTF-8', 'WERKZEUG_SERVER_FD': '3', 'XDG_GREETER_DATA_DIR': '/var/lib/lightdm-data/steinliber', 'GNOME_DESKTOP_SESSION_ID': 'this-is-deprecated', 'LC_MEASUREMENT': 'zh_CN.UTF-8', 'UPSTART_EVENTS': 'started starting', 'XDG_CURRENT_DESKTOP': 'Unity', 'LC_PAPER': 'zh_CN.UTF-8', 'LOGNAME': 'steinliber', 'XDG_SEAT': 'seat0', 'PATH': '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games', 'XDG_VTNR': '7', 'GNOME_KEYRING_CONTROL': '/run/user/1000/keyring-ZGJRph', 'ZSH': '/home/steinliber/.oh-my-zsh', 'DISPLAY': ':0', 'LANG': 'en_US.UTF-8', 'TERM': 'xterm', 'SHELL': '/usr/bin/zsh', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session0', 'XAUTHORITY': '/home/steinliber/.Xauthority', 'LANGUAGE': 'en_US', 'SHLVL': '1', 'MANDATORY_PATH': '/usr/share/gconf/ubuntu.mandatory.path', 'COMPIZ_CONFIG_PROFILE': 'ubuntu', 'UPSTART_INSTANCE': '', 'JOB': 'gnome-session', 'WINDOWID': '62921612', 'SESSIONTYPE': 'gnome-session', 'XMODIFIERS': '@im=fcitx', 'GPG_AGENT_INFO': '/run/user/1000/keyring-ZGJRph/gpg:0:1', 'HOME': '/home/steinliber', 'QT4_IM_MODULE': 'fcitx', 'SELINUX_INIT': 'YES', 'QT_QPA_PLATFORMTHEME': 'appmenu-qt5', 'XDG_RUNTIME_DIR': '/run/user/1000', 'GTK_IM_MODULE': 'fcitx', 'LC_ADDRESS': 'zh_CN.UTF-8', 'WERKZEUG_RUN_MAIN': 'true', 'SSH_AUTH_SOCK': '/run/user/1000/keyring-ZGJRph/ssh', 'VTE_VERSION': '3409', 'LC_CTYPE': 'en_US.UTF-8', 'GDMSESSION': 'ubuntu', 'UPSTART_JOB': 'unity-settings-daemon', 'UPSTART_SESSION': 'unix:abstract=/com/ubuntu/upstart-session/1000/1673', 'XDG_DATA_DIRS': '/usr/share/ubuntu:/usr/share/gnome:/usr/local/share/:/usr/share/', 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'XDG_SESSION_ID': 'c1', 'DBUS_SESSION_BUS_ADDRESS': 'unix:abstract=/tmp/dbus-8A5Emo2M1r', '_': '/usr/bin/python', 'DEFAULTS_PATH': '/usr/share/gconf/ubuntu.default.path', 'LC_IDENTIFICATION': 'zh_CN.UTF-8', 'DESKTOP_SESSION': 'ubuntu', 'LSCOLORS': 'Gxfxcxdxbxegedabagacad', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ubuntu:/usr/share/upstart/xdg:/etc/xdg', 'GNOME_KEYRING_PID': '1763', 'OLDPWD': '/home/steinliber', 'GDM_LANG': 'en_US', 'LC_TELEPHONE': 'zh_CN.UTF-8', 'GTK_MODULES': 'overlay-scrollbar:unity-gtk-module', 'LC_MONETARY': 'zh_CN.UTF-8', 'INSTANCE': 'Unity', 'PWD': '/home/steinliber/werkzeug', 'COLORTERM': 'gnome-terminal', 'LC_NAME': 'zh_CN.UTF-8', 'LC_TIME': 'zh_CN.UTF-8', 'LESS': '-R', 'PAGER': 'less', 'USER': 'steinliber'}
['/usr/bin/python', 'a.py']
* Debugger is active!
* Debugger pin code: 291-357-613
<werkzeug._reloader.StatReloaderLoop object at 0x7f34c55c7950> <Thread(Thread-1, started daemon 139864609179392)>

 

也就是说在调用run_with_reloader时因为此时并不是子进程,会调用sys.exit(reloader.restart_with_reloader())这个方法,之后就调用reloader.restart_with_reloader(),想了好久不知道为什么以及如何通过sys.exit()来调用这个方法,信号处理函数也没触发,哪位大牛可以帮忙解答下?

之后在restart_with_reloader中,得到了当时的环境变量以及启动的程序名,把环境变量设为子进程运行,通过subprocess方法来创建子进程即重新运行app,在子进程中,在run_with_reloader中会创建一个线程来运行服务器函数inner(),并将该线程设为守护线程,当主线程终止时,这个进程会被强制结束,而这个子线程就负责创建服务器服务。父线程调用reloader.run(),

class StatReloaderLoop(ReloaderLoop):
name = 'stat' def run(self):
mtimes = {}
while 1:
for filename in chain(_iter_module_files(),
self.extra_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:
self.trigger_reload(filename)
self._sleep(self.interval)

这是一个比较简单的reloader类,基本上的作用就是每隔参数提供的秒数就检测app所用的模块或文件的最后修改时间是否发生了改变,如果改变,就说明文件发生了修改,就调用trigger.reloader(filename)方法。这个方法就是登记了发现的改变,然后调用sys.exit(3),在run_with_reloader中可以看到若返回的状态码是3,就重新循环。重新取得模块环境以及APP名,然后再根据这些创建子进程。

这里要实现APP代码更改后,服务器马上根据新的配置重启,要先创建个子进程该进程取得当时的环境变量来调用,子进程又创建了子线程来运行服务器,而主线程就负责监视相关文件的变化,若发生了改变,就退出进程,子进程是守护进程也马上结束。主进程接受到状态码若为3,就重新进入循环,讲的有点绕,我的理解基本就是这样。

搞不明白的就是为什么要用sys.exit()来调用函数,程序中也没地方捕获这个异常啊以及那个信号处理函数的作用,两者有关系吗有大牛帮忙解答一下吗