跳转至

crates\nfarms-plugin\src\context.rs

源码路径:crates\nfarms-plugin\src\context.rs · 620 行

//! 作用域 / 服务容器 / 事件总线。

use std::any::Any;
use std::collections::BTreeMap;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, OnceLock, RwLock};

use crate::plugin::{PluginError, Result};

/// 注册进 [Context] 的服务。
///
/// 用 `Arc<dyn Any + Send + Sync>` 而不是泛型 map:服务表要在**运行时**
/// 按名字查,类型信息只在取出的时候才用得上(见 [`Context::get`])。
pub type Service = Arc<dyn Any + Send + Sync>;

/// 内核事件。
///
/// 用枚举而不是 `&str` + `Box<dyn Any>`:事件的种类是可数的,
/// 枚举能让编译器帮忙查漏,字符串名只会把错误推迟到运行期。
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
    /// 插件装载成功
    PluginLoaded { name: String },
    /// 插件装载失败(依赖缺失不算失败,那种是 Pending)
    PluginFailed { name: String, error: String },
    /// 插件被卸载
    PluginDisposed { name: String },
    /// 有一个服务被提供出来。内核靠它重试等待中的插件。
    ServiceProvided { name: String },
    /// 切到了某一页
    PageOpened { id: String },
    /// 设置写盘成功
    SettingsSaved { path: String },
    /// 界面产生的一条日志(插件里想记点什么,又不想直接依赖 nfarms-core::Log 时用它)
    Log { source: String, message: String },
}

pub type Listener = Arc<dyn Fn(&Event) + Send + Sync>;

/// [`Context::on`] 的返回值,用来取消订阅。
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub struct ListenerId(u64);

struct Inner {
    /// 作用域名。只用于日志和界面展示 —— cordis 里叫 `ctx.name`。
    name: String,
    /// 父作用域。服务与事件都**向上**可见,不向下。
    parent: Option<Arc<Inner>>,

    services: RwLock<BTreeMap<&'static str, Service>>,
    listeners: RwLock<Vec<(ListenerId, Listener)>>,

    /// 服务被 provide 时的回调。内核装在这里,免得 Context 反过来依赖 Kernel。
    ///
    /// 参数是 `&'static str`(服务名本来就是字面量),这样回调能把它直接记进
    /// `Vec<&'static str>`,不用先 clone 出一个 String。
    ///
    /// 类型是 `Arc<OnceLock<..>>` 而不是裸 `OnceLock<..>`:fork 出去的子作用域
    /// 必须和父作用域**共享同一个单元格**。裸 `OnceLock` 的 `clone` 只是复制当前
    /// 内容(`None` clone 出来还是 `None`,且父子是两个独立格子),于是"先 fork、
    /// 后由 `Kernel::new` set 钩子"的子作用域永远收不到通知 —— 它 provide 的服务
    /// 不会广播给内核,等这个服务的插件就永远停在 Pending。
    /// 共享单元格之后,钩子在哪一层被 set,整棵作用域树都看得见
    /// (见测试 `provide_hook_is_shared_with_forks_made_before_it`)。
    on_provide: Arc<OnceLock<ProvideHook>>,
}

/// 插件之间共享的上下文。
///
/// `Context` 是**句柄**:clone 它不会复制任何东西,两个 clone 指向同一个
/// 作用域。这也是为什么它可以随手塞进闭包、存进结构体。
/// provide 钩子的类型。
///
/// **抽成别名不只是为了短。** 写全了是
/// `Arc<OnceLock<Arc<dyn Fn(&'static str) + Send + Sync>>>` ——
/// 一眼看不出"它就是个回调",而它在签名里出现多次。
/// clippy 的 `type_complexity` 提的就是这件事。
pub type ProvideHook = Arc<dyn Fn(&'static str) + Send + Sync>;

#[derive(Clone)]
pub struct Context {
    inner: Arc<Inner>,
}

impl std::fmt::Debug for Context {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Context")
            .field("scope", &self.inner.name)
            .field("services", &self.service_names())
            .finish()
    }
}

