- 新增六层UI自动化架构:从Backend到Capabilities的完整分层实现 - 添加WeChat 4.0分辨率适配Profile与图像模板资源 - 实现幂等缓存、熔断器、重试策略、链路追踪与监控指标 - 新增头像下载安全校验、发布朋友圈路径白名单防护 - 优化密钥缓存、DB校验逻辑与初始化流程 - 补充完整错误码体系与启动清场机制
147 lines
4.8 KiB
Python
147 lines
4.8 KiB
Python
"""调试截图资源回收器:每小时清理,100MB 上限。
|
||
|
||
清理 /tmp/woc_debug/ 下的调试截图:
|
||
- 按 mtime 删除超过 max_age_hours 的文件
|
||
- 总量超 max_total_mb 时按 LRU(最旧 mtime)删除最老文件直到达标
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import logging
|
||
import os
|
||
import time
|
||
from typing import Optional
|
||
|
||
logger = logging.getLogger("woc-bridge")
|
||
|
||
|
||
class ResourceReaper:
|
||
"""调试截图资源回收器。
|
||
|
||
Attributes:
|
||
debug_screenshot_dir: 调试截图目录,默认 /tmp/woc_debug
|
||
max_age_hours: 文件最大存活小时数,默认 24
|
||
max_total_mb: 目录总大小上限 MB,默认 100
|
||
interval: 检查间隔秒数,默认 3600(1 小时)
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
debug_screenshot_dir: str = "/tmp/woc_debug",
|
||
max_age_hours: int = 24,
|
||
max_total_mb: int = 100,
|
||
interval: float = 3600.0,
|
||
) -> None:
|
||
self.debug_screenshot_dir = debug_screenshot_dir
|
||
self.max_age_hours = max_age_hours
|
||
self.max_total_mb = max_total_mb
|
||
self.interval = interval
|
||
self._task: Optional[asyncio.Task] = None
|
||
self._stopped = False
|
||
|
||
async def start(self) -> None:
|
||
"""启动后台回收任务。"""
|
||
if self._task is not None and not self._task.done():
|
||
return
|
||
self._stopped = False
|
||
self._task = asyncio.create_task(self._run())
|
||
logger.info(
|
||
"[reaper] started (dir=%s max_age=%dh max_total=%dMB)",
|
||
self.debug_screenshot_dir, self.max_age_hours, self.max_total_mb,
|
||
)
|
||
|
||
async def stop(self) -> None:
|
||
"""停止回收任务。"""
|
||
self._stopped = True
|
||
if self._task is not None and not self._task.done():
|
||
self._task.cancel()
|
||
try:
|
||
await asyncio.wait_for(self._task, timeout=2.0)
|
||
except (asyncio.TimeoutError, asyncio.CancelledError):
|
||
pass
|
||
self._task = None
|
||
logger.info("[reaper] stopped")
|
||
|
||
async def _run(self) -> None:
|
||
"""主循环:每 interval 秒清理一次。"""
|
||
while not self._stopped:
|
||
try:
|
||
await asyncio.sleep(self.interval)
|
||
await self.cleanup()
|
||
except asyncio.CancelledError:
|
||
break
|
||
except Exception as exc:
|
||
logger.warning("[reaper] cleanup exception: %s", exc)
|
||
|
||
async def cleanup(self) -> None:
|
||
"""执行一次清理:TTL 删除 + 总量限制 LRU 删除。"""
|
||
await asyncio.to_thread(self._cleanup_sync)
|
||
|
||
def _cleanup_sync(self) -> None:
|
||
"""同步清理实现(在 to_thread 中执行)。"""
|
||
if not os.path.isdir(self.debug_screenshot_dir):
|
||
return
|
||
|
||
now = time.time()
|
||
max_age_sec = self.max_age_hours * 3600
|
||
|
||
# 1. 收集所有文件 + mtime + size
|
||
files: list[tuple[str, float, int]] = [] # (path, mtime, size)
|
||
for name in os.listdir(self.debug_screenshot_dir):
|
||
path = os.path.join(self.debug_screenshot_dir, name)
|
||
if not os.path.isfile(path):
|
||
continue
|
||
try:
|
||
stat = os.stat(path)
|
||
files.append((path, stat.st_mtime, stat.st_size))
|
||
except OSError:
|
||
continue
|
||
|
||
if not files:
|
||
return
|
||
|
||
# 2. 删除超 TTL 的文件
|
||
deleted_ttl = 0
|
||
survivors: list[tuple[str, float, int]] = []
|
||
for path, mtime, size in files:
|
||
if now - mtime > max_age_sec:
|
||
try:
|
||
os.remove(path)
|
||
deleted_ttl += 1
|
||
except OSError as exc:
|
||
logger.debug("[reaper] remove %s failed: %s", path, exc)
|
||
survivors.append((path, mtime, size))
|
||
else:
|
||
survivors.append((path, mtime, size))
|
||
|
||
if deleted_ttl > 0:
|
||
logger.info(
|
||
"[reaper] deleted %d files (TTL > %dh)", deleted_ttl, self.max_age_hours
|
||
)
|
||
|
||
# 3. 总量超限时按 LRU(最旧 mtime)删除
|
||
total_bytes = sum(s for _, _, s in survivors)
|
||
max_bytes = self.max_total_mb * 1024 * 1024
|
||
if total_bytes <= max_bytes:
|
||
return
|
||
|
||
# 按 mtime 升序(最旧在前)
|
||
survivors.sort(key=lambda x: x[1])
|
||
deleted_lru = 0
|
||
for path, _, size in survivors:
|
||
if total_bytes <= max_bytes:
|
||
break
|
||
try:
|
||
os.remove(path)
|
||
total_bytes -= size
|
||
deleted_lru += 1
|
||
except OSError as exc:
|
||
logger.debug("[reaper] remove %s failed: %s", path, exc)
|
||
|
||
if deleted_lru > 0:
|
||
logger.info(
|
||
"[reaper] deleted %d files (LRU, total now %.1fMB)",
|
||
deleted_lru, total_bytes / (1024 * 1024),
|
||
)
|