Files
mirror_server/core/config_hotreload.py
HYC Fixer 82875b710a 复查修复(二): 同步模块与安全遗留
- 修复 remove_sync_source 引入的 stop_sync KeyError 回归(容错 get)
- stop_sync 不再删除存活线程条目(防双 worker);FTP 子目录递归传真名(不再 KeyError)
- 远程文件名统一 _safe_remote_name 校验(FTP/SFTP/HTTP 防路径穿越)
- temp 任务名加随机后缀防碰撞;temp 状态/线程完成后清理(防 sync_state.json 膨胀)
- save_sync_state 快照+原子写(临时文件+os.replace),持锁调用不死锁
- URL 打印脱敏(user:pass@ -> ***@)
- _need_sync_http size=0 不再全量重下(仅按存在性)
- cron 同一分钟去重;PyPI 进度只更新当前源
- v2 start_sync 不再把 bool 当 task_id
- UserRecord.to_dict 脱敏(不返回 password_hash/token),新增 to_dict_private/get_user_with_password
- config_hotreload 单配置源(set+persist 与热重载一致);server.py 用 get_all()
- 会话创建时顺带清理过期项;JSON stats/历史读改写加锁
- 解压目标目录先校验;FTP RETR 命令注入防护;rsync --delete 目标保护
- git/urlopen 补超时;cleanup_completed_tasks 删旧留新
2026-09-02 00:39:12 +08:00

