Files
2026-07-13 13:32:32 +08:00

100 lines
3.3 KiB
Python

#!/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()