在用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()來調用函數,程序中也沒地方捕獲這個異常啊以及那個信號處理函數的作用,兩者有關系嗎有大牛幫忙解答一下嗎