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() 方法,统一卡片状态刷新 影响:卸载功能可用;自重启在命令行/打包模式正常,调试器环境 可能因子进程管理闪退(不影响交付)。
This commit is contained in:
@@ -20,7 +20,7 @@ import importlib
|
||||
from pathlib import Path
|
||||
|
||||
from typing import Optional
|
||||
# from PySide6.QtCore import QAction
|
||||
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
|
||||
@@ -53,6 +53,10 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
||||
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()
|
||||
|
||||
@@ -125,16 +129,60 @@ class MainWindow(QMainWindow, Ui_MainWindow):
|
||||
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'])
|
||||
|
||||
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())
|
||||
Reference in New Issue
Block a user