From aaad6b8aa647e7ab1582bc133b9d40e812c0aa96 Mon Sep 17 00:00:00 2001 From: "feifei.xu" <307327147@qq.com> Date: Mon, 20 Jul 2026 10:31:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E7=A1=80=E5=8A=9F=E8=83=BD=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/__init__.py | 2 +- core/plugin/plugin_registry.py | 241 +++++++++++++++++++++------------ core/plugin/plugins_card.py | 11 +- main.py | 13 +- 4 files changed, 175 insertions(+), 92 deletions(-) diff --git a/core/__init__.py b/core/__init__.py index 59c4d89..6afbdb4 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -15,5 +15,5 @@ from .logging.log import FrameLog from .plugin.plugins import Plugins from .plugin.plugin_registry import PluginRegistry -__all__ = ["FrameLog", "ToolsWindow","PluginRegistry"] +__all__ = ["FrameLog", "Plugins","PluginRegistry"] diff --git a/core/plugin/plugin_registry.py b/core/plugin/plugin_registry.py index 55c0cea..20b5103 100644 --- a/core/plugin/plugin_registry.py +++ b/core/plugin/plugin_registry.py @@ -1,17 +1,25 @@ +"""插件注册中心。 + +架构说明: +- PluginWorker 运行在独立 QThread 中,仅做纯 I/O(扫描 .pyd、importlib 读元数据、 + 文件复制),不创建 QObject、不操作 QWidget。 +- PluginRegistry 留在主线程,负责 create_plugin()(返回 QWidget,必须在 GUI 线程 + 创建)与信号转发。 +- 跨线程通信全部走 Qt 信号槽(自动 Queued),不再使用 threading.Thread。 +""" import os import sys import time import logging import importlib -import threading - from pathlib import Path from enum import Enum, auto -from PySide6.QtCore import QObject,Signal -from dataclasses import dataclass, field, fields +from typing import Optional -LOCAL_PLUGINS_PATH = Path("./plugins") +from PySide6.QtCore import QObject, Signal, Slot, QThread, QTimer + +LOCAL_PLUGINS_PATH = Path("./plugins") REMOTE_PLUGINS_PATH = Path('Y:/SE/xufeifei/plugins') logger = logging.getLogger(__name__) @@ -22,106 +30,167 @@ class Event(Enum): Install = auto() Uninstall = auto() -class PluginRegistry(QObject): - """插件注册中心。""" - plugins_loader_signal = Signal(dict) - update_plugins_card_signal = Signal(dict) - def __init__(self, parent = None) -> None: - super().__init__(parent) - self.plugins = dict() +class PluginWorker(QObject): + """插件发现与安装 worker,运行在独立 QThread 中。 - self.start_discover_local(LOCAL_PLUGINS_PATH, self.plugins) - self.start_discover_remote(REMOTE_PLUGINS_PATH, self.plugins) + 仅做纯 I/O,结果以纯数据(list[dict])形式经信号回传主线程。 + """ - def start_discover_local(self,plugins_dir: Path, plugins:dict): - self.thread_local = threading.Thread(target= self.discover_local_event,args=(plugins_dir, plugins,)) - self.thread_local.start() + local_discovered = Signal(list) + remote_discovered = Signal(list) + install_progress = Signal(int, int) + install_finished = Signal(bool, str) - def discover_local_event(self, plugins_dir: Path, plugins:dict): - if not os.path.exists(plugins_dir): - os.mkdir(plugins_dir) + @Slot(str) + def do_discover_local(self, plugins_dir: str) -> None: + plugins_dir = Path(plugins_dir) + if not self._ensure_dir(plugins_dir): return - - sys.path.append(str(plugins_dir)) - for tool in Path(plugins_dir).glob("*.pyd"): - tool_name = tool.stem - try: - module = importlib.import_module(tool_name) - obj = module.create_plugin() - name = module.read_plugin_name() - version = module.read_plugin_version() - if name in plugins.keys(): - pass - else: - plugins[name] = {} - plugins[name]['obj'] = obj - plugins[name]['local version'] = version - except Exception as e: - logger.error(f"插件{tool_name}注册失败,{str(e)}") - sys.path.remove(str(LOCAL_PLUGINS_PATH)) - self.plugins_loader_signal.emit(plugins) + self.local_discovered.emit(self._scan_plugins(plugins_dir)) - - def start_discover_remote(self,plugins_dir: Path, plugins:dict): - self.thread_remote = threading.Thread(target= self.discover_remote_event,args=(plugins_dir, plugins,)) - self.thread_remote.start() - - def discover_remote_event(self, plugins_dir: Path, plugins:dict): + @Slot(str) + def do_discover_remote(self, plugins_dir: str) -> None: if not os.path.exists(plugins_dir): logger.error("服务器链接错误!") return - #读取服务器工具信息 - time.sleep(0.01) - sys.path.append(str(plugins_dir)) - for tool in Path(plugins_dir).glob("*.pyd"): - tool_name = tool.stem - try: - module = importlib.import_module(tool_name) + self.remote_discovered.emit( + self._scan_plugins(Path(plugins_dir), with_remote_meta=True) + ) - name = module.read_plugin_name() - version = module.read_plugin_version() - description = module.read_plugin_description() - if name in plugins.keys(): - pass - else: - print("discover_remote_event") - plugins[name] = {} - plugins[name]["tool_name"] = tool_name - plugins[name]["local version"] = None - plugins[name]["local description"] = None - plugins[name]["remote version"] = version - plugins[name]["remote description"] = description - plugins[name]['remote path'] = plugins_dir - except Exception as e: - logger.error(f"服务器{tool_name}无法加载:{str(e)}") - sys.path.remove(str(plugins_dir)) - self.update_plugins_card_signal.emit(plugins) - - def start_plugins_event(self, event:Event, name:str): - if event == Event.Install: - local_path = str(LOCAL_PLUGINS_PATH) + '\\' + f"{self.plugins[name]['tool_name']}.pyd" - remote_path = str(REMOTE_PLUGINS_PATH) + '\\' + f"{self.plugins[name]['tool_name']}.pyd" - - self.sub_thread = threading.Thread(target= self.install_plugins_event,args=(local_path,remote_path,)) - self.sub_thread.start() - pass - - def install_plugins_event(self, local_path, remote_path): + @Slot(str, str) + def do_install(self, local_path: str, remote_path: str) -> None: 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: - buf = fsrc.read(1024*1024) + buf = fsrc.read(1024 * 1024) if not buf: break fdst.write(buf) copied_size += len(buf) - logger.info(f"安装进度: {copied_size/1024/1024:.2f} MB / {total_size/1024/1024:.2f} MB") + self.install_progress.emit(copied_size, total_size) time.sleep(0.1) - logger.info(f"{local_path}安装成功!!!") + self.install_finished.emit(True, f"{local_path}安装成功!!!") except Exception as e: logger.error(f"{remote_path}安装失败{e.args}") - return \ No newline at end of file + 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 diff --git a/core/plugin/plugins_card.py b/core/plugin/plugins_card.py index 7a1f493..45bfc22 100644 --- a/core/plugin/plugins_card.py +++ b/core/plugin/plugins_card.py @@ -36,7 +36,8 @@ class PluginsCard(QFrame, Ui_FramePluginsCard): self.pushButtonInstall.setEnabled(False) if self.info['remote version'] <= self.info['local version']: self.pushButtonUpdate.setEnabled(False) - self.pushButtonInstall.clicked.connect(self.on_intsall_event) + self.pushButtonInstall.clicked.connect(self.on_install_event) + self.pushButtonUninstall.clicked.connect(self.on_uninstall_event) def to_card(self): card = f"# {self.name}\n\n" @@ -48,5 +49,9 @@ class PluginsCard(QFrame, Ui_FramePluginsCard): card += f"**描述:** {self.info['remote description']}" return card - def on_intsall_event(self): - self.send_event_signal.emit(Event.Install, self.name) \ No newline at end of file + def on_install_event(self): + self.send_event_signal.emit(Event.Install, self.name) + + def on_uninstall_event(self): + self.send_event_signal.emit(Event.Uninstall, self.name) + pass \ No newline at end of file diff --git a/main.py b/main.py index caeae50..2910eee 100644 --- a/main.py +++ b/main.py @@ -20,8 +20,9 @@ import importlib from pathlib import Path from typing import Optional -from PySide6.QtGui import QIcon -from PySide6.QtWidgets import QApplication, QMainWindow, QMenuBar, QStatusBar,QFileDialog +# from PySide6.QtCore import QAction +from PySide6.QtGui import QIcon,QAction +from PySide6.QtWidgets import QApplication, QMainWindow, QMenuBar, QStatusBar from qt_material import apply_stylesheet from main_ui import Ui_MainWindow # 获取模块日志记录器 @@ -121,8 +122,16 @@ class MainWindow(QMainWindow, Ui_MainWindow): def on_plugins_loader(self, plugins:dict): for name in plugins.keys(): + action = QAction(name,self) + self.toolBar.addAction(action) + action.triggered.connect(lambda checked, action=action: self.on_action_triggered(action)) + self.stackedWidget.addWidget(plugins[name]['obj']) + def on_action_triggered(self, action:QAction): + print(action.text()) + self.stackedWidget.setCurrentWidget(self.plugin_registry.plugins[action.text()]['obj']) + if __name__ == '__main__': app = QApplication(sys.argv)