Django 4.0.6源码分析:自动重启机制

之前分析了一波Flask的源码,其实DEBUG模式下,也有自动重启的功能,不过没有深究。最近在研究Django框架,同样也有自动重启的功能,这次我们就来研究一下吧。

Ps:Python看源码有个小技巧,可以随时修改源码文件用print来辅助我们边运行边看代码。

当我们用 python manage.py runserver 启动Django项目之后,每次改动保存都会自动进行服务器的重启。
先看下manage.py的代码

def main():
   	# 主要代码 用 python manage.py runserver启动时
   	# sys.argv 为 ['manage.py', 'runserver']
    execute_from_command_line(sys.argv)

if __name__ == '__main__':
    main()
def execute_from_command_line(argv=None):
    utility = ManagementUtility(argv)
    utility.execute()
class ManagementUtility:
    def execute(self):
        try:
        	# subcommand 为 runserver
            subcommand = self.argv[1]
        except IndexError:
            subcommand = "help"  # Display help if no arguments were given.

        if subcommand == "help":
        	pass
        else:
            # runserver 最终走这个分支执行
            self.fetch_command(subcommand).run_from_argv(self.argv)
  
    def fetch_command(self, subcommand):
        commands = get_commands()
        try:
        	# 根据subcommand获取对应的命令
        	# 见下面的分析 此处 app_name值为 django.core
            app_name = commands[subcommand]
        except KeyError:
			pass
        if isinstance(app_name, BaseCommand):
            # If the command is already loaded, use it directly.
            klass = app_name
        else:
        	# app_name 为 str 走这边
        	# 下面代码可以看到 就是构建一个  django.core.management.commands.runserver.Command 实例
            klass = load_command_class(app_name, subcommand)
        return klass

下面先研究下 get_commands 方法

@functools.lru_cache(maxsize=None)
def get_commands():
	# 此方法位于 django/core/management 内 
	# 所以__path__[0] 就是 django/core/management 的绝对路劲
	# 注意 value 直接写死为 django.core
    commands = {name: "django.core" for name in find_commands(__path__[0])}
    if not settings.configured:
        return commands

def find_commands(management_dir):
	# 此处 command_dir 即 django/core/management/commands 目录
    command_dir = os.path.join(management_dir, "commands")
    # 扫描目录 返回此目录下的 单个文件名称列表
    return [
        name
        for _, name, is_pkg in pkgutil.iter_modules([command_dir])
        if not is_pkg and not name.startswith("_")
    ]

def load_command_class(app_name, name):
    module = import_module("%s.management.commands.%s" % (app_name, name))
    return module.Command()

由上可见,python manage.py runserver 根据 runserver命令,去寻找并构造了 特定的实例来执行具体逻辑。
runserver 对应的实例就是 django.core.management.commands.runserver.Command 实例,下面分析

class Command(BaseCommand):

    def run(self, **options):
        use_reloader = options["use_reloader"]
        if use_reloader:
            # 此处就是 自动重启的关键
            autoreload.run_with_reloader(self.inner_run, **options)
        else:
            self.inner_run(None, **options)

发现监测文件改动并自动重启的逻辑在 django/utils/autoreload.py 中,下面重点研究

def run_with_reloader(main_func, *args, **kwargs):
	# 注册信号 接受到 终止信号 就正常退出当前进程
    signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
    try:
    	# 检测环境变量 根据环境变量分别执行启动逻辑
    	# 主进程会再启动一个子进程 子进程中才会有此环境变量
        if os.environ.get(DJANGO_AUTORELOAD_ENV) == "true":
        	# 获取一个reloader实例,里面封装了检测文件变化的逻辑
            reloader = get_reloader()
            # 启动时候的日志打印
            logger.info(
                "Watching for file changes with %s", reloader.__class__.__name__
            )
            start_django(reloader, main_func, *args, **kwargs)
        else:
        	# 第一次启动没有环境变量 走这边启动
            exit_code = restart_with_reloader()
            sys.exit(exit_code)
    except KeyboardInterrupt:
        pass

