refactor: reloader 并入 auto_update 自动安装

This commit is contained in:
snowy-yang committed 2026-09-11 20:33:49 +08:00
1 parent a68ae28960
commit 47cb288e69
3 files changed
+111 -104

No files matched your search

+6 -10
View File
@@ -1,19 +1,15 @@
"""机器人入口
两种启动方式:
- 开发调试:nb run --reload (文件变化自动重载,不走本入口)
- 正式运行:python bot.py (监督进程模式,auto_update 插件拉取更新后可优雅重启)
"""
import nonebot
from nonebot.adapters.onebot.v11 import Adapter as ONEBOT_V11Adapter
nonebot.init()
nonebot.load_from_toml("pyproject.toml") # 适配器 + 第三方插件 + 本地插件目录
driver = nonebot.get_driver()
driver.register_adapter(ONEBOT_V11Adapter)
from src.plugins.auto_update.reloader import install_reloader
nonebot.load_builtin_plugins("echo")
install_reloader()
nonebot.load_from_toml("pyproject.toml")
if __name__ == "__main__":
nonebot.run()
+105 -8
View File
@@ -2,18 +2,33 @@
定时从远程仓库拉取更新(fetch + 仅快进合并,绝不触碰本地改动),
本地插件代码随之更新;若 uv.lock / pyproject.toml 有变化则执行
uv sync 同步第三方插件依赖。更新后可向指定群发通知,
可选自动退出进程(配合进程守护工具实现重启加载新代码)。
uv sync 同步第三方插件依赖。更新后可向指定群发通知并自动重启。
重启方式按启动环境自动选择:
- 默认入口 bot.py 直接启动:自动启用监督进程模式(multiprocessing),
拉取完整结束后触发干净重启,不会像 --reload 那样在拉取中途
触发重载加载到残缺代码,无需修改入口文件、无需进程守护工具
- nb run --reload:不干预,文件变化由 uvicorn/watchfiles 自动重载
- 其他部署方式:可配置 AUTO_UPDATE_RESTART 退出进程交给守护工具拉起
"""
import asyncio
import os
import shutil
import sys
import threading
from multiprocessing import get_context
from pathlib import Path
from typing import Any
import nonebot
from nonebot import get_bots, get_driver, get_plugin_config, logger, require
from nonebot.adapters.onebot.v11 import ActionFailed, Bot, Message, NetworkError
from nonebot.adapters.onebot.v11 import (
ActionFailed,
Bot,
Message,
NetworkError,
)
from nonebot.plugin import PluginMetadata
require("nonebot_plugin_apscheduler")
@@ -21,7 +36,6 @@ require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler
from .config import Config
from .reloader import Reloader
__plugin_meta__ = PluginMetadata(
name="git自动更新",
@@ -42,7 +56,7 @@ _HAS_GIT = shutil.which("git") is not None
_HAS_UV = shutil.which("uv") is not None
# 以 --reload 模式运行时(如 nb run --reload),uvicorn/watchfiles 监听到
# 文件变化会自动重启 worker 加载新代码,插件无需(也不应)自己退出进程
# 文件变化会自动重启 worker 加载新代码,插件无需(也不应)自己重启进程
_RELOAD_MODE = "--reload" in sys.argv
if not (PROJECT_ROOT / ".git").exists():
@@ -56,6 +70,88 @@ if not _HAS_UV:
_ENV = {**os.environ, "GIT_TERMINAL_PROMPT": "0"}
# ------------------------ 监督进程重启器 ------------------------
_nb_run = nonebot.run
class ReloaderNotEnabledError(RuntimeError):
"""监督进程模式未启用(非默认入口启动时不接管 nonebot.run)"""
class Reloader:
"""跨进程重启开关:机器人进程中调用 reload() 请求父进程重启自己"""
# 实际对象为 multiprocessing.Event(与 threading.Event 接口一致)
event: threading.Event | None = None
@classmethod
def reload(cls, delay: int = 0) -> None:
"""请求重启机器人进程
参数:
delay: 延迟秒数(给群通知等收尾工作留出发送时间)
"""
if cls.event is None:
raise ReloaderNotEnabledError
if delay > 0:
threading.Timer(delay, cls.event.set).start()
return
cls.event.set()
def _bot_process(ev: Any, *args: Any, **kwargs: Any) -> None:
"""监督子进程入口:真正的机器人在这里运行"""
Reloader.event = ev
_nb_run(*args, **kwargs)
def _supervised_run(*args: Any, **kwargs: Any) -> None:
"""监督进程入口:循环拉起机器人子进程,直到其自行退出或收到重启事件"""
should_exit = False
ctx = get_context("spawn")
while not should_exit:
event = ctx.Event()
process = ctx.Process(
target=_bot_process, args=(event, *args), kwargs=kwargs
)
process.start()
while not should_exit:
if event.wait(1):
logger.info("[auto_update] 收到重启事件,正在重启机器人进程")
process.terminate()
process.join(config.auto_update_reboot_grace_time)
if process.is_alive():
logger.warning(
"[auto_update] 进程未在 "
f"{config.auto_update_reboot_grace_time} "
"秒内优雅退出,强制结束"
)
process.kill()
break
if not process.is_alive():
# 子进程自行退出(如 Ctrl+C),监督进程随之结束
should_exit = True
def _install_reloader() -> None:
"""从默认入口 bot.py 直接启动时自动接管 nonebot.run"""
if _RELOAD_MODE:
return
main_module = sys.modules.get("__main__")
main_file = str(getattr(main_module, "__file__", "") or "")
if Path(main_file).name != "bot.py":
return
nonebot.run = _supervised_run
logger.info("[auto_update] 已启用监督进程模式(更新拉取完整后自动优雅重启)")
_install_reloader()
# ------------------------ 更新检查 ------------------------
async def _run(cmd: list[str], cwd: Path, timeout: int = 120) -> tuple[bool, str]:
"""运行命令,返回 (是否成功, 合并了 stderr 的输出)"""
try:
@@ -155,8 +251,9 @@ async def _check_and_update() -> None:
)
return
dep_note = await _sync_dependencies()
if Reloader.event is not None:
# 监督进程模式(python bot.py):拉取完整结束后再干净重启,
supervised = Reloader.event is not None
if supervised:
# 监督进程模式:拉取完整结束后再干净重启,
# 避免 --reload 文件监听在拉取中途触发、加载到残缺代码
restart_note = "即将优雅重启加载新代码"
elif _RELOAD_MODE:
@@ -171,7 +268,7 @@ async def _check_and_update() -> None:
notice += f"\n{restart_note}"
logger.info(f"[auto_update] 更新完成,{restart_note}")
await _notice(notice)
if Reloader.event is not None:
if supervised:
Reloader.reload(delay=5) # 给群通知留出发送时间
elif config.auto_update_restart and not _RELOAD_MODE:
# 延迟硬退出,交给进程守护工具重新拉起以加载新代码
-86
View File
@@ -1,86 +0,0 @@
"""进程监督式重启器
用 multiprocessing 监督进程包裹 nonebot.run:
- 父进程只负责拉起/终止真正的机器人子进程
- 机器人子进程中调用 ``Reloader.reload()`` 通知父进程重启自己,
全部代码与插件随之完整重载
- 相比 --reload 的文件监听,重启时机完全由插件控制:
git pull 完整结束后才触发重启,不会加载到拉取一半的残缺代码
通过 ``install_reloader()`` 显式启用(仅应在 bot.py 中调用),
不启用时对 nb run 等启动方式零影响。
"""
import threading
from multiprocessing import get_context
from typing import Any
import nonebot
from nonebot import get_plugin_config, logger
from .config import Config
_nb_run = nonebot.run
_reloader_config = get_plugin_config(Config)
class ReloaderNotEnabledError(RuntimeError):
"""Reloader 未启用(未通过 python bot.py 的监督进程模式启动)"""
class Reloader:
"""跨进程重启开关:机器人进程中调用 reload() 请求父进程重启自己"""
# 实际对象为 multiprocessing.Event(与 threading.Event 接口一致)
event: threading.Event | None = None
@classmethod
def reload(cls, delay: int = 0) -> None:
"""请求重启机器人进程
参数:
delay: 延迟秒数(给群通知等收尾工作留出发送时间)
"""
if cls.event is None:
raise ReloaderNotEnabledError
if delay > 0:
threading.Timer(delay, cls.event.set).start()
return
cls.event.set()
def _run(ev: Any, *args: Any, **kwargs: Any) -> None:
Reloader.event = ev
_nb_run(*args, **kwargs)
def run(*args: Any, **kwargs: Any) -> None:
"""监督进程入口:循环拉起机器人子进程,直到其自行退出或收到重启事件"""
should_exit = False
ctx = get_context("spawn")
while not should_exit:
event = ctx.Event()
process = ctx.Process(target=_run, args=(event, *args), kwargs=kwargs)
process.start()
while not should_exit:
if event.wait(1):
logger.info("[auto_update] 收到重启事件,正在重启机器人进程")
process.terminate()
process.join(_reloader_config.auto_update_reboot_grace_time)
if process.is_alive():
logger.warning(
"[auto_update] 进程未在 "
f"{_reloader_config.auto_update_reboot_grace_time} "
"秒内优雅退出,强制结束"
)
process.kill()
break
if not process.is_alive():
# 子进程自行退出(如 Ctrl+C),监督进程随之结束
should_exit = True
def install_reloader() -> None:
"""将 nonebot.run 替换为监督进程版入口(仅应在 bot.py 中显式调用)"""
nonebot.run = run