WechatOnCloud/bridge/woc_bridge/ui/resource_reaper.py
Kris 15fc62d478 feat: 完成微信UI自动化新架构全量开发与集成
- 新增六层UI自动化架构:从Backend到Capabilities的完整分层实现
- 添加WeChat 4.0分辨率适配Profile与图像模板资源
- 实现幂等缓存、熔断器、重试策略、链路追踪与监控指标
- 新增头像下载安全校验、发布朋友圈路径白名单防护
- 优化密钥缓存、DB校验逻辑与初始化流程
- 补充完整错误码体系与启动清场机制
2026-07-16 01:19:47 +08:00

147 lines
4.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""调试截图资源回收器每小时清理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: 检查间隔秒数,默认 36001 小时)
"""
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),
)