93 lines
3.8 KiB
Python
93 lines
3.8 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
轻量级事件总线
|
||
|
||
支持 subscribe(event_name, callback)、unsubscribe(event_name, callback)
|
||
和 publish(event_name, data),用于步骤面板间的去中心化参数传导。
|
||
|
||
2026-06-30 修复:
|
||
- publish() 中的 except Exception: pass 改为 traceback 日志输出(静默吞异常会导致 bug 无法发现)
|
||
- 新增 unsubscribe() 方法,允许面板销毁时清理订阅,防止内存泄漏和野指针回调
|
||
"""
|
||
|
||
import sys
|
||
import threading
|
||
import traceback
|
||
from collections import defaultdict
|
||
from typing import Any, Callable, Dict, List, Optional
|
||
|
||
|
||
class EventBus:
|
||
"""发布-订阅事件总线(线程安全)。"""
|
||
|
||
def __init__(self):
|
||
self._subscribers: Dict[str, List[Callable]] = defaultdict(list)
|
||
self._lock = threading.Lock()
|
||
# 异常日志回调(可选注入,供 LogManager 使用)
|
||
self._error_logger: Optional[Callable[[str], None]] = None
|
||
|
||
def set_error_logger(self, logger: Callable[[str], None]):
|
||
"""注入异常日志回调。若不设置,异常信息输出到 stderr。"""
|
||
self._error_logger = logger
|
||
|
||
def subscribe(self, event_name: str, callback: Callable[[dict], None]):
|
||
"""订阅事件。callback 接收一个 dict 作为事件数据。"""
|
||
with self._lock:
|
||
if callback not in self._subscribers[event_name]:
|
||
self._subscribers[event_name].append(callback)
|
||
|
||
def unsubscribe(self, event_name: str, callback: Callable[[dict], None]):
|
||
"""取消订阅。面板销毁或不再需要接收事件时调用,防止野指针回调。"""
|
||
with self._lock:
|
||
subs = self._subscribers.get(event_name)
|
||
if subs and callback in subs:
|
||
subs.remove(callback)
|
||
# 清理空列表,避免字典膨胀
|
||
if not subs:
|
||
del self._subscribers[event_name]
|
||
|
||
def publish(self, event_name: str, data: Dict[str, Any]):
|
||
"""发布事件,通知所有订阅者。订阅者异常不再静默吞掉,而是输出 traceback。
|
||
|
||
迭代订阅者列表的副本,防止回调中调用 unsubscribe() 导致跳过后续订阅者。
|
||
|
||
2026-07-01 修复:捕获 RuntimeError(sip 野指针异常)防止已删除的 C++ 对象回调崩溃。
|
||
"""
|
||
with self._lock:
|
||
snapshot = list(self._subscribers.get(event_name, []))
|
||
for callback in snapshot:
|
||
try:
|
||
callback(data)
|
||
except RuntimeError:
|
||
# ★ 野指针保护:C++ 对象已删除时 sip 抛出 RuntimeError,静默跳过。
|
||
cb_name = getattr(callback, '__name__', None) or repr(callback)
|
||
err_msg = (
|
||
f"[EventBus] 事件 '{event_name}' 的订阅者 {cb_name!r} C++ 对象已销毁,跳过。\n"
|
||
+ traceback.format_exc()
|
||
)
|
||
if self._error_logger:
|
||
try:
|
||
self._error_logger(err_msg)
|
||
except Exception:
|
||
print(err_msg, file=sys.stderr, flush=True)
|
||
else:
|
||
print(err_msg, file=sys.stderr, flush=True)
|
||
except Exception:
|
||
cb_name = getattr(callback, '__name__', None) or repr(callback)
|
||
err_msg = (
|
||
f"[EventBus] 事件 '{event_name}' 的订阅者 {cb_name!r} 抛出异常:\n"
|
||
+ traceback.format_exc()
|
||
)
|
||
if self._error_logger:
|
||
try:
|
||
self._error_logger(err_msg)
|
||
except Exception:
|
||
print(err_msg, file=sys.stderr, flush=True)
|
||
else:
|
||
print(err_msg, file=sys.stderr, flush=True)
|
||
|
||
|
||
# 全局单例
|
||
global_event_bus = EventBus()
|