"""插件运行目录与本地仓库的文件变化监控。""" from __future__ import annotations import time import threading from collections.abc import Callable from pathlib import Path from typing import Any, Optional FederatedChangeResolver = Callable[[Path], Optional[tuple[str, Optional[dict], bool]]] RuntimePluginResolver = Callable[[Path], Optional[str]] MonitorSuppression = Callable[[str], bool] LocalCandidateResolver = Callable[[Path], Optional[dict]] LocalPluginSync = Callable[[str, Optional[dict]], bool] PluginReloader = Callable[[str], Any] DependencyManifestStatus = Callable[[Path], Optional[bool]] WatchFunction = Callable[..., Any] class PluginMonitorController: """独立管理插件文件监控线程的启动、停止和重建。""" def __init__(self, *, runner: Callable[[], None], log: Any) -> None: """保存监控循环入口和日志端口,线程状态仅由本组件持有。""" self._runner = runner self._logger = log self._thread: Optional[threading.Thread] = None self._stop_event = threading.Event() @property def stop_event(self) -> threading.Event: """返回供 watchfiles 监听的停止事件。""" return self._stop_event def reload(self, enabled: bool) -> None: """按当前配置停止旧线程,并在启用时创建新线程。""" self.stop() if enabled: self.start() def start(self) -> None: """启动唯一的守护监控线程。""" if self._thread and self._thread.is_alive(): self._logger.info("插件文件修改监测已经在运行中...") return self._logger.info("开始监测插件文件修改...") self._stop_event.clear() self._thread = threading.Thread(target=self._runner, daemon=True) self._thread.start() def stop(self) -> None: """请求监控线程退出,并在限定时间内等待其清理。""" if not self._thread or not self._thread.is_alive(): self._logger.info("未启用插件文件修改监测,无需停止") return self._logger.info("正在停止插件文件修改监测...") self._stop_event.set() self._thread.join(timeout=5) if self._thread.is_alive(): self._logger.warning("插件文件修改监测线程在5秒内未能正常停止。") self._thread = None self._logger.info("插件文件修改监测停止完成") class PluginChangeMonitor: """把文件变化归并为本地同步和运行态重载动作。""" def __init__( self, *, runtime_root: Path, local_roots: Callable[[], list[Path]], stop_event: Any, recent_sync: dict[str, float], federated_change: FederatedChangeResolver, runtime_plugin: RuntimePluginResolver, local_candidate: LocalCandidateResolver, sync_local: LocalPluginSync, reload_plugin: PluginReloader, dependency_manifest_status: DependencyManifestStatus, watch: WatchFunction, log: Any, monitor_suppressed: Optional[MonitorSuppression] = None, ) -> None: """保存监控路径、变化解析器和副作用回调。""" self._runtime_root = runtime_root self._local_roots = local_roots self._stop_event = stop_event self._recent_sync = recent_sync self._federated_change = federated_change self._runtime_plugin = runtime_plugin self._monitor_suppressed = monitor_suppressed or (lambda _plugin_id: False) self._local_candidate = local_candidate self._sync_local = sync_local self._reload_plugin = reload_plugin self._dependency_manifest_status = dependency_manifest_status self._watch = watch self._logger = log def run(self) -> None: """运行 watchfiles 主循环并按批次同步、重载插件。""" plugin_paths = [str(self._runtime_root)] plugin_paths.extend( str(path) for path in self._local_roots() if path.exists() and path.is_dir() ) self._logger.info(">>> 监控线程已启动,准备进入watch循环...") for changes in self._watch( *plugin_paths, stop_event=self._stop_event, rust_timeout=1000, yield_on_timeout=True, ): if not changes: continue self._process_changes(changes) def _process_changes(self, changes: Any) -> None: """把一批文件事件归并为最多一次同步和一次重载。""" plugins_to_reload = set() local_plugins_to_sync = {} for _change_type, path_str in changes: event_path = Path(path_str) if "__pycache__" in event_path.parts: continue manifest_status = self._dependency_manifest_status(event_path) if manifest_status is not None: self._handle_dependency_manifest_change( event_path, active=manifest_status, ) continue federated_change = self._federated_change(event_path) if federated_change: plugin_id, candidate, remote_entry_ready = federated_change if candidate and remote_entry_ready: if candidate.get("compatible") is False: self._logger.info( f"检测到本地插件 {plugin_id} 联邦构建产物变化," f"但跳过同步:{candidate.get('skip_reason')}" ) elif plugin_id not in local_plugins_to_sync: local_plugins_to_sync[plugin_id] = ( candidate, event_path, False, ) continue if event_path.suffix != ".py": continue runtime_plugin_id = self._runtime_plugin(event_path) if runtime_plugin_id and self._monitor_suppressed(runtime_plugin_id): self._logger.debug( f"插件 {runtime_plugin_id} 正在写入,跳过本批文件监控重载" ) continue candidate = ( self._local_candidate(event_path) if not runtime_plugin_id else None ) if runtime_plugin_id: last_sync_time = self._recent_sync.get(runtime_plugin_id) if last_sync_time and time.time() - last_sync_time < 2: continue plugins_to_reload.add(runtime_plugin_id) elif candidate: if candidate.get("compatible") is False: package_version = candidate.get("package_version") source_root = ( f"plugins.{package_version}" if package_version else "plugins" ) self._logger.info( f"检测到本地插件 {candidate.get('id')} 文件变化," f"来源:{source_root},文件:{event_path}," f"但跳过同步:{candidate.get('skip_reason')}" ) continue local_plugins_to_sync[candidate.get("id")] = ( candidate, event_path, True, ) for plugin_id, (candidate, event_path, should_reload) in ( local_plugins_to_sync.items() ): package_version = candidate.get("package_version") source_root = ( f"plugins.{package_version}" if package_version else "plugins" ) change_name = "Python 文件" if should_reload else "联邦构建产物" self._logger.info( f"检测到本地插件 {plugin_id} {change_name}变化," f"来源:{source_root},文件:{event_path}" ) if self._sync_local(plugin_id, candidate) and should_reload: plugins_to_reload.add(plugin_id) if not plugins_to_reload: return self._logger.info( f"检测到插件文件变化,准备重载: {list(plugins_to_reload)}" ) for plugin_id in plugins_to_reload: try: self._reload_plugin(plugin_id) except Exception as err: self._logger.error( f"插件 {plugin_id} 热重载失败: {err}", exc_info=True, ) def _handle_dependency_manifest_change( self, event_path: Path, *, active: bool, ) -> None: """记录依赖文件变化,但不在监控线程中隐式安装依赖。""" candidate = self._local_candidate(event_path) if not candidate: return if candidate.get("compatible") is False: self._logger.info( f"检测到本地插件 {candidate.get('id')} 依赖文件变化," f"但跳过处理:{candidate.get('skip_reason')}" ) return if not active: self._logger.debug( f"检测到本地插件 {candidate.get('id')} 非生效依赖文件变化:" f"{event_path.name}" ) return self._logger.warning( f"检测到本地插件 {candidate.get('id')} 依赖文件变化," "请重新安装本地插件以安装依赖" )