• 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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    def execute_from_command_line(argv=None):
        utility = ManagementUtility(argv)
        utility.execute()
    
    • 1
    • 2
    • 3
    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
    
    • 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

    下面先研究下 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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    由上可见,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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    发现监测文件改动并自动重启的逻辑在 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__
                )
    
    • 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
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    可以看到 子进程检测文件改动以及自动退出的逻辑在 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
    • 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
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    总结一下:

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

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

  • 相关阅读:
    数据结构——堆
    面渣逆袭:RocketMQ二十三问
    麒麟v10安装Redis(ARM架构)
    vue3 开启 https
    『无为则无心』Python日志 — 65、日志模块logging的使用
    第一章 概述
    python集成乐玩插件
    未来10年,NAND 与DRAM依然是存储主角
    老卫带你学---leetcode刷题(347. 前 K 个高频元素)
    动手实践:从栈帧看字节码是如何在 JVM 中进行流转的
  • 原文地址:https://blog.csdn.net/weixin_37882382/article/details/125874302