static NEXT_LISTENER: AtomicU64 = AtomicU64::new(1);

impl Context {
    /// 建一个根作用域。
    pub fn root(name: impl Into<String>) -> Self {
        Self {
            inner: Arc::new(Inner {
                name: name.into(),
                parent: None,
                services: RwLock::new(BTreeMap::new()),
                listeners: RwLock::new(Vec::new()),
                on_provide: Arc::new(OnceLock::new()),
            }),
        }
    }

    /// 开一个子作用域。
    ///
    /// 子作用域里 `provide` 的服务**只有它自己和它的后代**看得见 ——
    /// 这是插件的隔离手段:两个插件分别 fork 一个作用域,各自注册同名的
    /// 服务互不干扰。
    ///
    /// 父子之间只有两条通路:服务/事件**向上可见**,以及 `on_provide` 钩子那个
    /// **共享单元格**。所以钩子在 fork 之前还是之后 set 都算数。
    ///
    /// ⚠️ 目前生产代码里没有任何地方调 `fork`(`crates/nfarms-gui` 只走
    /// `Kernel::load` / `load_all`),这套隔离能力**只被本文件的单元测试覆盖**,
    /// 算是"已经写好的设施"而不是"在跑的功能"。要拿它给真插件做隔离之前,
    /// 先补一条跨 crate 的集成测试 —— 否则"两个插件各 fork 一个作用域互不干扰"
    /// 只是纸面承诺;在那之前,这里的语义改动没人会替你发现坏掉。
    pub fn fork(&self, name: impl Into<String>) -> Self {
        Self {
            inner: Arc::new(Inner {
                name: name.into(),
                parent: Some(self.inner.clone()),
                services: RwLock::new(BTreeMap::new()),
                listeners: RwLock::new(Vec::new()),
                // clone 的是 `Arc`,父子指向同一个 `OnceLock`:见 `Inner::on_provide` 的注释
                on_provide: self.inner.on_provide.clone(),
            }),
        }
    }

    pub fn name(&self) -> &str {
        &self.inner.name
    }

    /// 服务被 provide 时通知内核。只有 [crate::Kernel] 该调它。
    ///
    /// 钩子住在一个**全树共享**的单元格里:只生效一次,后来者被忽略。
    /// 内核是在 `Kernel::new()` 里对根作用域调的;某个子作用域哪怕在它之前
    /// 就 fork 了,之后照样能收到通知(共享单元格,不是各自一份拷贝)。
    pub(crate) fn set_provide_hook(&self, hook: Arc<dyn Fn(&'static str) + Send + Sync>) {
        // 已经设过就忽略:内核是单例,重复设只可能是 fork 出去的上下文在捣乱
        let _ = self.inner.on_provide.set(hook);
    }

    /// 注册一个服务。同名会**覆盖**本作用域里的旧值(和 cordis 一样),
    /// 但不会影响父作用域。
    ///
    /// 覆盖**不是静默的**:命中旧值时往 stderr 打一条警告(服务名 + 作用域名)。
    /// 这里刻意只打警告、不加新的 [`Event`] 变体:事件枚举的消费方
    /// (`nfarms-gui` 的 `format_event`)是不带 `_` 分支的穷尽 match,加变体就得
    /// 连带改那个 crate,超出本文件能负责的契约;等真的要把服务冲突送进界面时,
    /// 再一次性加变体 + 分支。覆盖之后**先拿到旧 `Arc` 的消费者仍然握着旧值**,
    /// 警告里会把这点写清楚(类型恰好一致时后挂的赢,先挂的插件不会自动换新的)。
    ///
    /// 名字必须是 `&'static str`:服务表的键就是 `&'static str`
    /// (`BTreeMap<&'static str, Service>`),内核靠这一点把服务名直接记进
    /// `PluginRecord::provides`,一次分配都不用。查询侧(`get`/`require`/`has`)
    /// 收 `&str`,是为了能用运行期拼出来的名字去**查** —— 但**挂**不上去:
    /// `format!("page.{id}")` 这种动态名在编译期就被挡住。这是**有意的约束**,
    /// 不是漏掉的灵活性;哪天真需要动态名,得把键换成 `String`/`Arc<str>`,
    /// 连同内核的 `provides` 和 provide 钩子的签名一起改,别只放宽这一处。
    ///
    /// 返回 `Arc<T>`,省得调用方自己再包一层。
    pub fn provide<T: Send + Sync + 'static>(&self, name: &'static str, value: T) -> Arc<T> {
        let arc = Arc::new(value);
        // 旧值在这里没用(调用方拿的是新值),但 `insert` 会用它决定要不要警告。
        let _ = self.insert(name, arc.clone());
        arc
    }

