refactor: 优化日志系统关闭流程,使用 print 替代 logger 输出,确保在关闭后仍能记录信息

feat: 添加服务器关闭时的超时处理,避免 shutdown 持续挂起
fix: 更新生产模式设置,使用 Starlette 的 FileResponse 处理静态文件
pull/1364/head
墨梓柒 2025-11-17 17:51:29 +08:00
parent e57a996626
commit 060ce5d55b
No known key found for this signature in database
GPG Key ID: 4A65B9DBA35F7635
4 changed files with 24 additions and 22 deletions

13
bot.py
View File

@ -107,9 +107,6 @@ async def graceful_shutdown(): # sourcery skip: use-named-expression
logger.info("麦麦优雅关闭完成") logger.info("麦麦优雅关闭完成")
# 关闭日志系统,释放文件句柄
shutdown_logging()
except Exception as e: except Exception as e:
logger.error(f"麦麦关闭失败: {e}", exc_info=True) logger.error(f"麦麦关闭失败: {e}", exc_info=True)
@ -241,7 +238,7 @@ if __name__ == "__main__":
# 确保 loop 在任何情况下都尝试关闭(如果存在且未关闭) # 确保 loop 在任何情况下都尝试关闭(如果存在且未关闭)
if "loop" in locals() and loop and not loop.is_closed(): if "loop" in locals() and loop and not loop.is_closed():
loop.close() loop.close()
logger.info("事件循环已关闭") print("[主程序] 事件循环已关闭")
# 关闭日志系统,释放文件句柄 # 关闭日志系统,释放文件句柄
try: try:
@ -249,6 +246,8 @@ if __name__ == "__main__":
except Exception as e: except Exception as e:
print(f"关闭日志系统时出错: {e}") print(f"关闭日志系统时出错: {e}")
# 在程序退出前暂停,让你有机会看到输出 print("[主程序] 准备退出...")
# input("按 Enter 键退出...") # <--- 添加这行
sys.exit(exit_code) # <--- 使用记录的退出码 # 使用 os._exit() 强制退出,避免被阻塞
# 由于已经在 graceful_shutdown() 中完成了所有清理工作,这是安全的
os._exit(exit_code)

View File

@ -843,8 +843,8 @@ def start_log_cleanup_task():
def shutdown_logging(): def shutdown_logging():
"""优雅关闭日志系统,释放所有文件句柄""" """优雅关闭日志系统,释放所有文件句柄"""
logger = get_logger("logger") # 先输出到控制台,避免日志系统关闭后无法输出
logger.info("正在关闭日志系统...") print("[logger] 正在关闭日志系统...")
# 关闭所有handler # 关闭所有handler
root_logger = logging.getLogger() root_logger = logging.getLogger()
@ -865,4 +865,5 @@ def shutdown_logging():
handler.close() handler.close()
logger_obj.removeHandler(handler) logger_obj.removeHandler(handler)
logger.info("日志系统已关闭") # 使用 print 而不是 logger因为 logger 已经关闭
print("[logger] 日志系统已关闭")

View File

@ -2,6 +2,7 @@ from fastapi import FastAPI, APIRouter
from fastapi.middleware.cors import CORSMiddleware # 新增导入 from fastapi.middleware.cors import CORSMiddleware # 新增导入
from typing import Optional from typing import Optional
from uvicorn import Config, Server as UvicornServer from uvicorn import Config, Server as UvicornServer
import asyncio
import os import os
from rich.traceback import install from rich.traceback import install
@ -82,8 +83,17 @@ class Server:
"""安全关闭服务器""" """安全关闭服务器"""
if self._server: if self._server:
self._server.should_exit = True self._server.should_exit = True
await self._server.shutdown() try:
self._server = None # 添加 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: def get_app(self) -> FastAPI:
"""获取 FastAPI 实例""" """获取 FastAPI 实例"""

View File

@ -41,8 +41,7 @@ def setup_production_mode() -> bool:
"""设置生产模式 - 挂载静态文件""" """设置生产模式 - 挂载静态文件"""
try: try:
from src.common.server import get_global_server from src.common.server import get_global_server
from fastapi.staticfiles import StaticFiles from starlette.responses import FileResponse
from fastapi.responses import FileResponse
server = get_global_server() server = get_global_server()
base_dir = Path(__file__).parent.parent.parent base_dir = Path(__file__).parent.parent.parent
@ -58,14 +57,6 @@ def setup_production_mode() -> bool:
logger.warning("💡 请确认前端已正确构建") logger.warning("💡 请确认前端已正确构建")
return False return False
# 挂载静态资源
if (static_path / "assets").exists():
server.app.mount(
"/assets",
StaticFiles(directory=str(static_path / "assets")),
name="assets"
)
# 处理 SPA 路由 # 处理 SPA 路由
@server.app.get("/{full_path:path}") @server.app.get("/{full_path:path}")
async def serve_spa(full_path: str): async def serve_spa(full_path: str):
@ -77,6 +68,7 @@ def setup_production_mode() -> bool:
# 检查文件是否存在 # 检查文件是否存在
file_path = static_path / full_path file_path = static_path / full_path
if file_path.is_file(): if file_path.is_file():
# 直接返回文件Starlette 会自动管理文件句柄
return FileResponse(file_path) return FileResponse(file_path)
# 返回 index.htmlSPA 路由) # 返回 index.htmlSPA 路由)