品讀 werkzeug reloader 實(shí)現(xiàn)機(jī)制

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>

可以看到,ReloaderLooprestart_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ò)了...希望自己的代碼有一天能這么好看吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一. 操作系統(tǒng)概念 操作系統(tǒng)位于底層硬件與應(yīng)用軟件之間的一層.工作方式: 向下管理硬件,向上提供接口.操作系統(tǒng)進(jìn)行...
    月亮是我踢彎得閱讀 6,159評論 3 28
  • 必備的理論基礎(chǔ) 1.操作系統(tǒng)作用: 隱藏丑陋復(fù)雜的硬件接口,提供良好的抽象接口。 管理調(diào)度進(jìn)程,并將多個(gè)進(jìn)程對硬件...
    drfung閱讀 3,757評論 0 5
  • 大綱 一.Socket簡介 二.BSD Socket編程準(zhǔn)備 1.地址 2.端口 3.網(wǎng)絡(luò)字節(jié)序 4.半相關(guān)與全相...
    VD2012閱讀 2,700評論 0 5
  • 一、Python簡介和環(huán)境搭建以及pip的安裝 4課時(shí)實(shí)驗(yàn)課主要內(nèi)容 【Python簡介】: Python 是一個(gè)...
    _小老虎_閱讀 6,331評論 0 10
  • 2018年9月29日 星期六 晴轉(zhuǎn)陰 393篇 今天早上因?yàn)橛悬c(diǎn)事稍微耽擱了一會(huì)兒,我準(zhǔn)備送坤坤去上學(xué)時(shí),坤坤突然...
    我是快樂的老爸閱讀 170評論 0 4

友情鏈接更多精彩內(nèi)容