Files
model_team_tools/main.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

188 lines
6.5 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.
"""
Model Team Tools - 主程序入口
应用程序主窗口的初始化和配置模块
使用 PySide6 构建的桌面应用程序框架
主要功能:
- 初始化 Qt 主窗口
- 加载 Qt Material 样式表
- 配置菜单栏和状态栏
- 提供统一的日志记录接口
"""
import os
import sys
import logging
import core
import traceback
import importlib
from pathlib import Path
from typing import Optional
from PySide6.QtCore import QProcess, QTimer
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
# 获取模块日志记录器
logger = logging.getLogger(__name__)
# 版本号常量
MAJOR_VER: int = 0 # 主版本号
MINOR_VER: int = 0 # 次版本号
MICRO_VER: int = 1 # 修订版本号
APP_NAME: str = "Model Team Tools"
APP_VERSION: str = f"{MAJOR_VER}.{MINOR_VER}.{MICRO_VER}"
LOCAL_PLUGINS_PATH = Path("./plugins")
class MainWindow(QMainWindow, Ui_MainWindow):
"""
主窗口类
继承自 QMainWindowQt 主窗口基类)和 Ui_MainWindowQt Designer 生成的 UI 界面)
负责应用程序主窗口的初始化、样式配置和界面布局
"""
def __init__(self) -> None:
super().__init__()
self.setupUi(self)
self.plugin_registry = core.PluginRegistry()
self.plugins = core.Plugins(self.plugin_registry)
self.plugin_registry.plugins_loader_signal.connect(self.on_plugins_loader)
self.plugin_registry.uninstall_completed_signal.connect(self.on_plugin_uninstalled)
self.tool_actions: dict = {}
self.plugin_widgets: dict = {}
self.initUI()
def initUI(self) -> None:
"""
初始化用户界面组件
执行以下初始化操作:
- 加载 Qt 样式表和自定义样式
- 设置窗口标题
- 配置菜单栏
- 配置状态栏
- 记录初始化日志
注意:此方法在主窗口创建后调用
"""
self.load_stylesheet()
self.setWindowTitle(APP_NAME)
self.setWindowIcon(QIcon('resources/main.ico'))
self.menubar: Optional[QMenuBar] = self.menuBar()
self.statusbar: QStatusBar = self.statusBar()
self.statusbar.showMessage(f"Version: {APP_VERSION}")
self.action_tools.triggered.connect(self.on_tools)
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 on_tools(self) -> None:
"""打开工具箱窗口
创建并显示工具箱窗口实例。包含完整的错误处理机制,
确保任何异常都不会导致主程序崩溃。
异常处理:
- 捕获所有异常并记录到日志
- 显示详细的错误堆栈信息
- 不影响主窗口的正常使用
日志记录:
- 成功打开工具箱时记录 INFO 级别日志
- 打开失败时记录 ERROR 级别日志
"""
try:
self.plugins.show()
except Exception as e:
logger.error(f"无法打开工具箱窗口: {e}")
logger.error(traceback.format_exc())
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))
widget = plugins[name]['obj']
self.stackedWidget.addWidget(widget)
self.tool_actions[name] = action
self.plugin_widgets[name] = widget
def on_action_triggered(self, action:QAction):
print(action.text())
self.stackedWidget.setCurrentWidget(self.plugin_registry.plugins[action.text()]['obj'])
def on_plugin_uninstalled(self, name: str) -> None:
"""卸载完成后清理主窗口工具栏与堆栈窗口中的对应项,并自动重启。
.pyd 已被进程加载,运行时无法删除(Windows 文件锁),因此重启进程:
新进程启动时会先执行待删清单,在 import 之前删除 .pyd 文件。
"""
action = self.tool_actions.pop(name, None)
if action is not None:
self.toolBar.removeAction(action)
action.deleteLater()
widget = self.plugin_widgets.pop(name, None)
if widget is not None:
self.stackedWidget.removeWidget(widget)
widget.deleteLater()
self.statusbar.showMessage(f"{name} 已卸载,应用即将重启以完成清理...", 1500)
QTimer.singleShot(1000, self._restart_app)
def _restart_app(self) -> None:
"""启动新进程并退出当前进程。
sys.executable + sys.argv 同时兼容开发模式(python.exe main.py
与打包模式(main.exe)。
"""
work_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
# 【重要】添加防递归标记,防止新进程启动后再次触发重启逻辑
args = sys.argv[:]
if '--restarting' not in args:
args.append('--restarting')
# 启动新进程
pid = QProcess.startDetached(sys.executable, args, work_dir)
if pid:
print(f"新进程启动成功 (PID: {pid}),准备退出当前进程...")
# ✅ 关键修复:延迟 300ms 退出,确保新进程彻底“断奶”
QTimer.singleShot(300, QApplication.quit)
else:
print("重启失败,请检查路径或权限")
if __name__ == '__main__':
app = QApplication(sys.argv)
main: MainWindow = MainWindow()
app.aboutToQuit.connect(main.plugin_registry.shutdown)
main.show()
sys.exit(app.exec())