#!/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() 导致跳过后续订阅者。 """ with self._lock: snapshot = list(self._subscribers.get(event_name, [])) for callback in snapshot: try: callback(data) 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()