werkzeug使用reloader可以在文件被改變時(shí)自動(dòng)加載更改過的文件,使用方法也很簡單,run_simple('localhost', 4000, application,use_reloader=True),ues_reloader=True即可。本文試圖去品讀一下reloader的實(shí)現(xiàn)以及一些小細(xì)節(jié)。
原理
先概述下整個(gè)reloader的原理,看起來會(huì)舒服一些。
非reloader的啟動(dòng)很簡單,會(huì)調(diào)用make_server方法,然后調(diào)用serve_forever()去循環(huán)獲取新的請求。
而reloader的機(jī)制,會(huì)起一個(gè)子進(jìn)程,子進(jìn)程有兩個(gè)線程,一個(gè)線程會(huì)去跑server,一個(gè)線程去監(jiān)控文件是否變動(dòng),如果文件發(fā)生變動(dòng),子進(jìn)程會(huì)退出,并返回返回碼3(自定義的返回碼,標(biāo)識因?yàn)槲募兓顺觯?。父進(jìn)程檢測子進(jìn)程的退出碼,并加以判斷,如果是3,則重復(fù)上面的步驟,去再啟動(dòng)一次子進(jìn)程,當(dāng)然,此時(shí)加載的文件都會(huì)是新的文件了。
代碼角度
接下來從代碼的角度出發(fā),看下整個(gè)流程。
入口
def inner():
try:
fd = int(os.environ['WERKZEUG_SERVER_FD'])
except (LookupError, ValueError):
fd = None
srv = make_server(hostname, port, application, threaded,
processes, request_handler,
passthrough_errors, ssl_context,
fd=fd)
if fd is None:
log_startup(srv.socket)
srv.serve_forever()
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(get_sockaddr(hostname, port, address_family))
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()
# Do not use relative imports, otherwise "python -m werkzeug.serving"
# breaks.
from werkzeug._reloader import run_with_reloader
run_with_reloader(inner, extra_files, reloader_interval,
reloader_type)
else:
inner()
上面就是use_reloader起作用部分的代碼??梢钥吹剑褂昧?code>use_reloader之后相比較沒加做了很多事情(廢話 = = )。接下去會(huì)挑這幾行代碼里的需要注意的點(diǎn)講下。
-
WERKZEUG_RUN_MAIN
WERKZEUG_RUN_MAIN在這里其實(shí)還沒賦值,看不太出具體的作用,可以在后面再看。初始肯定是null,第一次執(zhí)行這幾行代碼的時(shí)候是會(huì)進(jìn)入到if語句的(實(shí)際上這幾行代碼在每次代碼更新執(zhí)行reloader的時(shí)候都會(huì)重復(fù)進(jìn)入,后面再說) -
can_open_by_fd
這個(gè)參數(shù)是前面定義的,
can_open_by_fd = not WIN and hasattr(socket, 'fromfd'),先不管windows系統(tǒng)下的情況,后面的fromdfd方法的解釋如下create a socket object from an open file descriptor [*]
即從文件描述符創(chuàng)建一個(gè)socket。后面會(huì)創(chuàng)建一個(gè)socket,并把socket的文件描述符保存起來,方面?zhèn)鬟f。(實(shí)際上會(huì)在父進(jìn)程子進(jìn)程之間進(jìn)行傳遞)
-
socket.SO_REUSEADDR
允許使用
TIME_WAIT的端口。我們知道,TIME_WAIT狀態(tài)下的端口是無法使用的,加上socket.SO_REUSEADDR參數(shù)后使這個(gè)socket的端口之后可以重復(fù)使用。 -
為什么直接創(chuàng)建一個(gè)socket,而不是在
inner中使用make_server去創(chuàng)建?因?yàn)樾枰獋鬟ffd,在整個(gè)程序的入口需要先行創(chuàng)建。在后邊我們會(huì)看到,子進(jìn)程回去使用fd去創(chuàng)建socket(或者說是從fd恢復(fù)socket)
-
inner
在
use_reloader為true的情況下,fd是存在的,會(huì)運(yùn)行一個(gè)server,并且使用該fd對應(yīng)的socket
在處理完是否為WERKZEUG_RUN_MAIN的情況后,程序進(jìn)入run_with_reloader方法。
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, lambda *args: sys.exit(0))
try:
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
t = threading.Thread(target=main_func, args=())
t.setDaemon(True)
t.start()
reloader.run()
else:
sys.exit(reloader.restart_with_reloader())
except KeyboardInterrupt:
pass
先往下看下if語句。同樣的,第一次進(jìn)入,還沒賦值WERKZEUG_RUN_MAIN,會(huì)進(jìn)去sys.exit(reloader.restart_with_reloader()),會(huì)把reloader.restart_with_reloader()的返回值作為程序的退出碼。
reloader_loops是一個(gè)監(jiān)控文件變化的類,有兩個(gè)實(shí)現(xiàn),分別是StatReloaderLoop以及WatchDogReloaderLoop,二者區(qū)別在于監(jiān)控文件變動(dòng)的方法不同。
ReloaderLoop
class ReloaderLoop(object):
name = None
_sleep = staticmethod(time.sleep)
def __init__(self, extra_files=None, interval=1):
# 接受 extra_files 參數(shù),除了監(jiān)控.py的變化以外,還會(huì)監(jiān)控 extra_files 列表中所有文件的變化
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):
while 1:
_log('info', ' * Restarting with %s' % self.name)
# 獲取到啟動(dòng)腳本,如['/usr/bin/python','test.py']
args = _get_args_for_reloading()
# 把環(huán)境變量(包括前面的fd等)
new_environ = os.environ.copy()
new_environ['WERKZEUG_RUN_MAIN'] = 'true'
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)
trigger_reload方法是供子類去調(diào)用的,子類監(jiān)控到文件的變化時(shí)會(huì)去調(diào)用trigger_reload,并且使進(jìn)程退出,退出碼為3(3在這里表示這因?yàn)槲募兓顺觯?/p>
可以看到,ReloaderLoop的restart_with_reloader方法會(huì)去啟動(dòng)一個(gè)子進(jìn)程,并賦予所有的環(huán)境變量(包括fd),子進(jìn)程會(huì)去帶上WERKZEUG_RUN_MAIN參數(shù)重新去跑下前面的run_simple方法。并且會(huì)捕獲子進(jìn)程的退出碼,如上面講的,如果返回的是3的話,表示文件變化而倒是子進(jìn)程退出,直接重啟就好了,即繼續(xù)循環(huán),啟動(dòng)子進(jìn)程;如果程序是因?yàn)槠渌蛲顺龅模瑒t返回返回碼。
子進(jìn)程
接下來,我們看看子進(jìn)程會(huì)做些什么。截止到上面的分析,我們知道,子進(jìn)程相比較原先的父進(jìn)程,目前唯一的泣別就是環(huán)境變量中WERKZEUG_RUN_MAIN為true,而這個(gè)字段會(huì)在兩個(gè)地方會(huì)用到,一是最開始的if use_reloader:判斷中,有這個(gè)字段的則不會(huì)去創(chuàng)建socket(畢竟父進(jìn)程已經(jīng)創(chuàng)建完成且把fd放在了環(huán)境變量中),二是run_with_reloader方法中。讓我們再看下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, lambda *args: sys.exit(0))
try:
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
t = threading.Thread(target=main_func, args=())
t.setDaemon(True)
t.start()
reloader.run()
else:
sys.exit(reloader.restart_with_reloader())
except KeyboardInterrupt:
pass
子進(jìn)程到達(dá)了這個(gè)方法,會(huì)啟動(dòng)一個(gè)線程,運(yùn)行main_func方法,也就是最開始的inner方法,用來啟動(dòng)一個(gè)server,該線程會(huì)被設(shè)置為deamon線程,即守護(hù)線程。守護(hù)線程會(huì)在其他線程退出后自動(dòng)退出。
另外,reloader會(huì)運(yùn)行run()方法,作用是監(jiān)控文件的變化,并調(diào)用trigger_reload方法,在文件發(fā)生變化時(shí)退出,并返回3返回碼。
還有一點(diǎn),signal.signal(signal.SIGTERM, lambda *args: sys.exit(0)),這句看起來很簡單,就是捕獲signal.SIGTERM信號,也就是捕獲kill或者是ctrl + c,并且退出。不過這里我還是有點(diǎn)疑問,為什么需要這個(gè)呢?加了信號之后唯一的區(qū)別,本來子進(jìn)程退出會(huì)返回一個(gè)負(fù)數(shù),加上之后會(huì)返回0。0代表著命令的成功執(zhí)行,難道就是為了讓程序更加'美麗'?
再看ReloaderLoop
到了這里,整個(gè)流程算是理通了,就是我們一開始的原理。但還有一個(gè)問題我們之前一直選擇跳過,就是ReloaderLoop的具體實(shí)現(xiàn)。我們前面說到,他有兩個(gè)實(shí)現(xiàn),分別為StatReloaderLoop以及WatchdogReloaderLoop。
reloader_loops = {
'stat': StatReloaderLoop,
'watchdog': WatchdogReloaderLoop,
}
try:
__import__('watchdog.observers')
except ImportError:
reloader_loops['auto'] = reloader_loops['stat']
else:
reloader_loops['auto'] = reloader_loops['watchdog']
接下來,我們會(huì)細(xì)致得去看一下具體的實(shí)現(xiàn)。
StatReloaderLoop
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)
StatReloaderLoop的實(shí)現(xiàn)很簡單,就是挨個(gè)去看文件的上次修改時(shí)間來確認(rèn)文件是否發(fā)生改變,需要注意的是,如果interval比較小而文件又比較多的情況下,這個(gè)方法會(huì)很耗資源(顯而易見),剩下的沒啥好說的...
WatchdogReloaderLoop
WatchdogReloaderLoop依賴了第三方的庫watchdog,這是一個(gè)可以監(jiān)控文件變化的庫,跨平臺,運(yùn)維用的比較多。他允許自定義監(jiān)控一系列文件的變化,并在變化時(shí)調(diào)用相應(yīng)的handler。
class WatchdogReloaderLoop(ReloaderLoop):
def __init__(self, *args, **kwargs):
ReloaderLoop.__init__(self, *args, **kwargs)
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
self.observable_paths = set()
# 根據(jù)發(fā)生變化的文件名,確定是否需要重啟(如果變動(dòng)了一個(gè)不重要的小文件就沒必要重啟了)
def _check_modification(filename):
if filename in self.extra_files:
self.trigger_reload(filename)
dirname = os.path.dirname(filename)
if dirname.startswith(tuple(self.observable_paths)):
if filename.endswith(('.pyc', '.pyo', '.py')):
self.trigger_reload(filename)
# 定義一個(gè)處理器類,分別處理不懂改變時(shí)要做的事(都是調(diào)用_check_modification方法)
class _CustomHandler(FileSystemEventHandler):
def on_created(self, event): _check_modification(event.src_path)
def on_modified(self, event): ...
def on_moved(self, event): ...
def on_deleted(self, event): ...
self.observer_class = Observer
self.event_handler = _CustomHandler()
self.should_reload = False
def trigger_reload(self, filename):
# 調(diào)用會(huì)發(fā)生在handler中,退出沒什么卵用,所以覆寫了這個(gè)方法,讓run循環(huán)自動(dòng)退出
self.should_reload = True
self.log_reload(filename)
def run(self):
watches = {}
observer = self.observer_class()
observer.start()
try:
while not self.should_reload:
# 使用watchdog去檢查文件是否發(fā)生變化,并使用handler去處理。
finally:
# observer是一個(gè)線程,讓observer也正常退出
observer.stop()
observer.join()
# 返回3,標(biāo)識文件發(fā)生變化
sys.exit(3)
這部分代碼很長,我把不太重要的省略掉了。代碼比較簡單,注釋都卸載里邊了,簡單的說就是使用watchdog的方式去調(diào)用處理文件變化的事件,并按正常流程退出。
小結(jié)
werkzeug的代碼真的很神,很多可以看的地方,比如父進(jìn)程通過環(huán)境變量給子進(jìn)程傳遞信息,父進(jìn)程創(chuàng)建socket并獲取其fd,子進(jìn)程通過fd去創(chuàng)建socket,即便在重啟的過程中也不至于connection refused,再比如使用退出碼讓子進(jìn)程給父進(jìn)程傳遞信息,再比如清晰的邏輯,各個(gè)環(huán)節(jié)的劃分,reloader具體實(shí)現(xiàn)類的抽象等,都很值得學(xué)習(xí)。
我在看這代碼之前想了很久,如果我來做reloader機(jī)制會(huì)如何去做,反正我做能實(shí)現(xiàn)功能就不錯(cuò)了...希望自己的代碼有一天能這么好看吧。