python web框架autoreload原理(以bottle爲例)

    這兩天在看bottle的時候,發現它也有代碼auto reload的功能,就到它的源碼中看了一下。python

    當設置reloader=True的時候,主進程不會啓動bottle服務,而是使用與主進程啓動時相同的命令行參數建立一個新的子進程。而後主進程不斷忙等待子進程結束,拿到子進程的return code,若是子進程返回的code爲3,則從新以相同的命令行參數從新啓動子進程,以前的代碼的改動就被從新reload了。在子進程中,主線程在跑bottle的服務,另一個線程在不斷的check全部import的module文件是否修改(check原理以後會在代碼中看到),若是檢測到文件的改動,check線程會發送一個KeyboardInterrupt exception到主線程,kill掉bottle的服務,而後子進程以returncode=3退出。git

    在bottle源碼中,autoreload功能主要涉及兩個地方一個是run函數,另一個是FileCheckerThread類。github

  先看一下run函數部分的代碼片斷(reloader部分帶註釋的bottle源碼 :https://github.com/kagxin/recipes/blob/master/bottle/bottle.py)。服務器

reloader 爲True是開啓autoreload功能app

if reloader and not os.environ.get('BOTTLE_CHILD'):  # reloader 爲True,且環境變量中的BOTTLE_CHILD沒有設置的時候,執行reloader建立新的子進程的邏輯
        import subprocess
        lockfile = None
        try:
            fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock')  # 臨時文件是惟一的
            os.close(fd)  # We only need this file to exist. We never write to it
            while os.path.exists(lockfile):
                args = [sys.executable] + sys.argv  # 拿到完整的命令行參數
                environ = os.environ.copy()
                environ['BOTTLE_CHILD'] = 'true'
                environ['BOTTLE_LOCKFILE'] = lockfile  # 設置兩個環境變量
                print(args, lockfile)
                p = subprocess.Popen(args, env=environ)  # 子進程的環境變量中,BOTTLE_CHILD設置爲true字符串,這子進程不會再進入if reloader and not os.environ.get('BOTTLE_CHILD') 這個分支,而是執行以後分支開啓bottle服務器
                while p.poll() is None:  # Busy wait...  等待運行bottle服務的子進程結束
                    os.utime(lockfile, None)  # I am alive!  更新lockfile文件,的access time 和 modify time
                    time.sleep(interval)
                if p.poll() != 3:
                    if os.path.exists(lockfile): os.unlink(lockfile)
                    sys.exit(p.poll())
        except KeyboardInterrupt:
            pass
        finally:
            if os.path.exists(lockfile):  # 清楚lockfile
                os.unlink(lockfile)
        return
    ...
    ...

代碼分析:函數

    程序執行,當reloader爲True並且環境變量中沒有BOTTLE_CHILD的時候,執行以後邏輯,BOTTLE_CHILD這個環境變量是用來的在Popen使用命令行參數啓動子進程的時候,讓啓動的子進程不要進入當前分支,而是直接執行以後啓動bottle服務的邏輯。this

    先不要關注lockfile文件,它的主要做用是讓子進程經過判斷它的modify time是否更新,來判斷主進程是否依然存活。while p.poll() is None:... 這段代碼是在忙等待子進程結束,同時使用os.utime不斷更新lockfile的aceess time和modify time。若是returncode==3說明子進程因文件修改而結束,則在當前循環中經過popen使用相同的命令行從新啓動子進程。命令行

if reloader:
        lockfile = os.environ.get('BOTTLE_LOCKFILE')
        bgcheck = FileCheckerThread(lockfile, interval)  # 在當前進程中,建立用於check文件改變的線程
        with bgcheck:  # FileCheckerThread 實現了,上下文管理器協議, 
            server.run(app)
        if bgcheck.status == 'reload':  # 監控的module文件發生改變,以returncode=3退出子進程,父進程會拿到這個returncode從新啓動一個子進程,即bottle服務進程
            sys.exit(3)
    else:
        server.run(app)

代碼分析:線程

    這個是子進程中的主體部分,在bgcheck這上下文管理器中,運行bottle服務,server.run(app)是阻塞的直到收到主線程結束信號。在這個上下文管理器中,運行着一個check文件改動的線程。若是文件改動就會向當前主線程發送KeyboardInterrupt終止server.run(app)。上下文管理器退出時會忽略這個KeyboardInterrupt異常,而後以returncode==3退出子進程。code

class FileCheckerThread(threading.Thread):
    """ Interrupt main-thread as soon as a changed module file is detected,
        the lockfile gets deleted or gets too old. """

    def __init__(self, lockfile, interval):
        threading.Thread.__init__(self)
        self.daemon = True
        self.lockfile, self.interval = lockfile, interval
        #: Is one of 'reload', 'error' or 'exit'
        self.status = None

    def run(self):
        exists = os.path.exists
        mtime = lambda p: os.stat(p).st_mtime
        files = dict()

        for module in list(sys.modules.values()):
            path = getattr(module, '__file__', '')
            if path[-4:] in ('.pyo', '.pyc'): path = path[:-1]
            if path and exists(path): files[path] = mtime(path)  # 拿到全部導入模塊文件的modify time

        while not self.status:
            if not exists(self.lockfile)\
            or mtime(self.lockfile) < time.time() - self.interval - 5:
                self.status = 'error'
                thread.interrupt_main()
            for path, lmtime in list(files.items()):
                if not exists(path) or mtime(path) > lmtime:  # 若是文件發生改動,
                    self.status = 'reload'
                    thread.interrupt_main()  # raise 一個 KeyboardInterrupt exception in 主線程
                    break 
            time.sleep(self.interval)

    def __enter__(self):
        self.start()

    def __exit__(self, exc_type, *_):
        if not self.status: self.status = 'exit'  # silent exit
        self.join()
        return exc_type is not None and issubclass(exc_type, KeyboardInterrupt)

代碼分析:

    這個類有__enter__和__exit__這兩個dunder方法,實現了上下文管理器協議。在進入這個上下文管理器的時候,啓動這個線程,退出時等待線程結束,且忽略了KeyboardInterrupt異常,由於__exit__返回True以外的值時,with中的異常纔會向上冒泡。

    在run方法中在for module in list(sys.modules.values()):...這個for循環中拿到全部module文件的modify time。而後在以後的while循環中,監測文件改動,若是有改動調用thread.interrupt_main(),在主線程(bottle所在線程)中raise,KeyboardInterrupt異常。

上面就是整個bottle auto reload機制的代碼。

reloader部分帶註釋的bottle源碼 :

https://github.com/kagxin/recipes/blob/master/bottle/bottle.py

歡迎拍磚磚交流╭(╯^╰)╮

相關文章
相關標籤/搜索