#!/usr/bin/env python3 """Refresh Watcher — 常驻进程,监控信号文件,debounce 后执行 refresh_live_data.py。 替代 kanban_update.py 中每次操作都 fork 子进程的方式。 多 Agent 并发时,200 次 touch → 合并为 1 次 refresh。 运行方式: python3 scripts/refresh_watcher.py 部署方式: - systemd: 参见 edict.service - docker-compose: 参见 edict/docker-compose.yml - 手动前台: python3 scripts/refresh_watcher.py """ import logging import os import pathlib import signal import subprocess import sys import time _BASE = pathlib.Path(os.environ.get('EDICT_HOME', '')).resolve() if os.environ.get('EDICT_HOME') else pathlib.Path(__file__).resolve().parent.parent SIGNAL_FILE = _BASE / 'data' / '.refresh_pending' PID_FILE = _BASE / 'data' / '.refresh_watcher_pid' REFRESH_SCRIPT = _BASE / 'scripts' / 'refresh_live_data.py' DEBOUNCE_SEC = 2 # 信号文件稳定 2 秒后才执行 POLL_INTERVAL = 0.5 # 检查间隔 logging.basicConfig( level=logging.INFO, format='%(asctime)s [refresh_watcher] %(message)s', datefmt='%H:%M:%S', ) log = logging.getLogger('refresh_watcher') _running = True def _shutdown(signum, frame): global _running _running = False log.info(f'收到信号 {signum},准备退出') def main(): # 写 PID 文件,让 kanban_update.py 知道 watcher 在运行 PID_FILE.parent.mkdir(parents=True, exist_ok=True) PID_FILE.write_text(str(os.getpid())) log.info(f'Refresh watcher started (pid={os.getpid()}, debounce={DEBOUNCE_SEC}s)') signal.signal(signal.SIGTERM, _shutdown) signal.signal(signal.SIGINT, _shutdown) last_seen_mtime = 0.0 refresh_count = 0 try: while _running: try: if SIGNAL_FILE.exists(): mtime = SIGNAL_FILE.stat().st_mtime now = time.time() # 信号文件存在且已稳定 DEBOUNCE_SEC 秒 if mtime > last_seen_mtime and (now - mtime) >= DEBOUNCE_SEC: last_seen_mtime = mtime # 删除信号文件(在执行前删,避免执行期间的新 touch 被吞) try: SIGNAL_FILE.unlink() except FileNotFoundError: pass refresh_count += 1 log.info(f'🔄 执行 refresh #{refresh_count}') try: subprocess.run( [sys.executable, str(REFRESH_SCRIPT)], capture_output=True, timeout=30, ) except subprocess.TimeoutExpired: log.warning('refresh_live_data.py 超时 (30s)') except Exception as e: log.error(f'refresh 执行失败: {e}') except Exception as e: log.error(f'Watcher loop error: {e}') time.sleep(POLL_INTERVAL) finally: # 清理 PID 文件 try: PID_FILE.unlink() except Exception: pass log.info(f'Refresh watcher stopped (total refreshes: {refresh_count})') if __name__ == '__main__': main()