From 060ce5d55ba3a8162cfd46b196b3618f93605f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Mon, 17 Nov 2025 17:51:29 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=B3=BB=E7=BB=9F=E5=85=B3=E9=97=AD=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=EF=BC=8C=E4=BD=BF=E7=94=A8=20print=20=E6=9B=BF=E4=BB=A3=20logg?= =?UTF-8?q?er=20=E8=BE=93=E5=87=BA=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=9C=A8?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E5=90=8E=E4=BB=8D=E8=83=BD=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=20feat:=20=E6=B7=BB=E5=8A=A0=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8=E5=85=B3=E9=97=AD=E6=97=B6=E7=9A=84=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E5=A4=84=E7=90=86=EF=BC=8C=E9=81=BF=E5=85=8D=20shutdo?= =?UTF-8?q?wn=20=E6=8C=81=E7=BB=AD=E6=8C=82=E8=B5=B7=20fix:=20=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=94=9F=E4=BA=A7=E6=A8=A1=E5=BC=8F=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=EF=BC=8C=E4=BD=BF=E7=94=A8=20Starlette=20=E7=9A=84=20FileRespo?= =?UTF-8?q?nse=20=E5=A4=84=E7=90=86=E9=9D=99=E6=80=81=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 13 ++++++------- src/common/logger.py | 7 ++++--- src/common/server.py | 14 ++++++++++++-- src/webui/manager.py | 12 ++---------- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/bot.py b/bot.py index cf342507..ba210462 100644 --- a/bot.py +++ b/bot.py @@ -107,9 +107,6 @@ async def graceful_shutdown(): # sourcery skip: use-named-expression logger.info("麦麦优雅关闭完成") - # 关闭日志系统,释放文件句柄 - shutdown_logging() - except Exception as e: logger.error(f"麦麦关闭失败: {e}", exc_info=True) @@ -241,7 +238,7 @@ if __name__ == "__main__": # 确保 loop 在任何情况下都尝试关闭(如果存在且未关闭) if "loop" in locals() and loop and not loop.is_closed(): loop.close() - logger.info("事件循环已关闭") + print("[主程序] 事件循环已关闭") # 关闭日志系统,释放文件句柄 try: @@ -249,6 +246,8 @@ if __name__ == "__main__": except Exception as e: print(f"关闭日志系统时出错: {e}") - # 在程序退出前暂停,让你有机会看到输出 - # input("按 Enter 键退出...") # <--- 添加这行 - sys.exit(exit_code) # <--- 使用记录的退出码 + print("[主程序] 准备退出...") + + # 使用 os._exit() 强制退出,避免被阻塞 + # 由于已经在 graceful_shutdown() 中完成了所有清理工作,这是安全的 + os._exit(exit_code) diff --git a/src/common/logger.py b/src/common/logger.py index 4546e88a..55833c34 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -843,8 +843,8 @@ def start_log_cleanup_task(): def shutdown_logging(): """优雅关闭日志系统,释放所有文件句柄""" - logger = get_logger("logger") - logger.info("正在关闭日志系统...") + # 先输出到控制台,避免日志系统关闭后无法输出 + print("[logger] 正在关闭日志系统...") # 关闭所有handler root_logger = logging.getLogger() @@ -865,4 +865,5 @@ def shutdown_logging(): handler.close() logger_obj.removeHandler(handler) - logger.info("日志系统已关闭") + # 使用 print 而不是 logger,因为 logger 已经关闭 + print("[logger] 日志系统已关闭") diff --git a/src/common/server.py b/src/common/server.py index 140d86d1..ebdc9fa2 100644 --- a/src/common/server.py +++ b/src/common/server.py @@ -2,6 +2,7 @@ from fastapi import FastAPI, APIRouter from fastapi.middleware.cors import CORSMiddleware # 新增导入 from typing import Optional from uvicorn import Config, Server as UvicornServer +import asyncio import os from rich.traceback import install @@ -82,8 +83,17 @@ class Server: """安全关闭服务器""" if self._server: self._server.should_exit = True - await self._server.shutdown() - self._server = None + try: + # 添加 3 秒超时,避免 shutdown 永久挂起 + await asyncio.wait_for(self._server.shutdown(), timeout=3.0) + except asyncio.TimeoutError: + # 超时就强制标记为 None,让垃圾回收处理 + pass + except Exception: + # 忽略其他异常 + pass + finally: + self._server = None def get_app(self) -> FastAPI: """获取 FastAPI 实例""" diff --git a/src/webui/manager.py b/src/webui/manager.py index f4302c50..e4679f1e 100644 --- a/src/webui/manager.py +++ b/src/webui/manager.py @@ -41,8 +41,7 @@ def setup_production_mode() -> bool: """设置生产模式 - 挂载静态文件""" try: from src.common.server import get_global_server - from fastapi.staticfiles import StaticFiles - from fastapi.responses import FileResponse + from starlette.responses import FileResponse server = get_global_server() base_dir = Path(__file__).parent.parent.parent @@ -58,14 +57,6 @@ def setup_production_mode() -> bool: logger.warning("💡 请确认前端已正确构建") return False - # 挂载静态资源 - if (static_path / "assets").exists(): - server.app.mount( - "/assets", - StaticFiles(directory=str(static_path / "assets")), - name="assets" - ) - # 处理 SPA 路由 @server.app.get("/{full_path:path}") async def serve_spa(full_path: str): @@ -77,6 +68,7 @@ def setup_production_mode() -> bool: # 检查文件是否存在 file_path = static_path / full_path if file_path.is_file(): + # 直接返回文件,Starlette 会自动管理文件句柄 return FileResponse(file_path) # 返回 index.html(SPA 路由)