之前分析了一波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)
总结一下:
PS: 如果我们也想在其他地方实现检测到代码改动自动重启的功能,可以直接把django.utils.autoreload.py 复制出去,然后用autoreload.run_with_reloader() 执行目标函数即可。