热重载监督者优雅响应 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
+33 -5

No files matched your search

+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)