好记性不如烂笔头

0%

Python自动监控代码修改进行reload

最近在用 grpc[1] ,发现 grpc 的 Python server 目前还没有像 Flask 那样的修改后自动 reload ,开发不是很方便。

所以就看看有什么比较好的实现,发现 werkzeug[2] 已经有个比较好的实现,而且 Flask 用的就是它。就不用重复发明轮子了。

假设我们的启动 server 的代码写在了 run_server 里面,我们可以将其传入到 werkzeug 的 run_with_reloader ,就会拥有监控文件改变自动 reload 的功能。

1
2
3
4
5
6
7
from werkzeug._reloader import run_with_reloader

main_func = partial(run_server, grpc_host, grpc_port, concurrent)
if autoreload:
run_with_reloader(main_func)
else:
main_func()

原理

入口程序(主进程)进入 run_with_reloader 后,因为此时环境变量中没有 WERKZEUG_RUN_MAIN,所以不会运行主逻辑 run_server 。而是会取出自己的命令行参数,设置好 WERKZEUG_RUN_MAIN 环境变量,通过 subprocess 创建一个自己,这个时候子进程判断设置了 WERKZEUG_RUN_MAIN,此时才运行真正的程序逻辑。

werkzeug 的 reloader 封装了 Stat 和 WatchDog 两种 reloader ,当子进程监控到文件改变后,会调用 trigger_reload 退出自己。然后主进程判断特殊的返回码3后再次启动子进程。

从而完成了监控文件改变,自动 reload 自己的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class ReloaderLoop(object):
# ...
def restart_with_reloader(self):
"""Spawn a new Python interpreter with the same arguments as this one,
but running the reloader thread.
"""
while 1:
_log('info', ' * Restarting with %s' % self.name)
args = _get_args_for_reloading()
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 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

  1. https://github.com/grpc/grpc ↩︎

  2. werkzeug.pocoo.org ↩︎