热重载监督者优雅响应 Ctrl+C:先等子进程收尾再回收

- run_supervisor 捕获 KeyboardInterrupt:不打 traceback、退出码归零,
  给子进程 _GRACE_SECONDS=5s 自行收尾窗口(打退出日志、关连接),
  超时才 taskkill /F /T;文件变更重启仍立即强杀(子进程不知情不会自己退)
- main() 兜底监督循环间隙的 Ctrl+C
- start.bat 改为发射器:start 新开独立控制台跑 cmd /c 命令行(非批
  处理),Ctrl+C 直接停进程,规避「终止批处理操作吗(Y/N)」,发射器
  即退;bat 保持纯 ASCII 防 GBK 乱码
This commit is contained in:
snowy-yang committed 2026-09-24 21:55:17 +08:00
1 parent 6b6492dc8d
commit d71809d144
3 files changed
+32 -4

No files matched your search

+4
View File
@@ -82,7 +82,11 @@ def main(argv: list[str] | None = None) -> None:
# 保证「监控的树」与「实际加载的配置」一致
root = _project_root()
# 监督者不配置日志:子进程自己会配,避免两边抢写同一个日志文件
try:
code = reload.run_supervisor([sys.executable, "-m", "musicbot"], root)
except KeyboardInterrupt:
# 兜底 run_supervisor 循环间隙的 Ctrl+C,同样不打印 traceback
code = 0
sys.exit(code)
try:
asyncio.run(amain())
+23 -3
View File
@@ -23,6 +23,10 @@ CHILD_ENV = "_MUSICBOT_RELOAD_CHILD"
# 轮询间隔(秒):够快地感知保存动作,又不至于空耗 CPU
_POLL_INTERVAL = 1.0
# Ctrl+C 时给子进程的自行退出窗口(秒):子进程收到同一信号正在收尾
# (打退出日志、关连接),等它退完,超时才强杀
_GRACE_SECONDS = 5.0
# 监控的文件类型(源码与订阅/环境配置——两者都在启动时读取,改了需重启)
_WATCH_SUFFIXES = (".py", ".toml")
@@ -72,10 +76,20 @@ def _spawn(cmd: list[str], root: Path) -> subprocess.Popen:
return subprocess.Popen(cmd, cwd=root, env={**os.environ, CHILD_ENV: "1"})
def _terminate(proc: subprocess.Popen) -> None:
"""结束子进程及其后代(bot 可能再拉起 Python 子进程)。"""
def _terminate(proc: subprocess.Popen, grace: float = 0) -> None:
"""结束子进程及其后代(bot 可能再拉起 Python 子进程)。
grace > 0 时先等子进程自行退出,超时才强杀——用于 Ctrl+C 收尾场景;
文件变更重启时子进程并不知情、不会自己退,必须保持立即强杀。
"""
if proc.poll() is not None:
return
if grace:
try:
proc.wait(timeout=grace)
return
except subprocess.TimeoutExpired:
logger.warning(f"子进程未在 {grace:g}s 内自行退出,强制结束")
if sys.platform == "win32":
# terminate() 只结束直接子进程,/T 连整棵进程树一起收掉
subprocess.run(
@@ -103,6 +117,7 @@ def run_supervisor(cmd: list[str], root: Path) -> int:
while True:
logger.info("启动 bot 子进程")
proc = _spawn(cmd, root)
grace = 0.0
try:
before = snapshot(root)
while proc.poll() is None:
@@ -117,6 +132,11 @@ def run_supervisor(cmd: list[str], root: Path) -> int:
else:
# 子进程自行退出(含 Ctrl-C 透传):不重启,原样返回退出码
return proc.returncode or 0
except KeyboardInterrupt:
# Ctrl+C 同时送达子进程与此监督循环:吞掉异常不打印 traceback,
# 退出码归零,子进程的收尾交给下方带宽限的回收
grace = _GRACE_SECONDS
return 0
finally:
# 正常退出时是空操作;重启与 Ctrl-C 中断时回收子进程,避免孤儿
_terminate(proc)
_terminate(proc, grace)
+5 -1
View File
@@ -1 +1,5 @@
uv run musicbot --reload
@echo off
rem Run the bot in its own console: cmd /c executes a command line, not a
rem batch, so Ctrl+C shuts it down cleanly without cmd's
rem "Terminate batch job (Y/N)?" prompt. This launcher exits immediately.
start "musicbot" /d "%~dp0" cmd /c "uv run musicbot --reload"