    /// 注册一个**已经包好**的 `Arc`。
    ///
    /// 服务的消费者不止一个时用这个:比如页面注册表既要能 `get` 出来用,
    /// 又要能被别的插件持有。
    pub fn provide_arc<T: Send + Sync + 'static>(&self, name: &'static str, value: Arc<T>) {
        let _ = self.insert(name, value);
    }

    /// 写进本作用域的服务表,返回被顶掉的旧值(之前没注册过就是 `None`)。
    ///
    /// 返回值不是摆设:生产路径上它决定要不要打下面那条覆盖警告,测试里
    /// 它就是"覆盖确实被察觉到了"的证据(`overwriting_a_service_hands_back_the_old_value`)。
    fn insert(&self, name: &'static str, value: Service) -> Option<Service> {
        // 锁只包住 insert 本身:I/O 不该决定服务表的争用。
        let old = self
            .inner
            .services
            .write()
            .expect("服务表被毒化")
            .insert(name, value);
        if old.is_some() {
            eprintln!(
                "[nfarms-plugin] 服务 `{name}` 在作用域 `{}` 里被覆盖:旧值已被换掉,之前拿到旧 Arc 的消费者还握着旧值。",
                self.inner.name
            );
        }
        if let Some(hook) = self.inner.on_provide.get() {
            hook(name);
        }
        old
    }

    /// 取一个服务。**本作用域找不到就往父作用域找**。
    ///
    /// 类型不对当没找到 —— 名字对上了但类型错了,说明两个插件撞名了,
    /// 这时候 `None` 比 panic 好:调用方用 [`Context::require`] 能拿到带名字的错误。
    pub fn get<T: Send + Sync + 'static>(&self, name: &str) -> Option<Arc<T>> {
        let raw = self.lookup(name)?;
        Arc::downcast::<T>(raw).ok()
    }

    /// 同 [`Context::get`],但拿不到就报错(错误里带上服务名和作用域名)。
    pub fn require<T: Send + Sync + 'static>(&self, name: &str) -> Result<Arc<T>> {
        match self.lookup(name) {
            None => Err(PluginError::new(
                self.inner.name.clone(),
                format!("依赖的服务 `{name}` 还没有被提供"),
            )),
            Some(raw) => Arc::downcast::<T>(raw).map_err(|_| {
                PluginError::new(
                    self.inner.name.clone(),
                    format!("服务 `{name}` 的类型和取用方对不上(多半是两个插件撞名了)"),
                )
            }),
        }
    }

    fn lookup(&self, name: &str) -> Option<Service> {
        // 显式标类型:不写的话 `cur` 会被推成 `Option<&Arc<Inner>>`,
        // 而 `parent.as_deref()` 给的是 `Option<&Inner>`,两边对不上。
        let mut cur: Option<&Inner> = Some(&self.inner);
        while let Some(inner) = cur {
            if let Some(v) = inner.services.read().expect("服务表被毒化").get(name) {
                return Some(v.clone());
            }
            cur = inner.parent.as_deref();
        }
        None
    }

    /// 这条名字在当前作用域链上有没有(不看类型)。
    ///
    /// 插件加载器就是靠它判断 `inject` 满没满足的。
    pub fn has(&self, name: &str) -> bool {
        self.lookup(name).is_some()
    }

    /// 本作用域**自己**注册的服务名(不含继承来的),已排序。
    pub fn service_names(&self) -> Vec<&'static str> {
        self.inner
            .services
            .read()
            .expect("服务表被毒化")
            .keys()
            .copied()
            .collect()
    }

    /// 作用域链上所有可见的服务名,去重后排序。
    pub fn visible_service_names(&self) -> Vec<&'static str> {
        let mut out: Vec<&'static str> = Vec::new();
        // 显式标类型:不写的话 `cur` 会被推成 `Option<&Arc<Inner>>`,
        // 而 `parent.as_deref()` 给的是 `Option<&Inner>`,两边对不上。
        let mut cur: Option<&Inner> = Some(&self.inner);
        while let Some(inner) = cur {
            out.extend(inner.services.read().expect("服务表被毒化").keys().copied());
            cur = inner.parent.as_deref();
        }
        // 先全收进来再排序去重,而不是每插一个名字就 `Vec::contains` 线性扫一遍
        // (那是"作用域层数 × 服务数"的平方级)。结果和旧写法完全一致:
        // 同名服务在链上只留一个(子作用域遮蔽父的时候也只算可见的一次)。
        out.sort_unstable();
        out.dedup();
        out
    }

    /// 订阅事件,返回可以用来取消的 id。
    ///
    /// 事件先发给本作用域,再往上发给父作用域 —— 所以根作用域上的监听器
    /// 能看到所有子作用域里发生的事(界面用它来收全局日志)。
    pub fn on(&self, listener: impl Fn(&Event) + Send + Sync + 'static) -> ListenerId {
        let id = ListenerId(NEXT_LISTENER.fetch_add(1, Ordering::Relaxed));
        self.inner
            .listeners
            .write()
            .expect("监听器表被毒化")
            .push((id, Arc::new(listener)));
        id
    }

    /// 取消订阅。**沿作用域链往上找**:在根上订阅、从子作用域退订也能退掉
    /// (旧实现只翻自己那一层,拿子作用域退父作用域的订阅是静默无效的)。
    ///
    /// 返回**是否真的摘掉了一个监听器**:同一个 id 退第二次、或者传进来一个
    /// 不存在/已失效的 id,都返回 `false`。旧签名返回 `()`,调用方没任何办法
    /// 知道"这单其实没退成";现在这个 `bool` 就是那条契约,调用方可以直接
    /// `assert`/记日志,别再猜。
    pub fn off(&self, id: ListenerId) -> bool {
        // 显式标类型:不写的话 `cur` 会被推成 `Option<&Arc<Inner>>`,
        // 而 `parent.as_deref()` 给的是 `Option<&Inner>`,两边对不上。
        let mut cur: Option<&Inner> = Some(&self.inner);
        while let Some(inner) = cur {
            let removed = {
                let mut ls = inner.listeners.write().expect("监听器表被毒化");
                let before = ls.len();
                ls.retain(|(i, _)| *i != id);
                ls.len() != before
            };
            if removed {
                return true;
            }
            cur = inner.parent.as_deref();
        }
        false
    }

    /// 发一个事件。监听器是同步调用的 —— 界面线程里别在监听器里做重活。
    ///
    /// **每个监听器单独 `catch_unwind`**:一个监听器 panic 既不会吃掉同一事件
    /// 后面的监听器,也不会顺着 `emit` 传回调用方。旧写法是裸的
    /// `for l in chain { l(event) }`,一个 panic 就掀掉整条分发链,而且会一路展开出去:
    /// 插件的 `provide` → 钩子里的 `emit` → `plugin.apply` → `Kernel::resolve`。
    /// `resolve` 的 `resolving` 标志复位在循环之后、没有任何 RAII 保护,展开后不执行,
    /// 之后每次 `resolve()` 都在入口早退,所有插件永远停在 Pending ——
    /// 一个插件的坏监听器能把整个宿主的插件装载永久卡死。
    ///
    /// 所以这里**吞掉** panic(而不是分发完再 `resume_unwind`):分发链和内核的
    /// 状态机都得活着。代价是 panic 不再向上暴露,因此必须留痕 —— 消息打到 stderr
    /// (`Kernel::resolve` 那侧的 RAII guard 是配套的更深一层修法,不在本文件)。
    pub fn emit(&self, event: &Event) {
        // 显式标类型:不写的话 `cur` 会被推成 `Option<&Arc<Inner>>`,
        // 而 `parent.as_deref()` 给的是 `Option<&Inner>`,两边对不上。
        let mut cur: Option<&Inner> = Some(&self.inner);
        // 先收集再调用:监听器里再 emit(比如插件装载时 provide 又触发
        // ServiceProvided)不会撞上正在读的锁。
        let mut chain: Vec<Listener> = Vec::new();
        while let Some(inner) = cur {
            for (_, l) in inner.listeners.read().expect("监听器表被毒化").iter() {
                chain.push(l.clone());
            }
            cur = inner.parent.as_deref();
        }
        for l in chain {
            // AssertUnwindSafe:监听器可以随手抓自己那点状态(插件作者写的东西
            // 不该由我们来要求 UnwindSafe),隔离的是"panic 别扩散",不是数据竞争。
            if let Err(payload) = catch_unwind(AssertUnwindSafe(|| l(event))) {
                eprintln!(
                    "[nfarms-plugin] 事件监听器 panic,已跳过它并继续分发:{}(事件:{event:?})",
                    panic_message(payload.as_ref())
                );
            }
        }
    }
}

