From d71809d144bad246e548bb67db997b8b89e90403 Mon Sep 17 00:00:00 2001 From: snowy-yang <102860110+snowy-yang@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:55:17 +0800 Subject: [PATCH] =?UTF-8?q?=E7=83=AD=E9=87=8D=E8=BD=BD=E7=9B=91=E7=9D=A3?= =?UTF-8?q?=E8=80=85=E4=BC=98=E9=9B=85=E5=93=8D=E5=BA=94=20Ctrl+C=EF=BC=9A?= =?UTF-8?q?=E5=85=88=E7=AD=89=E5=AD=90=E8=BF=9B=E7=A8=8B=E6=94=B6=E5=B0=BE?= =?UTF-8?q?=E5=86=8D=E5=9B=9E=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 乱码 --- musicbot/main.py | 6 +++++- musicbot/reload.py | 26 +++++++++++++++++++++++--- start.bat | 6 +++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/musicbot/main.py b/musicbot/main.py index f916a01..1665f7d 100644 --- a/musicbot/main.py +++ b/musicbot/main.py @@ -82,7 +82,11 @@ def main(argv: list[str] | None = None) -> None: # 保证「监控的树」与「实际加载的配置」一致 root = _project_root() # 监督者不配置日志:子进程自己会配,避免两边抢写同一个日志文件 - code = reload.run_supervisor([sys.executable, "-m", "musicbot"], 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()) diff --git a/musicbot/reload.py b/musicbot/reload.py index 91c5abe..50f870d 100644 --- a/musicbot/reload.py +++ b/musicbot/reload.py @@ -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) diff --git a/start.bat b/start.bat index d89332a..1eb8984 100644 --- a/start.bat +++ b/start.bat @@ -1 +1,5 @@ -uv run musicbot --reload \ No newline at end of file +@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"