修复所有资源泄漏问题(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:
@@ -9,6 +9,7 @@ import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
import time
|
||||
import atexit
|
||||
from typing import Optional, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -38,6 +39,8 @@ class APIBrowser:
|
||||
self.logged_in = False
|
||||
self.log_callback = log_callback
|
||||
self.stop_flag = False
|
||||
self._closed = False # 防止重复关闭
|
||||
|
||||
# 设置代理
|
||||
if proxy_config and proxy_config.get("server"):
|
||||
proxy_server = proxy_config["server"]
|
||||
@@ -49,6 +52,9 @@ class APIBrowser:
|
||||
else:
|
||||
self.proxy_server = None
|
||||
|
||||
# 注册退出清理函数
|
||||
atexit.register(self._cleanup_on_exit)
|
||||
|
||||
def log(self, message: str):
|
||||
"""记录日志"""
|
||||
if self.log_callback:
|
||||
@@ -382,7 +388,29 @@ class APIBrowser:
|
||||
|
||||
def close(self):
|
||||
"""关闭会话"""
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
|
||||
try:
|
||||
self.session.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
def _cleanup_on_exit(self):
|
||||
"""进程退出时的清理函数(由atexit调用)"""
|
||||
if not self._closed:
|
||||
try:
|
||||
self.session.close()
|
||||
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 # 不抑制异常
|
||||
|
||||
60
app.py
60
app.py
@@ -3484,7 +3484,67 @@ def batch_stop_accounts():
|
||||
"stopped": stopped
|
||||
})
|
||||
|
||||
def cleanup_on_exit():
|
||||
"""应用退出时的清理函数"""
|
||||
import signal
|
||||
import sys
|
||||
|
||||
print("\n正在清理资源...")
|
||||
|
||||
# 1. 停止所有运行中的任务
|
||||
print("- 停止运行中的任务...")
|
||||
for account_id, thread in list(active_tasks.items()):
|
||||
try:
|
||||
# 设置停止标志
|
||||
if account_id in user_accounts:
|
||||
for user_id in user_accounts:
|
||||
if account_id in user_accounts[user_id]:
|
||||
user_accounts[user_id][account_id].should_stop = True
|
||||
except:
|
||||
pass
|
||||
|
||||
# 2. 等待所有线程完成(最多等待5秒)
|
||||
print("- 等待线程退出...")
|
||||
for account_id, thread in list(active_tasks.items()):
|
||||
try:
|
||||
if thread and thread.is_alive():
|
||||
thread.join(timeout=2)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 3. 关闭浏览器工作线程池
|
||||
print("- 关闭浏览器线程池...")
|
||||
try:
|
||||
shutdown_browser_worker_pool()
|
||||
except:
|
||||
pass
|
||||
|
||||
# 4. 关闭数据库连接池
|
||||
print("- 关闭数据库连接池...")
|
||||
try:
|
||||
db_pool._pool.close_all() if db_pool._pool else None
|
||||
except:
|
||||
pass
|
||||
|
||||
print("✓ 资源清理完成")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import signal
|
||||
import atexit
|
||||
|
||||
# 注册退出清理函数
|
||||
atexit.register(cleanup_on_exit)
|
||||
|
||||
# 注册信号处理器
|
||||
def signal_handler(sig, frame):
|
||||
print("\n\n收到退出信号,正在关闭...")
|
||||
cleanup_on_exit()
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
print("=" * 60)
|
||||
print("知识管理平台自动化工具 - 多用户版")
|
||||
print("=" * 60)
|
||||
|
||||
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user