这两天在看bottle的时候,发现它也有代码auto reload的功能,就到它的源码中看了一下。
当设置reloader=True的时候,主进程不会启动bottle服务,而是使用与主进程启动时相同的命令行参数创建一个新的子进程。然后主进程不断忙等待子进程结束,拿到子进程的return code,如果子进程返回的code为3,则重新以相同的命令行参数重新启动子进程,之前的代码的改动就被重新reload了。在子进程中,主线程在跑bottle的服务,另外一个线程在不断的check所有import的module文件是否修改(check原理之后会在代码中看到),如果检测到文件的改动,check线程会发送一个KeyboardInterrupt exception到主线程,kill掉bottle的服务,然后子进程以returncode=3退出。
在bottle源码中,autoreload功能主要涉及两个地方一个是run函数,另外一个是FileCheckerThread类。
先看一下run函数部分的代码片段(reloader部分带注释的bottle源码 :https://github.com/kagxin/recipes/blob/master/bottle/bottle.py)。
reloader 为True是开启autoreload功能
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服务的逻辑。
先不要关注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退出子进程。
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机制的代码。