新增热重载:文件变更自动重启(开发用,默认关)
- musicbot/reload.py:轮询监控 musicbot/ 下的 .py 与 intents.toml/.env, 变更后重启 bot 子进程;跳过 logs/、__pycache__/、.venv/ - 进程级重启而非模块重载:长连接 + 模块级状态 + handler 闭包, 就地重载易留幽灵 handler,重启子进程状态干净 - 子进程用 _MUSICBOT_RELOAD_CHILD 标记,不会套娃自我复制 - Windows 用 taskkill /T 收整棵进程树,实测连续重载无进程泄漏 - 开启方式:`uv run musicbot --reload`(start.bat 已带)或 RELOAD=on - 监督者不配日志,避免与子进程抢写同一日志文件
This commit is contained in:
1 parent
9bcdad7aa1
commit
a11c0b375f
8 files changed
+266
-4
No files matched your search
@@ -663,6 +663,101 @@ def test_setup_logging_writes_file(tmp_path: Path):
|
||||
lg.remove()
|
||||
|
||||
|
||||
def test_config_reload_flag(monkeypatch):
|
||||
"""RELOAD 默认关(生产不受影响),on 可开启,非法值按关处理。"""
|
||||
from musicbot.config import BotConfig
|
||||
|
||||
monkeypatch.delenv("RELOAD", raising=False)
|
||||
assert BotConfig.load().reload is False
|
||||
|
||||
monkeypatch.setenv("RELOAD", "on")
|
||||
assert BotConfig.load().reload is True
|
||||
|
||||
monkeypatch.setenv("RELOAD", "随便写")
|
||||
assert BotConfig.load().reload is False
|
||||
|
||||
|
||||
# ---- 热重载:文件监听与进程监督 ----
|
||||
|
||||
def test_watched_files_and_skip_rules(tmp_path: Path):
|
||||
"""监控 .py/.toml 与 .env;跳过 logs/、__pycache__/、.venv/ 与无关文件。"""
|
||||
from musicbot import reload
|
||||
|
||||
(tmp_path / "musicbot").mkdir()
|
||||
(tmp_path / "musicbot" / "music.py").write_text("", encoding="utf-8")
|
||||
(tmp_path / "tests").mkdir()
|
||||
(tmp_path / "tests" / "test_x.py").write_text("", encoding="utf-8")
|
||||
(tmp_path / "intents.toml").write_text("", encoding="utf-8")
|
||||
(tmp_path / ".env").write_text("", encoding="utf-8")
|
||||
(tmp_path / "README.md").write_text("", encoding="utf-8") # 无关后缀
|
||||
(tmp_path / "logs").mkdir()
|
||||
(tmp_path / "logs" / "musicbot.log").write_text("", encoding="utf-8")
|
||||
(tmp_path / "musicbot" / "__pycache__").mkdir()
|
||||
(tmp_path / "musicbot" / "__pycache__" / "x.py").write_text("", encoding="utf-8")
|
||||
|
||||
watched = reload.iter_watched_files(tmp_path)
|
||||
names = {p.relative_to(tmp_path).as_posix() for p in watched}
|
||||
assert names == {"musicbot/music.py", "tests/test_x.py", "intents.toml", ".env"}
|
||||
|
||||
|
||||
def test_changed_files_detects_edit_add_remove(tmp_path: Path):
|
||||
"""快照比对:改动/新增/删除都能识别,未变则返回空。"""
|
||||
from musicbot import reload
|
||||
|
||||
f = tmp_path / "musicbot"
|
||||
f.mkdir()
|
||||
mod = f / "music.py"
|
||||
mod.write_text("a", encoding="utf-8")
|
||||
|
||||
before = reload.snapshot(tmp_path)
|
||||
assert reload.changed_files(before, reload.snapshot(tmp_path)) == []
|
||||
|
||||
mod.write_text("bb", encoding="utf-8")
|
||||
assert reload.changed_files(before, reload.snapshot(tmp_path)) == [mod]
|
||||
|
||||
|
||||
def test_is_child_guard(monkeypatch):
|
||||
"""子进程标记生效:带标记的进程不再做监督,避免 --reload 无限自我复制。"""
|
||||
from musicbot import reload
|
||||
|
||||
monkeypatch.delenv(reload.CHILD_ENV, raising=False)
|
||||
assert reload.is_child() is False
|
||||
monkeypatch.setenv(reload.CHILD_ENV, "1")
|
||||
assert reload.is_child() is True
|
||||
|
||||
|
||||
def test_main_child_does_not_supervise(monkeypatch):
|
||||
"""子进程即使带 --reload / RELOAD=on 也只跑 bot,不起监督
|
||||
(否则 --reload 会无限自我复制)。"""
|
||||
from musicbot import main as mb_main
|
||||
from musicbot import reload
|
||||
|
||||
called: dict[str, bool] = {}
|
||||
|
||||
async def fake_amain() -> None:
|
||||
called["bot"] = True
|
||||
|
||||
monkeypatch.setattr(reload, "is_child", lambda: True)
|
||||
monkeypatch.setattr(mb_main, "amain", fake_amain)
|
||||
monkeypatch.setattr(
|
||||
reload, "run_supervisor",
|
||||
lambda *a, **k: called.setdefault("supervisor", True),
|
||||
)
|
||||
monkeypatch.setattr(mb_main.BotConfig, "load", staticmethod(
|
||||
lambda: replace(
|
||||
BotConfig(
|
||||
music_server_url="http://127.0.0.1:1",
|
||||
music_quality="128k",
|
||||
music_source="tx",
|
||||
),
|
||||
reload=True,
|
||||
)
|
||||
))
|
||||
|
||||
mb_main.main(["--reload"])
|
||||
assert called == {"bot": True} # 跑的是 bot,没有起监督
|
||||
|
||||
|
||||
# ---- 全量模式(不 @ 也能用)与去重 ----
|
||||
|
||||
def make_emitter(api, http, config) -> EventEmitter:
|
||||
|
||||
Reference in new issue
Block a user