/// 从 `catch_unwind` 的载荷里抠出一句给人看的说明。
///
/// `panic!` 的载荷实际上只有 `&'static str` 和 `String` 两种常见形式,其余
/// (比如 `panic_any`)给个占位说明 —— 这个函数自己绝不能 panic,
/// 否则就在错误处理路径上制造第二个 panic。
fn panic_message(payload: &(dyn Any + Send)) -> String {
    if let Some(s) = payload.downcast_ref::<&'static str>() {
        (*s).to_string()
    } else if let Some(s) = payload.downcast_ref::<String>() {
        s.clone()
    } else {
        "<非字符串 panic 载荷>".to_string()
    }
}

/// 一层作用域里的事件监听链长度。**只给单元测试用**:它自己也挂在
/// `#[cfg(test)]` 下,生产构建里这个函数根本不存在,所以别把它当成"自检入口"
/// 写进文档(旧注释就是这么写的,和 `#[cfg(test)]` 自相矛盾)。
#[cfg(test)]
pub(crate) fn listener_count(ctx: &Context) -> usize {
    ctx.inner.listeners.read().expect("监听器表被毒化").len()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn provide_and_get_roundtrip() {
        let ctx = Context::root("test");
        assert!(ctx.get::<u32>("n").is_none());
        ctx.provide("n", 42u32);
        assert_eq!(*ctx.get::<u32>("n").unwrap(), 42);
        assert!(ctx.has("n"));
        // 类型对不上就是 None,不 panic
        assert!(ctx.get::<String>("n").is_none());
        assert!(ctx.require::<String>("n").is_err());
    }

    #[test]
    fn child_sees_parent_but_not_the_other_way_round() {
        let root = Context::root("root");
        root.provide("shared", 1u8);
        let child = root.fork("child");
        child.provide("own", 2u8);

        assert!(child.get::<u8>("shared").is_some(), "子作用域要能看见父的");
        assert!(root.get::<u8>("own").is_none(), "父作用域不该看见子的");
        assert_eq!(child.visible_service_names(), vec!["own", "shared"]);
        assert_eq!(root.service_names(), vec!["shared"]);
    }

    #[test]
    fn same_name_in_child_shadows_parent() {
        let root = Context::root("root");
        root.provide("v", 1u32);
        let child = root.fork("child");
        child.provide("v", 9u32);
        assert_eq!(*root.get::<u32>("v").unwrap(), 1);
        assert_eq!(*child.get::<u32>("v").unwrap(), 9);
    }

    #[test]
    fn events_bubble_up_and_off_works() {
        let root = Context::root("root");
        let hits = Arc::new(std::sync::Mutex::new(Vec::new()));

        let h = hits.clone();
        let id = root.on(move |e| {
            if let Event::Log { message, .. } = e {
                h.lock().unwrap().push(message.clone());
            }
        });

        let child = root.fork("child");
        child.emit(&Event::Log {
            source: "t".into(),
            message: "来自子作用域".into(),
        });
        assert_eq!(hits.lock().unwrap().len(), 1, "子作用域的事件要冒泡到父");

        // `off` 现在返回"有没有真退掉":这里顺手把这条契约用起来,
        // 免得它变成一个没人看的返回值。
        assert!(root.off(id), "在订阅的同一层退订必须成功");
        child.emit(&Event::Log {
            source: "t".into(),
            message: "再来一次".into(),
        });
        assert_eq!(hits.lock().unwrap().len(), 1, "退订之后不该再收到");
    }

    #[test]
    fn listener_emitting_again_does_not_deadlock() {
        // 监听器里再 emit 是常见写法(比如「收到 A 就顺手记一条日志」)。
        // 如果事件分发是持锁遍历,这里会直接死锁。
        let root = Context::root("root");
        let deep = Context::root("deep");
        deep.on(|e| {
            if let Event::ServiceProvided { .. } = e {
                // 什么都不做,只要能在回调里安全地碰 Context 就行
            }
        });
        let counter = Arc::new(AtomicU64::new(0));
        let c = counter.clone();
        root.on(move |_| {
            c.fetch_add(1, Ordering::Relaxed);
        });
        root.emit(&Event::SettingsSaved {
            path: "x".into(),
        });
        root.emit(&Event::SettingsSaved {
            path: "x".into(),
        });
        assert_eq!(counter.load(Ordering::Relaxed), 2);
        assert_eq!(listener_count(&deep), 1);
    }

    /// 覆盖同名服务**不能再不留痕**:`insert` 必须把旧值交出来,生产路径上
    /// 同一个分支会往 stderr 打警告。旧实现把 `insert` 的返回值直接丢掉,
    /// 谁也说不清"这个服务是被谁顶掉的"。
    #[test]
    fn overwriting_a_service_hands_back_the_old_value() {
        let ctx = Context::root("test");
        let first: Arc<u32> = ctx.provide("svc", 1u32);

        let old = ctx
            .insert("svc", Arc::new(2u32))
            .expect("同名覆盖要把旧值交出来");
        // 旧值是以 `Service`(`Arc<dyn Any + Send + Sync>`)交回来的,取内容得 downcast
        assert_eq!(old.downcast_ref::<u32>(), Some(&1), "交回的是原来那个值");
        let first_as_service: Service = first.clone();
        assert!(
            Arc::ptr_eq(&old, &first_as_service),
            "而且必须是消费者手里那份同一个 Arc,不是拷贝"
        );
        assert!(
            ctx.insert("brand_new", Arc::new(3u32)).is_none(),
            "没有旧值的时候不能给假信号"
        );
        assert_eq!(*ctx.get::<u32>("svc").unwrap(), 2, "表里换成了新值");
        assert_eq!(*first, 1, "早就拿到旧 Arc 的消费者还握着旧值");
    }

    /// 钩子是**共享单元格**:内核在 `Kernel::new` 里 set,而任何在它之前
    /// fork 出来的子作用域也必须收到通知。旧实现 clone 裸 `OnceLock`
    /// (空 clone 还是空),这些子作用域 provide 的服务永远广播不出去,
    /// 等它的插件就永远停在 Pending。
    #[test]
    fn provide_hook_is_shared_with_forks_made_before_it() {
        let root = Context::root("root");
        let child = root.fork("child"); // 注意顺序:钩子还没设

        let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
        let s = seen.clone();
        root.set_provide_hook(Arc::new(move |name: &'static str| {
            s.lock().unwrap().push(name);
        }));

        child.provide("late", 1u8);
        assert_eq!(
            *seen.lock().unwrap(),
            vec!["late"],
            "先 fork 后 set 也要收到:父子共享同一个 OnceLock"
        );

        // 反方向同样成立:钩子从子作用域设进去,父作用域 provide 也得通知。
        // 这正是"共享"和"各一份拷贝"的区别。
        let root2 = Context::root("root2");
        let child2 = root2.fork("child2");
        let seen2 = Arc::new(std::sync::Mutex::new(Vec::new()));
        let s2 = seen2.clone();
        child2.set_provide_hook(Arc::new(move |name: &'static str| {
            s2.lock().unwrap().push(name);
        }));

        root2.provide("up", 2u8);
        assert_eq!(*seen2.lock().unwrap(), vec!["up"]);
    }

    /// 一个监听器 panic 不能吃掉后面的监听器,也不能顺着 `emit` 传回调用方
    /// (调用方是内核的 provide 钩子,展开出去会让 `resolving` 永久卡住)。
    #[test]
    fn one_panicking_listener_does_not_stop_the_others() {
        let root = Context::root("root");
        let counter = Arc::new(AtomicU64::new(0));

        root.on(|_| panic!("监听器炸了"));
        let c = counter.clone();
        root.on(move |_| {
            c.fetch_add(1, Ordering::Relaxed);
        });

        let child = root.fork("child");
        let c2 = counter.clone();
        child.on(move |_| {
            c2.fetch_add(1, Ordering::Relaxed);
        });

        // 分发顺序是"自己的监听器在前,父作用域的在后":
        // 子监听器 → 根的 panic 监听器 → 根的正常监听器。
        child.emit(&Event::SettingsSaved { path: "x".into() });
        assert_eq!(
            counter.load(Ordering::Relaxed),
            2,
            "panic 后面的监听器(而且是在父作用域上的)必须照常收到"
        );

        // 若 panic 还在往外传,这一行根本执行不到,测试会直接炸掉。
        root.emit(&Event::SettingsSaved { path: "x".into() });
        assert_eq!(
            counter.load(Ordering::Relaxed),
            3,
            "panic 不能顺着 emit 传回调用方"
        );
    }

    /// `off` 要沿作用域链往上找,并且如实报告有没有退掉。
    #[test]
    fn off_reaches_the_scope_that_registered_the_listener() {
        let root = Context::root("root");
        let child = root.fork("child");
        let hits = Arc::new(AtomicU64::new(0));
        let h = hits.clone();
        // 订阅在根上
        let id = root.on(move |_| {
            h.fetch_add(1, Ordering::Relaxed);
        });

        assert!(child.off(id), "从子作用域要能退掉父作用域上的订阅");
        assert!(!child.off(id), "同一个 id 退第二次要如实返回 false");
        assert!(
            !child.off(ListenerId(u64::MAX)),
            "不存在的 id 要返回 false,不能假装成功"
        );

        child.emit(&Event::SettingsSaved { path: "x".into() });
        assert_eq!(hits.load(Ordering::Relaxed), 0, "退订之后不该再收到");
    }

    /// 同名服务跨作用域出现时,`visible_service_names` 只能算一次。
    #[test]
    fn visible_service_names_dedups_names_from_several_scopes() {
        let root = Context::root("root");
        root.provide("v", 1u32);
        let mid = root.fork("mid");
        mid.provide("v", 2u32); // 子作用域遮蔽父的同名服务
        let leaf = mid.fork("leaf");
        leaf.provide("own", 3u8);

        assert_eq!(
            leaf.visible_service_names(),
            vec!["own", "v"],
            "链上同名的只算一次(先收集再排序去重,结果和旧的 contains 写法一致)"
        );
        assert_eq!(mid.service_names(), vec!["v"], "本作用域自己的表不受影响");
    }
}