diff --git a/agents/haonan/workspace/dev-workflow/stats.py b/agents/haonan/workspace/dev-workflow/stats.py index fd615b8..2f72768 100644 --- a/agents/haonan/workspace/dev-workflow/stats.py +++ b/agents/haonan/workspace/dev-workflow/stats.py @@ -8,6 +8,7 @@ Cross 模式统计报告模块 import json import os import tempfile +import fcntl from datetime import datetime from pathlib import Path from typing import Dict, List, Any, Optional @@ -26,12 +27,32 @@ def _ensure_stats_dir(): STATS_DIR.mkdir(parents=True, exist_ok=True) +# 锁文件路径 +LOCK_FILE = STATS_DIR / "stats_data.lock" + + +def _acquire_lock(lock_file_path: Path): + """获取文件锁""" + lock_file_path.parent.mkdir(parents=True, exist_ok=True) + lock_fd = open(lock_file_path, 'w') + fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX) + return lock_fd + + +def _release_lock(lock_fd): + """释放文件锁""" + fcntl.flock(lock_fd.fileno(), fcntl.LOCK_UN) + lock_fd.close() + + def _load_stats_data() -> Dict[str, Any]: - """加载统计数据""" + """加载统计数据(带锁保护)""" if not STATS_FILE.exists(): return {"events": [], "metadata": {"created_at": datetime.now().isoformat()}} + lock_fd = None try: + lock_fd = _acquire_lock(LOCK_FILE) with open(STATS_FILE, 'r', encoding='utf-8') as f: return json.load(f) except json.JSONDecodeError as e: @@ -44,25 +65,40 @@ def _load_stats_data() -> Dict[str, Any]: return {"events": [], "metadata": {"created_at": datetime.now().isoformat()}} except IOError: return {"events": [], "metadata": {"created_at": datetime.now().isoformat()}} + finally: + if lock_fd: + _release_lock(lock_fd) def _atomic_write(data: Dict[str, Any]): - """原子写入统计数据""" + """原子写入统计数据(带锁保护)""" _ensure_stats_dir() - # 写入临时文件 - fd, temp_path = tempfile.mkstemp(dir=STATS_DIR, suffix='.tmp') + lock_fd = None + temp_path = None try: - with os.fdopen(fd, 'w', encoding='utf-8') as f: - json.dump(data, f, ensure_ascii=False, indent=2) + lock_fd = _acquire_lock(LOCK_FILE) + + # 写入临时文件 + with tempfile.NamedTemporaryFile( + mode='w', + encoding='utf-8', + dir=STATS_DIR, + suffix='.tmp', + delete=False + ) as temp_file: + json.dump(data, temp_file, ensure_ascii=False, indent=2) + temp_path = temp_file.name # 原子重命名 os.replace(temp_path, STATS_FILE) - except Exception: + temp_path = None # 防止在 finally 中再次删除 + finally: + if lock_fd: + _release_lock(lock_fd) # 清理临时文件 - if os.path.exists(temp_path): + if temp_path and os.path.exists(temp_path): os.unlink(temp_path) - raise def _filter_sensitive_data(data: Dict[str, Any]) -> Dict[str, Any]: