The principle of python web framework autoreload (take bottle as an example)

    When I was watching the bottle in the past two days, I found that it also has the function of code auto reload, so I took a look at its source code.

    When reloader=True is set, the main process will not start the bottle service, but will create a new subprocess with the same command line parameters as the main process started. Then the main process keeps busy waiting for the child process to end, and gets the return code of the child process. If the code returned by the child process is 3, restart the child process with the same command line parameters, and the previous code changes will be reloaded. . In the child process, the main thread is running the bottle service, and the other thread is constantly checking whether all imported module files are modified (the principle of check will be seen in the code), if a file change is detected, the check thread will send A KeyboardInterrupt exception goes to the main thread, kills the bottle service, and the child process exits with returncode=3.

    In the bottle source code, the autoreload function mainly involves two places, one is the run function, and the other is the FileCheckerThread class.

  Let's take a look at the code snippet of the run function part (the annotated bottle source code in the reloader part: https://github.com/kagxin/recipes/blob/master/bottle/bottle.py).

reloader is True to enable the autoreload function

    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
    ...
    ...

Code Analysis:

    Program execution, when reloader is True and there is no BOTTLE_CHILD in the environment variable, the logic after execution, the environment variable BOTTLE_CHILD is used when Popen uses command line parameters to start the child process, so that the started child process does not enter the current branch, Instead, it directly executes the logic to start the bottle service afterward.

    Don't pay attention to the lockfile file first, its main function is to let the child process judge whether the main process is still alive by judging whether its modify time is updated. while p.poll() is None:... This code is busy waiting for the child process to end, while using os.utime to continuously update the aceess time and modify time of the lockfile. If returncode==3 indicates that the child process ended due to file modification, restart the child process using the same command line via popen in the current loop.

    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)

Code Analysis:

    This is the main part of the child process. In the context manager of bgcheck, the bottle service is run, and server.run(app) is blocked until the end signal of the main thread is received. In this context manager, a thread that checks file changes is running. If the file changes, it will send KeyboardInterrupt to the current main thread to terminate server.run(app). The KeyboardInterrupt exception is ignored when the context manager exits, and the child process exits with 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)

Code Analysis:

    This class has two dunder methods, __enter__ and __exit__, that implement the context manager protocol. When entering the context manager, start the thread, wait for the thread to end when exiting, and ignore the KeyboardInterrupt exception, because the exception in with will bubble up when __exit__ returns a value other than True.

    In the run method, get the modify time of all module files in the for module in list(sys.modules.values()):... for loop. Then in the subsequent while loop, monitor file changes, if there are changes, call thread.interrupt_main(), raise in the main thread (the thread where the bottle is located), and KeyboardInterrupt exception.

The above is the code of the entire bottle auto reload mechanism.

The annotated bottle source code of the reloader part:

https://github.com/kagxin/recipes/blob/master/bottle/bottle.py

Welcome to Paizhuanzhuan exchange╭(╯^╰)╮

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324944414&siteId=291194637