Key features implemented: - Updated package metadata and dependencies in PKG-INFO, setup files - Added main.py entry point for backward compatibility with README launch method - Enhanced CLI with config options, system info command, and proper signal handling - Implemented minimal PluginManager loading only plugin-loader core plugin - Refactored PluginLoader to follow minimal core design, removed sandbox/isolation complexity - Updated auto-dependency plugin with safer PL injection mechanism and disabled pl_injection - Removed legacy plugin files (firewall, frp_proxy, ftp_server, multi_lang_deploy, ops_toolbox, security_gateway) as functionality moved to core plugin system - Improved gitignore with comprehensive ignore patterns The changes implement a minimal core framework design where only the plugin-loader is directly loaded by the core, with all other plugins managed through the PL injection mechanism, significantly simplifying the architecture.
198 lines
6.4 KiB
Python
198 lines
6.4 KiB
Python
"""热插拔插件 - 运行时加载/卸载/更新插件"""
|
|
import sys
|
|
import time
|
|
import threading
|
|
from pathlib import Path
|
|
from typing import Any, Optional, Callable
|
|
|
|
from oss.logger.logger import Log
|
|
from oss.plugin.types import Plugin, register_plugin_type
|
|
|
|
|
|
class HotReloadError(Exception):
|
|
"""热插拔错误"""
|
|
pass
|
|
|
|
|
|
class FileWatcher:
|
|
"""文件监听器"""
|
|
|
|
def __init__(self, watch_dirs: list[str], extensions: list[str], on_change: Callable):
|
|
self.watch_dirs = [Path(d) for d in watch_dirs]
|
|
self.extensions = extensions
|
|
self.on_change = on_change
|
|
self._running = False
|
|
self._thread: Optional[threading.Thread] = None
|
|
self._file_times: dict[str, float] = {}
|
|
self._scan_files()
|
|
|
|
def _scan_files(self):
|
|
"""扫描当前文件及其修改时间"""
|
|
for watch_dir in self.watch_dirs:
|
|
if watch_dir.exists():
|
|
for f in watch_dir.rglob("*"):
|
|
if f.is_file() and f.suffix in self.extensions:
|
|
self._file_times[str(f)] = f.stat().st_mtime
|
|
|
|
def start(self):
|
|
"""开始监听"""
|
|
self._running = True
|
|
self._thread = threading.Thread(target=self._watch_loop, daemon=True)
|
|
self._thread.start()
|
|
|
|
def stop(self):
|
|
"""停止监听"""
|
|
self._running = False
|
|
if self._thread:
|
|
self._thread.join(timeout=5)
|
|
|
|
def _watch_loop(self):
|
|
"""监听循环"""
|
|
while self._running:
|
|
changed = []
|
|
current_files = {}
|
|
|
|
for watch_dir in self.watch_dirs:
|
|
if watch_dir.exists():
|
|
for f in watch_dir.rglob("*"):
|
|
if f.is_file() and f.suffix in self.extensions:
|
|
fpath = str(f)
|
|
mtime = f.stat().st_mtime
|
|
current_files[fpath] = mtime
|
|
|
|
# 新文件或修改过
|
|
if fpath not in self._file_times:
|
|
changed.append(("new", f))
|
|
elif mtime > self._file_times[fpath]:
|
|
changed.append(("modified", f))
|
|
|
|
# 检查删除的文件
|
|
for fpath in self._file_times:
|
|
if fpath not in current_files:
|
|
changed.append(("deleted", Path(fpath)))
|
|
|
|
if changed:
|
|
self._file_times = current_files
|
|
self.on_change(changed)
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
class HotReloadPlugin(Plugin):
|
|
"""热插拔插件"""
|
|
|
|
def __init__(self):
|
|
self.plugin_loader_instance = None
|
|
self.watcher: Optional[FileWatcher] = None
|
|
self.watch_dirs: list[str] = []
|
|
self.watch_extensions: list[str] = [".py", ".json"]
|
|
|
|
def init(self, deps: dict = None):
|
|
"""初始化"""
|
|
pass
|
|
|
|
def start(self):
|
|
"""启动 - 自动开始监听默认目录"""
|
|
if not self.watch_dirs:
|
|
# 默认监听 store 目录
|
|
self.watch_dirs = ["store"]
|
|
self.start_watching()
|
|
|
|
def stop(self):
|
|
"""停止"""
|
|
if self.watcher:
|
|
self.watcher.stop()
|
|
|
|
def set_plugin_loader(self, plugin_loader):
|
|
"""设置插件加载器实例"""
|
|
self.plugin_loader_instance = plugin_loader
|
|
|
|
def set_watch_dirs(self, dirs: list[str]):
|
|
"""设置监听目录"""
|
|
self.watch_dirs = dirs
|
|
|
|
def start_watching(self):
|
|
"""开始监听文件变化"""
|
|
if self.watch_dirs and self.plugin_loader_instance:
|
|
self.watcher = FileWatcher(
|
|
self.watch_dirs,
|
|
self.watch_extensions,
|
|
self._on_file_change
|
|
)
|
|
self.watcher.start()
|
|
|
|
def _on_file_change(self, changes: list[tuple[str, Path]]):
|
|
"""文件变化回调"""
|
|
for change_type, fpath in changes:
|
|
# 只关心 main.py 和 manifest.json 的变化
|
|
if fpath.name not in ("main.py", "manifest.json"):
|
|
continue
|
|
|
|
plugin_dir = fpath.parent
|
|
plugin_name = plugin_dir.name
|
|
|
|
try:
|
|
if change_type == "new":
|
|
self.load_plugin(plugin_dir)
|
|
elif change_type == "modified":
|
|
self.reload_plugin(plugin_name, plugin_dir)
|
|
elif change_type == "deleted":
|
|
self.unload_plugin(plugin_name)
|
|
except Exception as e:
|
|
Log.error("hot-reload", f"处理变化失败: {e}")
|
|
|
|
def load_plugin(self, plugin_dir: Path) -> bool:
|
|
"""运行时加载插件"""
|
|
try:
|
|
plugin_name = plugin_dir.name
|
|
if plugin_name in self.plugin_loader_instance.plugins:
|
|
raise HotReloadError(f"插件已存在: {plugin_name}")
|
|
|
|
self.plugin_loader_instance.load(plugin_dir)
|
|
info = self.plugin_loader_instance.plugins[plugin_name]
|
|
instance = info["instance"]
|
|
instance.init()
|
|
instance.start()
|
|
return True
|
|
except Exception as e:
|
|
raise HotReloadError(f"加载插件失败: {e}")
|
|
|
|
def unload_plugin(self, plugin_name: str) -> bool:
|
|
"""运行时卸载插件"""
|
|
try:
|
|
if plugin_name not in self.plugin_loader_instance.plugins:
|
|
raise HotReloadError(f"插件不存在: {plugin_name}")
|
|
|
|
info = self.plugin_loader_instance.plugins[plugin_name]
|
|
instance = info["instance"]
|
|
instance.stop()
|
|
|
|
# 从模块缓存中移除
|
|
module = info.get("module")
|
|
if module and module.__name__ in sys.modules:
|
|
del sys.modules[module.__name__]
|
|
|
|
del self.plugin_loader_instance.plugins[plugin_name]
|
|
return True
|
|
except Exception as e:
|
|
raise HotReloadError(f"卸载插件失败: {e}")
|
|
|
|
def reload_plugin(self, plugin_name: str, plugin_dir: Path) -> bool:
|
|
"""运行时更新插件"""
|
|
try:
|
|
# 先卸载
|
|
self.unload_plugin(plugin_name)
|
|
# 再加载
|
|
return self.load_plugin(plugin_dir)
|
|
except Exception as e:
|
|
raise HotReloadError(f"更新插件失败: {e}")
|
|
|
|
|
|
# 注册类型
|
|
register_plugin_type("HotReloadError", HotReloadError)
|
|
register_plugin_type("FileWatcher", FileWatcher)
|
|
|
|
|
|
def New():
|
|
return HotReloadPlugin()
|