# 先看第一次启动时主进程执行的 restart_with_reloader方法
def restart_with_reloader():
	# 这边就是上面代码判断的环境变量
    new_environ = {**os.environ, DJANGO_AUTORELOAD_ENV: "true"}
    # 这边的args即 python manage.py runserver
    args = get_child_arguments()
    # 主进程中的死循环
    while True:
    	# 使用新进程执行上述命令 即 server在新的子进程中启动
    	# 子进程会一直执行 主进程会阻塞在这一行
        p = subprocess.run(args, env=new_environ, close_fds=False)
        # 子进程的returncode 为3时,即因为system.exit(3)退出时 
        # 会再次循环 即启动新进程再次启动server 
        # 然后 之前修改的代码自然更新
        if p.returncode != 3:
            return p.returncode

# 再来看子进程会执行的代码 
# 首先获得一个 reloader实例
def get_reloader():
    try:
        WatchmanReloader.check_availability()
    except WatchmanUnavailable:
    	# 挑这个看一下
        return StatReloader()
    return WatchmanReloader()

# 然后执行 start_django 方法
def start_django(reloader, main_func, *args, **kwargs):
    ensure_echo_on()

    main_func = check_errors(main_func)
    # 看出来在子进程内部 启动一个线程来执行runserver的逻辑
    django_main_thread = threading.Thread(
        target=main_func, args=args, kwargs=kwargs, name="django-main-thread"
    )
    # 设置为守护线程 进程退出时 直接退出 不用考虑此线程是否在工作
    django_main_thread.daemon = True
    django_main_thread.start()
	
    while not reloader.should_stop:
        try:
        	# 主要逻辑
            reloader.run(django_main_thread)
        except WatchmanUnavailable as ex:
            # It's possible that the watchman service shuts down or otherwise
            # becomes unavailable. In that case, use the StatReloader.
            reloader = StatReloader()
            logger.error("Error connecting to Watchman: %s", ex)
            logger.info(
                "Watching for file changes with %s", reloader.__class__.__name__
            )

可以看到 子进程检测文件改动以及自动退出的逻辑在 reloader 实例中,下面我们来看 StatReloader类

class StatReloader(BaseReloader):
    SLEEP_TIME = 1  # Check for changes once per second.
	
	# 继承自父类
    def run(self, django_main_thread):
        self.run_loop()

	# 继承自父类
    def run_loop(self):
        ticker = self.tick()
        while not self.should_stop
            try:
            	# 一直执行 next(ticker)
                next(ticker)
            except StopIteration:
                break
        self.stop()

	# 生成器函数
    def tick(self):
        mtimes = {}
        while True:
        	# 每一次next 都会检查是否有文件更新(文件的更新时间和之前不一致)
            for filepath, mtime in self.snapshot_files():
                old_time = mtimes.get(filepath)
                mtimes[filepath] = mtime
                if old_time is None:
                    logger.debug("File %s first seen with mtime %s", filepath, mtime)
                    continue
                elif mtime > old_time:
                    logger.debug(
                        "File %s previous mtime: %s, current mtime: %s",
                        filepath,
                        old_time,
                        mtime,
                    )
                    # 只要有文件更新就 notify
                    self.notify_file_changed(filepath)
			# 一秒钟检查一次
            time.sleep(self.SLEEP_TIME)
            yield
          
    def notify_file_changed(self, path):
        results = file_changed.send(sender=self, file_path=path)
        logger.debug("%s notified as changed. Signal results: %s.", path, results)
        if not any(res[1] for res in results):
        	# 关注这个
            trigger_reload(path)
            
def trigger_reload(filename):
	# 这个日志在文件变化时我们会看见
    logger.info("%s changed, reloading.", filename)
    # 和之前对上了
    # 子进程每个一秒钟检测下文件是否变化 有变化就 以returncode 3 退出
    # 主进程那边的循环发现子进程以 returncode3 退出 会再开启子进程执行runserver
    sys.exit(3)

总结一下:

  1. 当执行 python manage.py runserver 时,主进程中开启一个死循环,启动一个子进程来执行具体的代码。同时,当子进程以returncode=3退出时,再次启动新的子进程,从而使得修改后的代码得以执行。
  2. 子进程中以守护线程的方式启动一个线程来执行runserver的具体逻辑。同时,依赖reloader实例,每隔1秒钟监测下文件是否产生变化,若变化,则用system.exit(3)退出当前进程。
  3. 当我们修改了代码后,子进程检测到文件变动时退出,主进程知道后重新启动一个新的子进程,则我们修改的代码生效。

PS: 如果我们也想在其他地方实现检测到代码改动自动重启的功能,可以直接把django.utils.autoreload.py 复制出去,然后用autoreload.run_with_reloader() 执行目标函数即可。

你可能感兴趣的:(django,python,django,python)