Files
model_team_tools/core/plugin/plugins.py
T
feifei.xu 51e30d41c6 feat(plugin): 实现插件卸载功能与自重启机制
目的:补齐 Event.Uninstall 空实现,打通卸载全链路。

背景:原 plugin_registry.py 的 Uninstall 分支为空 pass;运行时直接
os.remove 删 .pyd 触发 WinError 5(Windows 文件锁,.pyd 已被 import)。

方案:
- 卸载流程分层清理:Worker 写待删清单 → Registry 清字典/obj →
  main 清 toolBar/stackedWidget → plugins_card 刷新为未安装
- .pyd 文件删除延迟到下次启动 _process_pending_uninstalls,
  在 worker 线程启动前、import 之前执行,避开文件锁
- 卸载后自动重启:QProcess.startDetached + QApplication.quit,
  兼容开发模式(python.exe)与打包模式(main.exe)
- _remove_with_retry 应对自重启时原进程句柄未释放的暂态竞争
- _on_local_discovered 加异常隔离,单插件失败不拖垮初始化
- QThread 生命周期管理:aboutToQuit → shutdown(quit+wait),
  finished → worker.deleteLater,消除 Destroyed 警告
- plugins_card 抽 refresh() 方法,统一卡片状态刷新

影响:卸载功能可用;自重启在命令行/打包模式正常,调试器环境
可能因子进程管理闪退(不影响交付)。
2026-07-20 13:15:16 +08:00

158 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
工具箱模块
提供工具箱主窗口,包含各种实用工具的入口。
使用 PySide6 构建的窗口框架。
主要功能:
- 工具箱主窗口界面
- 窗口居中显示功能
- 工具分类和导航
Author: Model Team
Version: 0.0.1
"""
import os
import sys
import logging
import threading
import importlib
from qt_material import apply_stylesheet
from pathlib import Path
from .plugins_ui import Ui_FormPlugins
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QWidget, QListWidgetItem
from PySide6.QtCore import QSize,Signal
from .plugins_card import PluginsCard
from .plugin_registry import PluginRegistry
logger = logging.getLogger(__name__)
LOCAL_PLUGINS_PATH = Path("./plugins")
REMOTE_PLUGINS_PATH = Path('Y:/SE/xufeifei/plugins')
class Plugins(QWidget, Ui_FormPlugins):
"""工具箱主窗口类
继承自 QWidgetQt 窗口基类)和 Ui_FormToolsQt Designer 生成的 UI 界面)
负责显示工具箱的主界面,提供工具的分类和导航功能。
窗口特性:
- 默认尺寸: 400x293 像素
- 支持窗口居中显示
- 可作为独立窗口或嵌入其他窗口使用
Attributes:
parent: 父窗口对象,默认为 None(顶级窗口)
"""
update_tool_card = Signal(dict)
def __init__(self, plugin_registry:PluginRegistry, parent: QWidget = None) -> None:
"""初始化工具箱窗口
创建工具箱窗口实例,初始化 UI 界面。
Args:
parent: 父窗口对象,用于建立父子关系。
默认为 None,表示这是顶级窗口。
"""
super().__init__(parent)
self.setupUi(self)
self.plugin_registry = plugin_registry
self.cards: dict = {}
self.initUI()
self.load_stylesheet()
self.plugin_registry.update_plugins_card_signal.connect(self.on_update_plugins_card)
self.plugin_registry.uninstall_completed_signal.connect(self.on_plugin_uninstalled)
def initUI(self) -> None:
"""
初始化用户界面
配置窗口标题、图标、大小和居中显示。
"""
self.setWindowTitle("工具箱")
self.setWindowIcon(QIcon('resources/tools.ico'))
# self.update_tool_card.connect(self.on_update_tool_card)
def load_stylesheet(self) -> None:
"""
加载 Qt 样式表和自定义样式文件
使用 qt_material 库加载暗色主题,并应用自定义 QSS 样式表
失败时记录错误日志,但不影响应用继续运行(降级策略)
异常处理:
- FileNotFoundError: 样式文件不存在
- RuntimeError: 主题加载失败
- 其他异常: 记录警告但不中断应用
"""
try:
apply_stylesheet(self, style=None,theme='dark_teal.xml', css_file='resources/my.qss')
except FileNotFoundError as e:
logger.warning(f"样式文件未找到,使用默认主题: {e}")
except RuntimeError as e:
logger.warning(f"主题加载失败,使用默认主题: {e}")
except Exception as e:
logger.warning(f"样式加载失败,使用默认主题: {e}")
# def start_load_tools(self):
# self.sub_thread = threading.Thread(target= self.load_tool_list,args=(self.tools,))
# self.sub_thread.start()
# def load_tool_list(self, tools:dict):
# if not os.path.exists(REMOTE_PLUGINS_PATH):
# logger.error("服务器链接错误!")
# return
# #读取服务器工具信息
# sys.path.append(str(REMOTE_PLUGINS_PATH))
# for tool in Path(REMOTE_PLUGINS_PATH).glob("*.pyd"):
# tool_name = tool.stem
# try:
# module = importlib.import_module(tool_name)
# name = module.read_tool_name()
# version = module.read_tool_version()
# description = module.read_tool_description()
# if name in tools.keys():
# pass
# else:
# tools[name] = {}
# tools[name]["name"] = tool_name
# tools[name]["local version"] = None
# tools[name]["local description"] = None
# tools[name]["remote version"] = version
# tools[name]["remote description"] = description
# tools[name]['remote path'] = REMOTE_PLUGINS_PATH
# except Exception as e:
# logger.error(f"服务器{tool_name}无法加载:{str(e)}")
# sys.path.remove(str(REMOTE_PLUGINS_PATH))
# self.update_tool_card.emit(tools)
def on_update_plugins_card(self, plugins:dict):
# print(plugins)
for name in plugins.keys():
item = QListWidgetItem(self.listWidget)
plugin_card = PluginsCard(name, plugins[name])
plugin_card.send_event_signal.connect(self.plugin_registry.start_plugins_event)
self.cards[name] = plugin_card
item.setSizeHint(QSize(plugin_card.sizeHint().width(), 200))
self.listWidget.addItem(item)
self.listWidget.setCurrentItem(item)
self.listWidget.setItemWidget(item, plugin_card)
pass
def on_plugin_uninstalled(self, name: str) -> None:
"""卸载完成后刷新对应卡片状态。"""
card = self.cards.get(name)
if card is not None:
card.refresh()