werkzeug中reloader的实现
在用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: 167-130-643
可以看出服务器会自动检测是哪个文件发生了改变并自动重启,接下来就看看它是怎么实现的,这是当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()来调用函数,程序中也没地方捕获这个异常啊以及那个信号处理函数的作用,两者有关系吗有大牛帮忙解答一下吗