Files
model_team_tools/core/plugin/plugin_registry.py
T

197 lines
7.3 KiB
Python
Raw Normal View History

2026-07-20 10:31:10 +08:00
"""插件注册中心。
架构说明:
- PluginWorker 运行在独立 QThread 中,仅做纯 I/O(扫描 .pyd、importlib 读元数据、
文件复制),不创建 QObject、不操作 QWidget。
- PluginRegistry 留在主线程,负责 create_plugin()(返回 QWidget,必须在 GUI 线程
创建)与信号转发。
- 跨线程通信全部走 Qt 信号槽(自动 Queued),不再使用 threading.Thread。
"""
2026-07-17 15:27:33 +08:00
import os
import sys
import time
import logging
import importlib
from pathlib import Path
from enum import Enum, auto
2026-07-20 10:31:10 +08:00
from typing import Optional
2026-07-17 15:27:33 +08:00
2026-07-20 10:31:10 +08:00
from PySide6.QtCore import QObject, Signal, Slot, QThread, QTimer
LOCAL_PLUGINS_PATH = Path("./plugins")
2026-07-17 15:27:33 +08:00
REMOTE_PLUGINS_PATH = Path('Y:/SE/xufeifei/plugins')
logger = logging.getLogger(__name__)
class Event(Enum):
Update = auto()
Install = auto()
Uninstall = auto()
2026-07-20 10:31:10 +08:00
class PluginWorker(QObject):
"""插件发现与安装 worker,运行在独立 QThread 中。
2026-07-17 15:27:33 +08:00
2026-07-20 10:31:10 +08:00
仅做纯 I/O,结果以纯数据(list[dict])形式经信号回传主线程。
"""
2026-07-17 15:27:33 +08:00
2026-07-20 10:31:10 +08:00
local_discovered = Signal(list)
remote_discovered = Signal(list)
install_progress = Signal(int, int)
install_finished = Signal(bool, str)
2026-07-17 15:27:33 +08:00
2026-07-20 10:31:10 +08:00
@Slot(str)
def do_discover_local(self, plugins_dir: str) -> None:
plugins_dir = Path(plugins_dir)
if not self._ensure_dir(plugins_dir):
2026-07-17 15:27:33 +08:00
return
2026-07-20 10:31:10 +08:00
self.local_discovered.emit(self._scan_plugins(plugins_dir))
2026-07-17 15:27:33 +08:00
2026-07-20 10:31:10 +08:00
@Slot(str)
def do_discover_remote(self, plugins_dir: str) -> None:
2026-07-17 15:27:33 +08:00
if not os.path.exists(plugins_dir):
logger.error("服务器链接错误!")
return
2026-07-20 10:31:10 +08:00
self.remote_discovered.emit(
self._scan_plugins(Path(plugins_dir), with_remote_meta=True)
)
2026-07-17 15:27:33 +08:00
2026-07-20 10:31:10 +08:00
@Slot(str, str)
def do_install(self, local_path: str, remote_path: str) -> None:
2026-07-17 15:27:33 +08:00
try:
total_size = os.path.getsize(remote_path)
copied_size = 0
with open(remote_path, 'rb') as fsrc, open(local_path, 'wb') as fdst:
while True:
2026-07-20 10:31:10 +08:00
buf = fsrc.read(1024 * 1024)
2026-07-17 15:27:33 +08:00
if not buf:
break
fdst.write(buf)
copied_size += len(buf)
2026-07-20 10:31:10 +08:00
self.install_progress.emit(copied_size, total_size)
2026-07-17 15:27:33 +08:00
time.sleep(0.1)
2026-07-20 10:31:10 +08:00
self.install_finished.emit(True, f"{local_path}安装成功!!!")
2026-07-17 15:27:33 +08:00
except Exception as e:
logger.error(f"{remote_path}安装失败{e.args}")
2026-07-20 10:31:10 +08:00
self.install_finished.emit(False, str(e))
@staticmethod
def _ensure_dir(plugins_dir: Path) -> bool:
if not os.path.exists(plugins_dir):
os.mkdir(plugins_dir)
return False
return True
@staticmethod
def _scan_plugins(plugins_dir: Path, with_remote_meta: bool = False) -> list:
results: list = []
sys.path.append(str(plugins_dir))
try:
for tool in plugins_dir.glob("*.pyd"):
tool_name = tool.stem
try:
module = importlib.import_module(tool_name)
item = {
"tool_name": tool_name,
"name": module.read_plugin_name(),
"version": module.read_plugin_version(),
}
if with_remote_meta:
item["description"] = module.read_plugin_description()
item["remote_path"] = str(plugins_dir)
results.append(item)
except Exception as e:
logger.error(f"插件{tool_name}加载失败:{e}")
finally:
sys.path.remove(str(plugins_dir))
return results
class PluginRegistry(QObject):
"""插件注册中心。
主线程负责创建插件实例(create_plugin 返回 QWidget,必须在 GUI 线程创建);
发现与安装的 I/O 由 PluginWorker 在独立 QThread 中执行,结果通过信号回传
主线程。所有跨线程通信经 Qt 信号槽(Queued)。
"""
plugins_loader_signal = Signal(dict)
update_plugins_card_signal = Signal(dict)
_discover_local_requested = Signal(str)
_discover_remote_requested = Signal(str)
_install_requested = Signal(str, str)
def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent)
self.plugins: dict = {}
self._worker = PluginWorker()
self._thread = QThread()
self._worker.moveToThread(self._thread)
self._discover_local_requested.connect(self._worker.do_discover_local)
self._discover_remote_requested.connect(self._worker.do_discover_remote)
self._install_requested.connect(self._worker.do_install)
self._worker.local_discovered.connect(self._on_local_discovered)
self._worker.remote_discovered.connect(self._on_remote_discovered)
self._worker.install_finished.connect(self._on_install_finished)
self._thread.start()
# 延迟到事件循环启动后触发,保证所有接收方先 connect 后 emit
QTimer.singleShot(0, self._start_discovery)
def _start_discovery(self) -> None:
self._discover_local_requested.emit(str(LOCAL_PLUGINS_PATH))
self._discover_remote_requested.emit(str(REMOTE_PLUGINS_PATH))
@Slot(list)
def _on_local_discovered(self, results: list) -> None:
for item in results:
name = item["name"]
if name not in self.plugins:
self.plugins[name] = {}
module = importlib.import_module(item["tool_name"])
self.plugins[name]["obj"] = module.create_plugin()
self.plugins[name]["local version"] = item["version"]
self.plugins_loader_signal.emit(self.plugins)
@Slot(list)
def _on_remote_discovered(self, results: list) -> None:
for item in results:
name = item["name"]
if name not in self.plugins:
self.plugins[name] = {}
self.plugins[name]["local version"] = None
self.plugins[name]["local description"] = None
self.plugins[name]["tool_name"] = item["tool_name"]
self.plugins[name]["remote version"] = item["version"]
self.plugins[name]["remote description"] = item["description"]
self.plugins[name]["remote path"] = item["remote_path"]
self.update_plugins_card_signal.emit(self.plugins)
@Slot(bool, str)
def _on_install_finished(self, success: bool, message: str) -> None:
if success:
logger.info(message)
self._start_discovery()
# self._discover_local_requested.emit(str(LOCAL_PLUGINS_PATH))
else:
logger.error(message)
def start_plugins_event(self, event: Event, name: str) -> None:
if event == Event.Install:
tool_name = self.plugins[name]['tool_name']
local_path = str(LOCAL_PLUGINS_PATH) + '\\' + f"{tool_name}.pyd"
remote_path = str(REMOTE_PLUGINS_PATH) + '\\' + f"{tool_name}.pyd"
self._install_requested.emit(local_path, remote_path)
elif event == Event.Uninstall:
tool_name = self.plugins[name]['tool_name']
local_path = str(LOCAL_PLUGINS_PATH) + '\\' + f"{tool_name}.pyd"
# os.
pass