Skip to content

Commit 1387163

Browse files
authored
Merge pull request #88 from ljzloser/master
重构插件系统为非阻塞多线程架构
2 parents e276770 + 37e4bd4 commit 1387163

File tree

14 files changed

+1405
-683
lines changed

14 files changed

+1405
-683
lines changed

plugin-dev-tutorial.md

Lines changed: 821 additions & 0 deletions
Large diffs are not rendered by default.

src/plugin_manager/event_dispatcher.py

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
事件分发器
33
44
负责将事件分发给订阅了该事件的插件
5-
支持优先级排序和异常隔离
5+
- 非阻塞:dispatch() 将事件投递到各插件的独立队列后立即返回
6+
- 每个插件在自己的线程中串行消费事件(由 BasePlugin.run() 负责)
7+
- 支持优先级排序和异常隔离
68
"""
79
from __future__ import annotations
810

@@ -29,13 +31,14 @@ class HandlerEntry:
2931

3032
class EventDispatcher:
3133
"""
32-
事件分发器
34+
事件分发器(非阻塞投递模式)
3335
3436
功能:
3537
- 管理事件订阅
3638
- 按优先级分发事件
37-
- 异常隔离(一个处理函数出错不影响其他)
38-
- 线程安全
39+
- dispatch() 不阻塞:将事件投递到各插件的队列,立即返回
40+
- 异常隔离通过各插件线程自行处理
41+
- 背压控制:队列满时丢弃事件并记录警告
3942
"""
4043

4144
def __init__(self):
@@ -56,7 +59,7 @@ def subscribe(
5659
event_type: 事件类型名称
5760
handler: 事件处理函数
5861
priority: 优先级(数值越小越先执行)
59-
plugin: 所属插件(用于取消订阅
62+
plugin: 所属插件(用于取消订阅和队列投递
6063
"""
6164
with self._lock:
6265
entry = HandlerEntry(
@@ -104,7 +107,7 @@ def unsubscribe_all(self, plugin: BasePlugin) -> None:
104107

105108
def dispatch(self, event_type: str, event: Any) -> None:
106109
"""
107-
分发事件给所有订阅者
110+
非阻塞分发事件:将事件投递到各插件的独立队列,立即返回
108111
109112
Args:
110113
event_type: 事件类型名称
@@ -126,31 +129,25 @@ def dispatch(self, event_type: str, event: Any) -> None:
126129
if entry.plugin and not entry.plugin.is_enabled:
127130
continue
128131

129-
try:
130-
entry.handler(event)
131-
except Exception as e:
132-
plugin_name = entry.plugin.name if entry.plugin else "unknown"
133-
logger.error(
134-
f"Handler error in plugin '{plugin_name}' "
135-
f"for event '{event_type}': {e}",
136-
exc_info=True,
137-
)
138-
139-
def dispatch_async(self, event_type: str, event: Any) -> None:
140-
"""
141-
异步分发事件(在新线程中执行)
142-
143-
Args:
144-
event_type: 事件类型名称
145-
event: 事件数据
146-
"""
147-
thread = threading.Thread(
148-
target=self.dispatch,
149-
args=(event_type, event),
150-
daemon=True,
151-
)
152-
thread.start()
153-
132+
if entry.plugin is not None:
133+
# 投递到插件队列(非阻塞)
134+
success = entry.plugin._enqueue_event(entry.handler, event)
135+
if not success:
136+
logger.warning(
137+
f"Dropped event '{event_type}' for plugin "
138+
f"'{entry.plugin.name}' (queue full)"
139+
)
140+
else:
141+
# 无归属插件的 handler(兜底),在调用者线程同步执行
142+
try:
143+
entry.handler(event)
144+
except Exception as e:
145+
logger.error(
146+
f"Handler error (no-plugin) for event "
147+
f"'{event_type}': {e}",
148+
exc_info=True,
149+
)
150+
154151
def get_handlers(self, event_type: str) -> list[HandlerEntry]:
155152
"""获取某事件的所有处理函数"""
156153
with self._lock:

src/plugin_manager/main_window.py

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from typing import TYPE_CHECKING
1313

1414
from PyQt5.QtCore import Qt, pyqtSignal, QPoint, QTimer
15-
from PyQt5.QtGui import QMouseEvent, QIcon, QPixmap
15+
from PyQt5.QtGui import QColor, QMouseEvent, QIcon, QPixmap
1616
from PyQt5.QtWidgets import (
1717
QApplication,
1818
QCheckBox,
@@ -39,7 +39,7 @@
3939
)
4040

4141
from .plugin_state import PluginStateManager, PluginState
42-
from .plugin_base import WindowMode, LogLevel
42+
from .plugin_base import PluginLifecycle, WindowMode, LogLevel
4343
from .app_paths import get_data_dir
4444

4545
if TYPE_CHECKING:
@@ -214,6 +214,15 @@ def _detach_tab(self, index: int, name: str, pos: QPoint | None = None) -> None:
214214

215215
self.tab_detached.emit(name)
216216

217+
def _cleanup_detached(self, name: str) -> None:
218+
"""安全清理 detached 窗口引用"""
219+
if name in self._detached_windows:
220+
w = self._detached_windows[name]
221+
w.blockSignals(True)
222+
w.close()
223+
w.deleteLater()
224+
del self._detached_windows[name]
225+
217226
def _attach_tab(self, name: str) -> None:
218227
"""将弹出的窗口嵌回标签页"""
219228
if name not in self._detached_windows:
@@ -444,6 +453,10 @@ def __init__(self, plugin_manager: PluginManager, parent=None):
444453
# 应用已保存的状态到插件
445454
self._apply_saved_states()
446455

456+
# 连接所有插件的 ready 信号,就绪后自动刷新列表
457+
for p in self._manager.plugins.values():
458+
p.ready.connect(lambda _p=p: self._on_plugin_ready(_p))
459+
447460
self._refresh_plugin_list()
448461

449462
# 定时刷新连接状态
@@ -672,6 +685,13 @@ def _stop_debug(self) -> None:
672685

673686
# ── 插件列表 ────────────────────────────────────────
674687

688+
def _on_plugin_ready(self, plugin) -> None:
689+
"""插件初始化完成,刷新列表显示"""
690+
self._refresh_plugin_list()
691+
self.statusBar().showMessage(
692+
self.tr("插件 {name} 就绪").format(name=plugin.name), 2000
693+
)
694+
675695
def _refresh_plugin_list(self) -> None:
676696
"""刷新插件列表和标签页"""
677697
t = self._tab_widget
@@ -696,12 +716,23 @@ def _refresh_plugin_list(self) -> None:
696716
li = QListWidgetItem(p.name)
697717
li.setData(Qt.UserRole, name)
698718
li.setIcon(p.plugin_icon)
699-
# 已禁用的用灰色
700-
if not p.is_enabled:
701-
li.setForeground(Qt.gray)
719+
720+
lc = p.lifecycle
721+
722+
# 根据生命周期状态显示
723+
if lc == PluginLifecycle.INITIALIZING:
724+
li.setForeground(QColor("#f57c00")) # 橙色:初始化中
725+
li.setText(f"{p.name} (⏳ 初始化中...)")
726+
elif not p.is_enabled:
727+
li.setForeground(Qt.gray) # 灰色:已禁用
728+
elif lc == PluginLifecycle.SHUTTING_DOWN:
729+
li.setForeground(QColor("#c62828")) # 红色:关闭中
730+
li.setText(f"{p.name} (⏸ 关闭中...)")
731+
702732
lst.addItem(li)
703733

704-
if p.widget and name not in t._detached_windows and name not in self._closed_plugins:
734+
# 只有就绪状态才创建窗口(INITIALIZING/STOPPED 不创建)
735+
if p.lifecycle == PluginLifecycle.READY and p.widget and name not in t._detached_windows and name not in self._closed_plugins:
705736
st = self._effective_state(name)
706737
if st.window_mode == WindowMode.DETACHED:
707738
t.add_detachable_tab(p.widget, name, icon=p.plugin_icon)
@@ -748,11 +779,13 @@ def _on_list_context_menu(self, pos) -> None:
748779
menu = QMenu(self)
749780
t = self._tab_widget
750781

751-
# 启用/禁用
782+
# 启用/禁用(非就绪状态不可切换,但 STOPPED 可以重新启用)
783+
lc = plugin.lifecycle
784+
can_control = lc in (PluginLifecycle.READY, PluginLifecycle.STOPPED)
752785
act_enable = menu.addAction("✅ " + self.tr("启用"))
753786
act_disable = menu.addAction("❌ " + self.tr("禁用"))
754-
act_enable.setEnabled(not plugin.is_enabled)
755-
act_disable.setEnabled(plugin.is_enabled)
787+
act_enable.setEnabled(can_control and not plugin.is_enabled)
788+
act_disable.setEnabled(lc == PluginLifecycle.READY and plugin.is_enabled)
756789
act_enable.triggered.connect(lambda: self._toggle_plugin(name, True))
757790
act_disable.triggered.connect(lambda: self._toggle_plugin(name, False))
758791

@@ -777,9 +810,10 @@ def _on_list_context_menu(self, pos) -> None:
777810
act_open = menu.addAction("🖥 " + self.tr("打开窗口"))
778811
act_close = menu.addAction("🚫 " + self.tr("关闭窗口"))
779812

780-
can_open = (has_closed or (not has_tab and plugin.widget is not None))
813+
# 窗口操作只有就绪状态才可用
814+
can_open = lc == PluginLifecycle.READY and (has_closed or (not has_tab and plugin.widget is not None))
781815
act_open.setEnabled(can_open)
782-
act_close.setEnabled(has_tab or has_detached)
816+
act_close.setEnabled(lc == PluginLifecycle.READY and (has_tab or has_detached))
783817

784818
# 打开日志文件
785819
act_log = menu.addAction("📋 " + self.tr("打开日志"))
@@ -801,6 +835,8 @@ def _toggle_plugin(self, name: str, enable: bool) -> None:
801835
if enable:
802836
self._manager.enable_plugin(name)
803837
else:
838+
# 禁用时先关闭窗口(处理 detached 窗口清理),再 shutdown
839+
self._close_plugin_window(name)
804840
self._manager.disable_plugin(name)
805841
self._sync_state(name, enabled=enable)
806842
self._refresh_plugin_list()
@@ -831,15 +867,6 @@ def _open_plugin_window(self, name: str) -> None:
831867
self._closed_plugins.discard(name)
832868
t.add_detachable_tab(plugin.widget, name, icon=plugin.plugin_icon)
833869

834-
def _cleanup_detached(self, name: str) -> None:
835-
"""安全清理 detached 窗口引用"""
836-
if name in self._detached_windows:
837-
w = self._detached_windows[name]
838-
w.blockSignals(True) # 阻止 closeEvent 再次触发 embed_requested
839-
w.close()
840-
w.deleteLater()
841-
del self._detached_windows[name]
842-
843870
def _close_plugin_window(self, name: str) -> None:
844871
"""关闭插件窗口(不销毁)"""
845872
t = self._tab_widget
@@ -904,6 +931,7 @@ def _open_plugin_settings(self, name: str) -> None:
904931
if new_state.enabled:
905932
self._manager.enable_plugin(name)
906933
else:
934+
self._close_plugin_window(name)
907935
self._manager.disable_plugin(name)
908936
# 立即应用日志级别
909937
plugin = self._manager.plugins.get(name)

0 commit comments

Comments
 (0)