348 lines
11 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
配置热更新模块
支持不重启服务的情况下重新加载配置
"""
import os
import sys
import json
import time
import threading
import logging
from typing import Dict, Any, Optional, Callable
from datetime import datetime
logger = logging.getLogger(__name__)
class ConfigHotReloader:
"""配置热重载管理器"""
def __init__(self, config_path: str, callback: Callable = None):
"""
初始化热重载管理器
Args:
config_path: 配置文件路径
callback: 配置变更时的回调函数,接收 (config, change_type) 参数
"""
self.config_path = config_path
self.callback = callback
self._config: Dict[str, Any] = {}
self._last_modified: float = 0
self._last_load_time: Optional[datetime] = None
self._lock = threading.RLock()
# 配置变更历史
self._change_history: list = []
# 监听器
self._listeners: Dict[str, list] = {
'on_change': [],
'on_error': []
}
# 加载初始配置
self.reload()
def reload(self, silent: bool = False) -> bool:
"""
重新加载配置
Args:
silent: 静默模式,不触发变更通知
Returns:
是否加载成功
"""
try:
if not os.path.exists(self.config_path):
if not silent:
logger.warning(f"配置文件不存在: {self.config_path}")
return False
# 获取文件修改时间
current_mtime = os.path.getmtime(self.config_path)
# 检查是否有变化
if current_mtime == self._last_modified and not silent:
return True
# 加载配置
with open(self.config_path, 'r', encoding='utf-8') as f:
new_config = json.load(f)
# 计算变更
changes = self._compute_changes(self._config, new_config)
with self._lock:
old_config = self._config.copy()
self._config = new_config
self._last_modified = current_mtime
self._last_load_time = datetime.now()
# 记录变更
if changes:
change_record = {
'timestamp': self._last_load_time.isoformat(),
'changes': changes,
'old_config_keys': list(old_config.keys()),
'new_config_keys': list(new_config.keys())
}
self._change_history.append(change_record)
# 保持历史记录在合理范围内
if len(self._change_history) > 100:
self._change_history = self._change_history[-50:]
if not silent and changes:
self._notify_change(changes)
logger.info(f"配置已重新加载: {self.config_path}")
return True
except json.JSONDecodeError as e:
error_msg = f"配置 JSON 格式错误: {e}"
logger.error(error_msg)
self._notify_error(error_msg)
return False
except Exception as e:
error_msg = f"加载配置失败: {e}"
logger.error(error_msg)
self._notify_error(error_msg)
return False
def _compute_changes(self, old: Dict, new: Dict) -> Dict:
"""计算配置变更"""
changes = {
'added': [],
'removed': [],
'modified': []
}
old_keys = set(old.keys())
new_keys = set(new.keys())
# 新增的键
for key in new_keys - old_keys:
changes['added'].append(key)
# 移除的键
for key in old_keys - new_keys:
changes['removed'].append(key)
# 修改的键
for key in old_keys & new_keys:
if old[key] != new[key]:
# 检查是否是嵌套字典
if isinstance(old[key], dict) and isinstance(new[key], dict):
nested = self._compute_nested_changes(old[key], new[key], f"{key}.")
if nested['added'] or nested['removed'] or nested['modified']:
changes['modified'].append({
'key': key,
'type': 'nested',
'changes': nested
})
else:
changes['modified'].append({
'key': key,
'type': 'value',
'old_value': old[key],
'new_value': new[key]
})
return changes
def _compute_nested_changes(self, old: Dict, new: Dict, prefix: str = "") -> Dict:
"""计算嵌套字典的变更"""
changes = {
'added': [],
'removed': [],
'modified': []
}
old_keys = set(old.keys())
new_keys = set(new.keys())
for key in new_keys - old_keys:
changes['added'].append(f"{prefix}{key}")
for key in old_keys - new_keys:
changes['removed'].append(f"{prefix}{key}")
for key in old_keys & new_keys:
if old[key] != new[key]:
changes['modified'].append(f"{prefix}{key}")
return changes
def _notify_change(self, changes: Dict):
"""通知配置变更"""
for listener in self._listeners['on_change']:
try:
if callable(listener):
listener(self._config, changes)
except Exception as e:
logger.error(f"配置变更监听器执行失败: {e}")
if self.callback:
try:
self.callback(self._config, changes)
except Exception as e:
logger.error(f"配置回调函数执行失败: {e}")
def _notify_error(self, error: str):
"""通知错误"""
for listener in self._listeners['on_error']:
try:
if callable(listener):
listener(error)
except Exception as e:
logger.error(f"错误监听器执行失败: {e}")
def add_change_listener(self, callback: Callable):
"""添加配置变更监听器"""
self._listeners['on_change'].append(callback)
def add_error_listener(self, callback: Callable):
"""添加错误监听器"""
self._listeners['on_error'].append(callback)
def get(self, key: str, default: Any = None) -> Any:
"""获取配置值"""
with self._lock:
return self._config.get(key, default)
def get_all(self) -> Dict:
"""获取完整配置"""
with self._lock:
return self._config.copy()
def set(self, key: str, value: Any, save: bool = True) -> bool:
"""设置配置值(仅内存中)"""
with self._lock:
self._config[key] = value
if save:
return self.save()
return True
def save(self, path: str = None) -> bool:
"""保存配置到文件"""
save_path = path or self.config_path
try:
with open(save_path, 'w', encoding='utf-8') as f:
json.dump(self._config, f, ensure_ascii=False, indent=4)
self._last_modified = os.path.getmtime(save_path)
return True
except Exception as e:
logger.error(f"保存配置失败: {e}")
return False
def get_change_history(self, limit: int = 10) -> list:
"""获取配置变更历史"""
return self._change_history[-limit:]
def watch(self, interval: float = 5.0):
"""
启动后台监控线程
Args:
interval: 检查间隔(秒)
"""
def _watch_loop():
while True:
try:
self.reload()
except Exception as e:
logger.error(f"配置监控错误: {e}")
time.sleep(interval)
thread = threading.Thread(target=_watch_loop, daemon=True)
thread.start()
logger.info(f"配置热监控已启动,间隔: {interval}秒")
class ConfigManager:
"""配置管理器 - 支持热更新(单配置源: 热重载开启时以 reloader 的配置为准)"""
def __init__(self, config: Dict[str, Any] = None):
self.config = config or {}
self._hot_reloader: Optional[ConfigHotReloader] = None
def _effective_config(self) -> Dict[str, Any]:
"""获取当前生效配置(避免与 reloader._config 双配置源分裂)"""
if self._hot_reloader is not None:
return self._hot_reloader._config
return self.config
def load_from_file(self, path: str, enable_watch: bool = False) -> bool:
"""从文件加载配置"""
if not os.path.exists(path):
return False
try:
with open(path, 'r', encoding='utf-8') as f:
self.config = json.load(f)
if enable_watch:
self._hot_reloader = ConfigHotReloader(path)
self._hot_reloader.watch()
return True
except Exception as e:
logger.error(f"加载配置失败: {e}")
return False
def hot_reload(self, path: str = None) -> bool:
"""触发热重载"""
if self._hot_reloader:
return self._hot_reloader.reload()
return False
def get(self, key: str, default: Any = None) -> Any:
"""获取配置值(读当前生效配置)"""
return self._effective_config().get(key, default)
def set(self, key: str, value: Any, persist: bool = False, path: str = None) -> bool:
"""设置配置值(写入当前生效配置,持久化时与 reloader 一致)"""
keys = key.split('.')
current = self._effective_config()
for k in keys[:-1]:
if k not in current:
current[k] = {}
current = current[k]
current[keys[-1]] = value
if persist:
if self._hot_reloader:
# set 已写入 reloader._config, save 落盘的就是新值
return self._hot_reloader.save(path)
elif path:
try:
with open(path, 'w', encoding='utf-8') as f:
json.dump(self._effective_config(), f, ensure_ascii=False, indent=4)
return True
except Exception as e:
logger.error(f"保存配置失败: {e}")
return False
return True
def add_change_listener(self, callback: Callable):
"""添加变更监听器"""
if self._hot_reloader:
self._hot_reloader.add_change_listener(callback)
def get_all(self) -> Dict:
"""获取完整配置(当前生效版本)"""
return self._effective_config().copy()