refactor: reloader 并入 auto_update 自动安装
This commit is contained in:
1 parent
a68ae28960
commit
47cb288e69
3 files changed
+111
-104
No files matched your search
@@ -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()
|
||||
@@ -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:
|
||||
# 延迟硬退出,交给进程守护工具重新拉起以加载新代码
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user