django中自动重载机制探究

Python 2017-11-13

起步

出于好奇,想看看 django 中是如何监听文件的变化,并实现自动重载的。经过分析,它的流程大致是这样的,django 程序启动的时候,会启动两个进程(不是线程),在主线程上,监听文件的变化,当发现有文件变化时,重新启动子进程;而那个子进程就是具体的 web 服务。

两个进程

关于重载的实现方式在 django/utils/autoreload.py 中,重启的设置在 python_reloader 函数中:

def restart_with_reloader():
    while True:
        args = [sys.executable] + ['-W%s' % o for o in sys.warnoptions] + sys.argv
        if sys.platform == "win32":
            args = ['"%s"' % arg for arg in args]
        new_environ = os.environ.copy()
        new_environ["RUN_MAIN"] = 'true'
        exit_code = os.spawnve(os.P_WAIT, sys.executable, args, new_environ)
        if exit_code != 3:
            return exit_code

def python_reloader(main_func, args, kwargs):
    if os.environ.get("RUN_MAIN") == "true":
        thread.start_new_thread(main_func, args, kwargs)
        try:
            reloader_thread()
        except KeyboardInterrupt:
            pass
    else:
        try:
            exit_code = restart_with_reloader()
            if exit_code < 0:
                os.kill(os.getpid(), -exit_code)
            else:
                sys.exit(exit_code)
        except KeyboardInterrupt:
            pass

python_reloader 会判断是否设置了 RUN_MAIN 为 True。开始时,是没有这个环境变量的,因此程序走 else 代码块。而在 restart_with_reloader 中,就设置了这个环境变量 new_environ["RUN_MAIN"] = 'true' 。精彩的部分到了,在新的环境变量中,用 os.spawnve 启动新子进程,而这个子进程运行的正是当前的命令(python manage.py runserver),现在 RUN_MAIN 为 True 了,执行 thread.start_new_thread(main_func, args, kwargs) ,也就是启动了一个 server。如果子进程不退出,就一直停在 os.spawnve 这一步; 如果子进程退出,而退出码不是 3,while 就被终结了;如果是 3,继续循环,重新创建子进程。

在此可以得出,django 的 autoreload 机制中,主进程其实也没做什么事,就是监控子进程的运行,如果子进程退出码是 3,继续创建子进程。但目前为止,似乎还缺少文件监听的部分,这部分应该就在 reloader_thread() 中。

文件监控与子进程重启

def reloader_thread():
    ensure_echo_on()
    if USE_INOTIFY:
        fn = inotify_code_changed
    else:
        fn = code_changed
    while RUN_RELOADER:
        change = fn()
        if change == FILE_MODIFIED:
            sys.exit(3)  # force reload
        elif change == I18N_MODIFIED:
            reset_translations()
        time.sleep(1)

ensure_echo_on() 中,先判断是否成功导入 termios 模块,这个模块是 unix 平台的控制通信端口的,具体怎么控制不怎么懂,这个 win 上是没有的。经过跟踪 USE_INOTIFY 这个值是为 False ,因此判断是否文件是否修改是 code_changed 函数。

def code_changed():
    global _mtimes, _win
    for filename in gen_filenames():
        stat = os.stat(filename)
        mtime = stat.st_mtime
        if _win:
            mtime -= stat.st_ctime
        if filename not in _mtimes:
            _mtimes[filename] = mtime
            continue
        if mtime != _mtimes[filename]:
            _mtimes = {}
            try:
                del _error_files[_error_files.index(filename)]
            except ValueError:
                pass
            return I18N_MODIFIED if filename.endswith('.mo') else FILE_MODIFIED
    return False

这样就清楚了,根据每个文件的最后修改时间来判断文件是否被修改,如果修改, code_changed() 返回 True。上一层函数就执行 sys.exit(3) 退出子进程。然后主进程监控到子进程的退出码为 3,就会重新建立新的子进程。监听文件修改的线程每1秒中执行一次。

总结

使用环境变量巧妙的建立两个进程,主进程负责监控子进程和创建新的进程;子进程用来执行命令。子进程中创建子线程来监控文件的修改,如果有修改,退出子进程。不知为何不能把监控文件的放在主进程中做呢,每个子进程都要再创建这个监控,多累啊,主进程不能控制子进程退出吗?


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

如果对您有用,您的支持将鼓励我继续创作!