修复所有资源泄漏问题(P0级bug)

修复的Bug:
- Bug #21: Playwright浏览器实例泄漏
- Bug #22: 数据库连接泄漏(已由连接池解决)
- Bug #23: 截图文件句柄泄漏
- Bug #24: 线程资源未清理
- Bug #25: requests.Session对象泄漏

主要改进:
1. PlaywrightAutomation类:
   - 添加atexit注册,确保进程退出时关闭浏览器
   - 添加__enter__/__exit__支持context manager
   - 添加_closed标志防止重复关闭
   - 添加_cleanup_on_exit静默清理方法

2. APIBrowser类:
   - 添加atexit注册,确保Session正确关闭
   - 添加__enter__/__exit__支持context manager
   - 添加_closed标志防止重复关闭

3. 截图功能增强:
   - 使用临时文件机制
   - 添加文件大小验证
   - 失败时自动清理临时文件
   - 确保不产生垃圾文件

4. 应用关闭清理:
   - 添加cleanup_on_exit()函数
   - 注册SIGINT/SIGTERM信号处理器
   - 停止所有运行中的任务
   - 等待线程优雅退出
   - 关闭浏览器线程池
   - 关闭数据库连接池

影响:
- 防止长期运行导致的内存泄漏
- 确保进程异常退出时正确清理资源
- 提升系统稳定性和可靠性

受影响文件:
- playwright_automation.py
- api_browser.py
- app.py

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-11 13:48:06 +08:00
parent 481f4dfbac
commit c1a65debbc
3 changed files with 156 additions and 2 deletions

View File

@@ -11,6 +11,7 @@ from playwright.sync_api import sync_playwright, Browser, BrowserContext, Page,
import time
import json
import threading
import atexit
from typing import Optional, Callable
from dataclasses import dataclass
from app_config import get_config
@@ -147,6 +148,10 @@ class PlaywrightAutomation:
self.context: Optional[BrowserContext] = None
self.page: Optional[Page] = None
self.main_page: Optional[Page] = None
self._closed = False # 防止重复关闭
# 注册退出清理函数,确保进程异常退出时也能关闭浏览器
atexit.register(self._cleanup_on_exit)
def log(self, message: str):
"""记录日志"""
@@ -1269,7 +1274,16 @@ class PlaywrightAutomation:
Returns:
是否截图成功
"""
import os
temp_filepath = None
try:
# 确保目录存在
os.makedirs(os.path.dirname(filepath), exist_ok=True)
# 先保存到临时文件
temp_filepath = filepath + '.tmp'
# 使用最高质量设置截图
# type='jpeg' 指定JPEG格式支持quality参数
# quality=100 表示100%的JPEG质量范围0-100最高质量
@@ -1277,19 +1291,47 @@ class PlaywrightAutomation:
# 视口分辨率 2560x1440 确保高清晰度
# 这样可以生成更清晰的截图大小约500KB-1MB左右
self.main_page.screenshot(
path=filepath,
path=temp_filepath,
type='jpeg',
full_page=True,
quality=100
)
self.log(f"截图已保存: {filepath}")
# 验证文件是否成功创建且大小合理
if not os.path.exists(temp_filepath):
raise FileNotFoundError(f"截图文件未创建: {temp_filepath}")
file_size = os.path.getsize(temp_filepath)
if file_size == 0:
raise ValueError(f"截图文件为空: {temp_filepath}")
# 文件验证通过,重命名为最终文件名
if os.path.exists(filepath):
os.remove(filepath) # 删除旧文件
os.rename(temp_filepath, filepath)
self.log(f"截图已保存: {filepath} ({file_size} bytes)")
return True
except Exception as e:
self.log(f"截图失败: {str(e)}")
# 清理临时文件
try:
if temp_filepath and os.path.exists(temp_filepath):
os.remove(temp_filepath)
except Exception as cleanup_error:
self.log(f"清理临时文件失败: {cleanup_error}")
return False
def close(self):
"""完全关闭浏览器进程(每个账号独立)并确保资源释放"""
# 防止重复关闭
if self._closed:
return
self._closed = True
errors = []
# 第一步:关闭上下文
@@ -1339,6 +1381,30 @@ class PlaywrightAutomation:
self.log(f"资源清理完成,但有{len(errors)}个警告")
# else部分日志已精简
def _cleanup_on_exit(self):
"""进程退出时的清理函数由atexit调用"""
if not self._closed:
try:
# 静默关闭,避免在退出时产生过多日志
if self.context:
self.context.close()
if self.browser:
self.browser.close()
if self.playwright:
self.playwright.stop()
self._closed = True
except:
pass # 退出时忽略所有错误
def __enter__(self):
"""Context manager支持 - 进入"""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager支持 - 退出"""
self.close()
return False # 不抑制异常
# 简单的测试函数
if __name__ == "__main__":