這兩天在看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
歡迎拍磚磚交流╭(╯^╰)╮