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
|
import nonebot
|
||||||
|
from nonebot.adapters.onebot.v11 import Adapter as ONEBOT_V11Adapter
|
||||||
|
|
||||||
nonebot.init()
|
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__":
|
if __name__ == "__main__":
|
||||||
nonebot.run()
|
nonebot.run()
|
||||||
@@ -2,18 +2,33 @@
|
|||||||
|
|
||||||
定时从远程仓库拉取更新(fetch + 仅快进合并,绝不触碰本地改动),
|
定时从远程仓库拉取更新(fetch + 仅快进合并,绝不触碰本地改动),
|
||||||
本地插件代码随之更新;若 uv.lock / pyproject.toml 有变化则执行
|
本地插件代码随之更新;若 uv.lock / pyproject.toml 有变化则执行
|
||||||
uv sync 同步第三方插件依赖。更新后可向指定群发通知,
|
uv sync 同步第三方插件依赖。更新后可向指定群发通知并自动重启。
|
||||||
可选自动退出进程(配合进程守护工具实现重启加载新代码)。
|
|
||||||
|
重启方式按启动环境自动选择:
|
||||||
|
- 默认入口 bot.py 直接启动:自动启用监督进程模式(multiprocessing),
|
||||||
|
拉取完整结束后触发干净重启,不会像 --reload 那样在拉取中途
|
||||||
|
触发重载加载到残缺代码,无需修改入口文件、无需进程守护工具
|
||||||
|
- nb run --reload:不干预,文件变化由 uvicorn/watchfiles 自动重载
|
||||||
|
- 其他部署方式:可配置 AUTO_UPDATE_RESTART 退出进程交给守护工具拉起
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
|
from multiprocessing import get_context
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import nonebot
|
||||||
from nonebot import get_bots, get_driver, get_plugin_config, logger, require
|
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
|
from nonebot.plugin import PluginMetadata
|
||||||
|
|
||||||
require("nonebot_plugin_apscheduler")
|
require("nonebot_plugin_apscheduler")
|
||||||
@@ -21,7 +36,6 @@ require("nonebot_plugin_apscheduler")
|
|||||||
from nonebot_plugin_apscheduler import scheduler
|
from nonebot_plugin_apscheduler import scheduler
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .reloader import Reloader
|
|
||||||
|
|
||||||
__plugin_meta__ = PluginMetadata(
|
__plugin_meta__ = PluginMetadata(
|
||||||
name="git自动更新",
|
name="git自动更新",
|
||||||
@@ -42,7 +56,7 @@ _HAS_GIT = shutil.which("git") is not None
|
|||||||
_HAS_UV = shutil.which("uv") is not None
|
_HAS_UV = shutil.which("uv") is not None
|
||||||
|
|
||||||
# 以 --reload 模式运行时(如 nb run --reload),uvicorn/watchfiles 监听到
|
# 以 --reload 模式运行时(如 nb run --reload),uvicorn/watchfiles 监听到
|
||||||
# 文件变化会自动重启 worker 加载新代码,插件无需(也不应)自己退出进程
|
# 文件变化会自动重启 worker 加载新代码,插件无需(也不应)自己重启进程
|
||||||
_RELOAD_MODE = "--reload" in sys.argv
|
_RELOAD_MODE = "--reload" in sys.argv
|
||||||
|
|
||||||
if not (PROJECT_ROOT / ".git").exists():
|
if not (PROJECT_ROOT / ".git").exists():
|
||||||
@@ -56,6 +70,88 @@ if not _HAS_UV:
|
|||||||
_ENV = {**os.environ, "GIT_TERMINAL_PROMPT": "0"}
|
_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]:
|
async def _run(cmd: list[str], cwd: Path, timeout: int = 120) -> tuple[bool, str]:
|
||||||
"""运行命令,返回 (是否成功, 合并了 stderr 的输出)"""
|
"""运行命令,返回 (是否成功, 合并了 stderr 的输出)"""
|
||||||
try:
|
try:
|
||||||
@@ -155,8 +251,9 @@ async def _check_and_update() -> None:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
dep_note = await _sync_dependencies()
|
dep_note = await _sync_dependencies()
|
||||||
if Reloader.event is not None:
|
supervised = Reloader.event is not None
|
||||||
# 监督进程模式(python bot.py):拉取完整结束后再干净重启,
|
if supervised:
|
||||||
|
# 监督进程模式:拉取完整结束后再干净重启,
|
||||||
# 避免 --reload 文件监听在拉取中途触发、加载到残缺代码
|
# 避免 --reload 文件监听在拉取中途触发、加载到残缺代码
|
||||||
restart_note = "即将优雅重启加载新代码"
|
restart_note = "即将优雅重启加载新代码"
|
||||||
elif _RELOAD_MODE:
|
elif _RELOAD_MODE:
|
||||||
@@ -171,7 +268,7 @@ async def _check_and_update() -> None:
|
|||||||
notice += f"\n{restart_note}"
|
notice += f"\n{restart_note}"
|
||||||
logger.info(f"[auto_update] 更新完成,{restart_note}")
|
logger.info(f"[auto_update] 更新完成,{restart_note}")
|
||||||
await _notice(notice)
|
await _notice(notice)
|
||||||
if Reloader.event is not None:
|
if supervised:
|
||||||
Reloader.reload(delay=5) # 给群通知留出发送时间
|
Reloader.reload(delay=5) # 给群通知留出发送时间
|
||||||
elif config.auto_update_restart and not _RELOAD_MODE:
|
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