跳转至

crates\nfarms-plugin\src\kernel.rs

源码路径:crates\nfarms-plugin\src\kernel.rs · 906 行

//! 插件装载器:依赖解析、生命周期、快照。

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};

use crate::context::{Context, Event};
use crate::plugin::{Plugin, PluginError, Result};

/// 插件当前处于哪个阶段。
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PluginState {
    /// 还在等 `inject` 里列的服务 —— 不是错误,只是时机没到
    Pending,
    /// 正在 `apply`(只在装载过程中短暂出现,外面看不到)
    Loading,
    /// 装载完成
    Active,
    /// `apply` 返回了错误,或者 `apply` 里 panic 了。
    ///
    /// **终态,内核不会重试**:`pick_and_mark_loading` 只挑 `Pending`,
    /// 也没有对外的重试入口,想再装一次只能新起一个 `Kernel`。
    /// 这是写明的取舍而不是疏漏 —— 插件失败几乎都是「依赖的服务根本不存在」,
    /// 自动重试只会把同一条错误反复刷进日志;真要做界面上的「重试」按钮,
    /// 得先在这里加一个 `Kernel::retry(name)` 之类的入口。
    Failed(String),
    /// 已经 `dispose`(再 `dispose` 不会回调第二次)
    Disposed,
}

impl PluginState {
    pub fn label(&self) -> &'static str {
        match self {
            PluginState::Pending => "等待依赖",
            PluginState::Loading => "装载中",
            PluginState::Active => "已装载",
            PluginState::Failed(_) => "装载失败",
            PluginState::Disposed => "已卸载",
        }
    }

    pub fn is_active(&self) -> bool {
        matches!(self, PluginState::Active)
    }
}

/// 给界面看的一份插件快照 —— 故意全是拥有所有权的值,
/// 免得 UI 拿着锁去画。
#[derive(Clone, Debug)]
pub struct PluginRecord {
    pub name: String,
    pub version: String,
    pub state: PluginState,
    pub inject: Vec<&'static str>,
    /// 这个插件在 `apply` 里 provide 出来的服务
    pub provides: Vec<&'static str>,
}

struct Record {
    plugin: Arc<dyn Plugin>,
    state: PluginState,
    provides: Vec<&'static str>,
}

#[derive(Default)]
struct State {
    records: Vec<Record>,
    /// 正在 `apply` 的是第几个 —— provide 回调靠它把服务算到正确的插件头上
    applying: Option<usize>,
}

struct Inner {
    ctx: Context,
    /// records 和 applying 放一把锁里:provide 回调要在 apply 中途改 records,
    /// 分成两把锁就得小心加锁顺序,这里直接绕开。
    state: Mutex<State>,
    /// 防重入。插件在 `apply` 里 provide 会触发 `resolve`,
    /// 不拦一下就是无限递归。
    resolving: AtomicBool,
}

/// `resolve` 的收尾守卫。
///
/// 为什么不在函数末尾直接 `store(false)`:插件 `apply` 里 panic 会 unwind
/// 出去,末尾那行永远执行不到,`resolving` 就永久停在 `true` —— 之后每次
/// `provide` 触发的 `resolve` 都在开头直接返回,进程还活着,插件却再也不
/// 自动装载。守卫在正常返回和 unwind 两条路上都会跑,复位才有保证。
struct ResolvingGuard<'a>(&'a AtomicBool);

impl Drop for ResolvingGuard<'_> {
    fn drop(&mut self) {
        self.0.store(false, Ordering::SeqCst);
    }
}

/// 把 panic 的载荷转成一行给人看的文字。
///
/// `panic!("字面量")` 的载荷是 `&'static str`,`panic!("{}", x)` 的是 `String`,
/// 别的类型(比如 `panic_any`)只能给个兜底说法 —— 这里只用于界面/事件,
/// 不值得为它把崩溃本身弄复杂。
fn panic_text(payload: &(dyn std::any::Any + Send)) -> String {
    if let Some(s) = payload.downcast_ref::<&str>() {
        (*s).to_string()
    } else if let Some(s) = payload.downcast_ref::<String>() {
        s.clone()
    } else {
        "插件 apply panic(载荷不是字符串)".to_string()
    }
}

/// 插件内核。
///
/// `Kernel` 是句柄(内部一个 `Arc`),clone 出来的是同一个内核。
#[derive(Clone)]
pub struct Kernel {
    inner: Arc<Inner>,
}

impl std::fmt::Debug for Kernel {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let st = self.inner.state.lock().expect("插件表被毒化");
        f.debug_struct("Kernel")
            .field("plugins", &st.records.len())
            .field(
                "active",
                &st.records.iter().filter(|r| r.state.is_active()).count(),
            )
            .finish()
    }
}

impl Default for Kernel {
    fn default() -> Self {
        Self::new()
    }
}

impl Kernel {
    pub fn new() -> Self {
        let ctx = Context::root("nfarms");
        let inner = Arc::new(Inner {
            ctx,
            state: Mutex::new(State::default()),
            resolving: AtomicBool::new(false),
        });
        let kernel = Self { inner };

        // 服务一被 provide 就:记到当前装卸的插件头上 → 广播 → 重试等待中的插件。
        // 用 Weak 而不是 Arc:回调装在 Context 里,Context 又在内核里,
        // 强引用就直接循环泄漏了。
        let weak = Arc::downgrade(&kernel.inner);
        kernel
            .inner
            .ctx
            .set_provide_hook(Arc::new(move |name: &'static str| {
            let Some(inner) = weak.upgrade() else {
                return;
            };
            // 注意别写成 `if let Some(idx) = inner.state.lock()….applying { … }`:
            // `if let` 的条件里产生的临时值会活到整个块结束,块里再加锁就是自死锁。
            // 先把值拷出来,锁立刻释放。
            let applying = inner.state.lock().expect("插件表被毒化").applying;
            if let Some(idx) = applying {
                let mut st = inner.state.lock().expect("插件表被毒化");
                if let Some(r) = st.records.get_mut(idx)
                    && !r.provides.contains(&name)
                {
                    r.provides.push(name);
                }
            } else {
                // 归属未知:不是在某个插件的 `apply` 里 provide 的 —— 宿主自己
                // 提供(`Kernel::provide`),或者插件在后台线程 / `apply` 之外
                // provide。这种服务不会记进任何 `records[].provides`,插件页于是
                // 对不上账;以前这个分支是空的,静默得没人知道,所以发一条日志
                // 把它说破。(注意这里不能持锁 emit:监听器可能回头再碰内核。)
                inner.ctx.emit(&Event::Log {
                    source: "kernel".to_string(),
                    message: format!(
                        "服务 `{name}` 在插件 apply 之外被提供,不记入任何插件的 provides"
                    ),
                });
            }
            inner.ctx.emit(&Event::ServiceProvided {
                name: name.to_string(),
            });
            Kernel { inner: inner.clone() }.resolve();
        }));

        kernel
    }

    pub fn context(&self) -> &Context {
        &self.inner.ctx
    }

    /// 宿主直接提供的服务(不挂在任何插件名下)。
    pub fn provide<T: Send + Sync + 'static>(&self, name: &'static str, value: T) -> Arc<T> {
        self.inner.ctx.provide(name, value)
    }

    pub fn provide_arc<T: Send + Sync + 'static>(&self, name: &'static str, value: Arc<T>) {
        self.inner.ctx.provide_arc(name, value);
    }

    /// 注册一个插件。同名插件只装第一个(重复注册通常意味着构建脚本写了两遍)。
    ///
    /// 被忽略的那个会发一条 [`Event::Log`]:以前这里彻底静默,注册方以为装上了,
    /// 界面上既不报错也不提示,只能靠数插件个数发现少了一个。
    /// 发 Log 而不是 `PluginFailed` —— 装上的那个是好的,失败的只是"后一个没被装"。
    pub fn load(&self, plugin: impl Plugin) -> &Self {
        self.add(Arc::new(plugin))
    }

    /// 一次注册一批。
    pub fn load_all<I>(&self, plugins: I) -> &Self
    where
        I: IntoIterator,
        I::Item: Plugin,
    {
        for p in plugins {
            self.add(Arc::new(p));
        }
        self
    }

    fn add(&self, plugin: Arc<dyn Plugin>) -> &Self {
        let name = plugin.name().to_string();
        let duplicate = {
            let mut st = self.inner.state.lock().expect("插件表被毒化");
            if st.records.iter().any(|r| r.plugin.name() == name) {
                true
            } else {
                st.records.push(Record {
                    plugin,
                    state: PluginState::Pending,
                    provides: Vec::new(),
                });
                false
            }
        };
        if duplicate {
            // 先把锁放掉再 emit:监听器里回头调内核(很常见)会撞锁。
            self.inner.ctx.emit(&Event::Log {
                source: "kernel".to_string(),
                message: format!("插件 `{name}` 重名,后注册的这个已忽略(同名只装第一个)"),
            });
            return self;
        }
        self.resolve();
        self
    }

    /// 把所有依赖已经满足的插件装上。
    ///
    /// 循环而不是只扫一遍:装上的插件会 provide 新服务,新服务可能又让
    /// 后面某个插件满足条件 —— 一遍扫完就漏了。
    fn resolve(&self) {
        if self.inner.resolving.swap(true, Ordering::SeqCst) {
            // 外层已经在跑了,那一轮会把我们刚 provide 的服务一并考虑进去
            return;
        }
        // 守住 resolving 的复位 —— panic 从 apply 里 unwind 出去时,
        // 函数末尾那句 store(false) 是跑不到的(见 ResolvingGuard 的注释)。
        let _resolving_guard = ResolvingGuard(&self.inner.resolving);
        loop {
            let picked = self.pick_and_mark_loading();
            let Some(idx) = picked else { break };

            let plugin = {
                let st = self.inner.state.lock().expect("插件表被毒化");
                st.records[idx].plugin.clone()
            };
            let name = plugin.name().to_string();
            // 包一层 catch_unwind 不是为了把 panic 吞掉(紧接着就 resume_unwind
            // 原样抛回去,崩溃照旧可见),而是为了在 unwind 之前把记录从 Loading
            // 收拾成 Failed。不收拾的话这条记录会永久卡在 Loading:`pick` 只挑
            // Pending,而外面把 Loading 显示成 Pending,于是界面写着「等待依赖」、
            // `ready()` 报「缺 []」,查不出所以然。
            // 这里的锁都在 apply 之外,所以 unwind 不会毒化插件表。
            let outcome = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
                plugin.apply(&self.inner.ctx)
            })) {
                Ok(outcome) => outcome,
                Err(payload) => {
                    let message = panic_text(payload.as_ref());
                    {
                        let mut st = self.inner.state.lock().expect("插件表被毒化");
                        st.applying = None;
                        st.records[idx].state = PluginState::Failed(message.clone());
                    }
                    self.inner.ctx.emit(&Event::PluginFailed {
                        name: name.clone(),
                        error: message,
                    });
                    std::panic::resume_unwind(payload);
                }
            };

            {
                let mut st = self.inner.state.lock().expect("插件表被毒化");
                st.applying = None;
                st.records[idx].state = match &outcome {
                    Ok(()) => PluginState::Active,
                    Err(e) => PluginState::Failed(e.message.clone()),
                };
            }
            match &outcome {
                Ok(()) => self.inner.ctx.emit(&Event::PluginLoaded { name }),
                Err(e) => self.inner.ctx.emit(&Event::PluginFailed {
                    name,
                    error: e.message.clone(),
                }),
            }
        }
        // 函数在这里返回,_resolving_guard 的 Drop 复位 resolving。
    }

    /// 找出第一个「依赖齐全且在等待」的插件,把它标成 Loading。
    fn pick_and_mark_loading(&self) -> Option<usize> {
        let mut st = self.inner.state.lock().expect("插件表被毒化");
        let idx = st.records.iter().position(|r| {
            r.state == PluginState::Pending
                && r.plugin
                    .inject()
                    .iter()
                    .all(|dep| self.inner.ctx.has(dep))
        })?;
        st.records[idx].state = PluginState::Loading;
        st.applying = Some(idx);
        Some(idx)
    }

    /// 装载到此为止。还有插件在等依赖就报错,并**逐个列出缺哪个服务** ——
    /// 「有插件没装上」这种信息,不写清楚等于没写。
    pub fn ready(&self) -> Result<()> {
        self.resolve();
        let st = self.inner.state.lock().expect("插件表被毒化");
        let mut lines = Vec::new();
        for r in st.records.iter() {
            if r.state != PluginState::Pending {
                continue;
            }
            let missing: Vec<&str> = r
                .plugin
                .inject()
                .iter()
                .copied()
                .filter(|d| !self.inner.ctx.has(d))
                .collect();
            lines.push(format!("{} 缺 {:?}", r.plugin.name(), missing));
        }
        if lines.is_empty() {
            return Ok(());
        }
        Err(PluginError::new(
            "kernel",
            format!("{} 个插件在等依赖:{}", lines.len(), lines.join(";")),
        ))
    }

    /// 反向卸载(后装的先卸)。和 cordis 一样:依赖别人的必须最后走。
    ///
    /// 幂等:已经 `Disposed` 的插件不会再回调第二次。
    ///
    /// **退出语义**(这条以前是悬着的):`Kernel` 是句柄,最后一个句柄被 drop
    /// 时会自动走一遍同样的清理(见 [`Inner`] 的 `Drop`)。生产代码
    /// (`nfarms-gui`)只把 `Kernel` 存在 `App` 里,并不显式调这个函数 ——
    /// 窗口关闭、`App` 被 drop 时它才真的跑到。所以插件不要依赖 `dispose`
    /// 做「必须发生」的收尾:`std::process::exit` 不会 unwind,线程正 panic
    /// 期间的 drop 也会被跳过(见 `Inner` 的 `Drop` 注释)。
    pub fn dispose(&self) {
        dispose_inner(&self.inner);
    }

    /// 全部插件快照(顺序 = 注册顺序)。
    pub fn records(&self) -> Vec<PluginRecord> {
        let st = self.inner.state.lock().expect("插件表被毒化");
        st.records
            .iter()
            .map(|r| {
                let mut provides = r.provides.clone();
                provides.sort_unstable();
                provides.dedup();
                PluginRecord {
                    name: r.plugin.name().to_string(),
                    version: r.plugin.version().to_string(),
                    state: match &r.state {
                        // Loading 是内部瞬时状态,外面看到的一律算 Pending
                        PluginState::Loading => PluginState::Pending,
                        s => s.clone(),
                    },
                    inject: r.plugin.inject().to_vec(),
                    provides,
                }
            })
            .collect()
    }

    pub fn len(&self) -> usize {
        self.inner.state.lock().expect("插件表被毒化").records.len()
    }

    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    pub fn active_count(&self) -> usize {
        self.inner
            .state
            .lock()
            .expect("插件表被毒化")
            .records
            .iter()
            .filter(|r| r.state.is_active())
            .count()
    }

    /// 找某个插件当前的状态。
    pub fn state_of(&self, name: &str) -> Option<PluginState> {
        self.inner
            .state
            .lock()
            .expect("插件表被毒化")
            .records
            .iter()
            .find(|r| r.plugin.name() == name)
            .map(|r| match &r.state {
                PluginState::Loading => PluginState::Pending,
                s => s.clone(),
            })
    }
}

/// 卸载的实际实现([`Kernel::dispose`] 和 `Inner` 的 `Drop` 共用一份)。
///
/// 分两趟而不是一趟:
/// 1. 先把**所有**还没 `Disposed` 的记录(含 `Failed` / `Pending`)改成
///    `Disposed` —— 以前只有 `Active` 会被回调,`apply` 执行到一半失败的插件
///    就永远没机会释放自己已经占下的资源;
/// 2. 再按注册的逆序回调 `dispose`。
///
/// 先统一改状态是必须的:第二趟里插件可能 `provide` 出新服务,那会经 provide
/// 回调触发 `resolve`,而 `resolve` 只挑 `Pending` —— 不先改状态的话,一次
/// 「卸载」会把还挂着的插件现场装起来,越卸越多。
fn dispose_inner(inner: &Inner) {
    let plugins: Vec<Arc<dyn Plugin>> = {
        let mut st = inner.state.lock().expect("插件表被毒化");
        let mut out = Vec::with_capacity(st.records.len());
        for r in st.records.iter_mut() {
            if r.state == PluginState::Disposed {
                continue;
            }
            r.state = PluginState::Disposed;
            out.push(r.plugin.clone());
        }
        out
    };
    for plugin in plugins.into_iter().rev() {
        let name = plugin.name().to_string();
        // dispose 里报错不应该打断其它插件的清理 —— 记进事件就够了
        match plugin.dispose(&inner.ctx) {
            Ok(()) => inner.ctx.emit(&Event::PluginDisposed { name }),
            Err(e) => inner.ctx.emit(&Event::PluginFailed {
                name,
                error: format!("卸载时出错:{e}"),
            }),
        }
    }
}

/// 最后一个句柄消失时自动卸载。
///
/// 为什么挂在 `Inner` 而不是 `Kernel` 上:`Kernel` 是 `Clone` 的句柄,谁都会
/// 拿临时 clone(比如 provide 回调里那一下);给 `Kernel` 写 `Drop` 会让临时
/// clone 一消失就卸载整个内核。`Inner` 只在强引用归零时 drop,那一刻才是
/// 「这个内核真的没人要了」。
///
/// 有这个 `Drop`,插件的清理才不是"没着落"的 API:生产代码把 `Kernel` 存进
/// `App`,窗口关闭、`App` 被 drop 时清理自动发生。反过来说,`process::exit`
/// 这类不 unwind 的退场方式仍然不会跑到它 —— 所以别把关键落盘寄托在 `dispose`。
impl Drop for Inner {
    fn drop(&mut self) {
        // 正在 panic 时不再跑插件代码:drop 里再 panic 会直接 abort,
        // 把一次本可以 unwind 的崩溃变成进程硬杀。
        if std::thread::panicking() {
            return;
        }
        dispose_inner(self);
    }
}

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

    struct Provider {
        name: &'static str,
        service: &'static str,
    }
    impl Plugin for Provider {
        fn name(&self) -> &str {
            self.name
        }
        fn apply(&self, ctx: &Context) -> Result<()> {
            ctx.provide(self.service, format!("来自 {}", self.name));
            Ok(())
        }
    }

    struct Consumer {
        name: &'static str,
        needs: &'static [&'static str],
        seen: Arc<Mutex<Vec<String>>>,
    }
    impl Plugin for Consumer {
        fn name(&self) -> &str {
            self.name
        }
        fn inject(&self) -> &[&'static str] {
            self.needs
        }
        fn apply(&self, ctx: &Context) -> Result<()> {
            for dep in self.needs {
                // require 给的是 Arc<String>,要的是里面的字符串本身
                let v = ctx.require::<String>(dep)?;
                self.seen.lock().unwrap().push((*v).clone());
            }
            Ok(())
        }
    }

    #[test]
    fn consumer_waits_until_provider_shows_up() {
        let kernel = Kernel::new();
        let seen = Arc::new(Mutex::new(Vec::new()));

        // 故意**先**装消费者:它依赖的服务还不存在
        kernel.load(Consumer {
            name: "consumer",
            needs: &["greeting"],
            seen: seen.clone(),
        });
        assert_eq!(
            kernel.state_of("consumer"),
            Some(PluginState::Pending),
            "依赖没满足时应该挂起,而不是报错"
        );
        assert!(seen.lock().unwrap().is_empty());
        assert!(kernel.ready().is_err(), "还有插件没装上,ready 要报出来");

        // 后装的插件把服务提供出来 —— 前面挂着的那个应该自动装上
        kernel.load(Provider {
            name: "provider",
            service: "greeting",
        });
        assert_eq!(kernel.state_of("consumer"), Some(PluginState::Active));
        assert_eq!(kernel.state_of("provider"), Some(PluginState::Active));
        assert_eq!(seen.lock().unwrap().as_slice(), ["来自 provider"]);
        kernel.ready().expect("依赖满足后 ready 应该过");
    }

    #[test]
    fn ready_lists_the_missing_service() {
        let kernel = Kernel::new();
        kernel.load(Consumer {
            name: "consumer",
            needs: &["nobody-provides-this"],
            seen: Arc::new(Mutex::new(Vec::new())),
        });
        let err = kernel.ready().unwrap_err();
        assert!(
            err.message.contains("nobody-provides-this"),
            "错误里必须点名缺的是哪个服务,实际是:{}",
            err.message
        );
        assert_eq!(kernel.active_count(), 0);
    }

    #[test]
    fn provides_are_attributed_to_the_right_plugin() {
        let kernel = Kernel::new();
        kernel.provide("host-service", 1u8); // 宿主直接给的,不该算到插件头上
        kernel.load(Provider {
            name: "p1",
            service: "svc-1",
        });
        kernel.load(Provider {
            name: "p2",
            service: "svc-2",
        });

        let recs = kernel.records();
        assert_eq!(recs.len(), 2);
        assert_eq!(recs[0].provides, vec!["svc-1"]);
        assert_eq!(recs[1].provides, vec!["svc-2"]);
        assert_eq!(recs[0].state, PluginState::Active);
        assert!(!recs[0].version.is_empty());
    }

    #[test]
    fn dispose_runs_in_reverse_load_order() {
        let log = Arc::new(Mutex::new(Vec::new()));

        struct Tracked {
            name: &'static str,
            log: Arc<Mutex<Vec<String>>>,
        }
        impl Plugin for Tracked {
            fn name(&self) -> &str {
                self.name
            }
            fn apply(&self, _ctx: &Context) -> Result<()> {
                self.log.lock().unwrap().push(format!("load {}", self.name));
                Ok(())
            }
            fn dispose(&self, _ctx: &Context) -> Result<()> {
                self.log.lock().unwrap().push(format!("drop {}", self.name));
                Ok(())
            }
        }

        let kernel = Kernel::new();
        for n in ["a", "b", "c"] {
            kernel.load(Tracked {
                name: n,
                log: log.clone(),
            });
        }
        kernel.dispose();
        assert_eq!(
            log.lock().unwrap().as_slice(),
            ["load a", "load b", "load c", "drop c", "drop b", "drop a"],
            "卸载顺序必须是装载顺序的反向 —— 依赖别人的要先走"
        );
        assert_eq!(kernel.active_count(), 0);
    }

    #[test]
    fn failed_plugin_is_recorded_and_does_not_block_others() {
        struct Bad;
        impl Plugin for Bad {
            fn name(&self) -> &str {
                "bad"
            }
            fn apply(&self, _ctx: &Context) -> Result<()> {
                Err(PluginError::new("bad", "故意失败"))
            }
        }

        let kernel = Kernel::new();
        kernel.load(Bad);
        kernel.load(Provider {
            name: "good",
            service: "ok",
        });
        assert_eq!(
            kernel.state_of("bad"),
            Some(PluginState::Failed("故意失败".into()))
        );
        assert_eq!(
            kernel.state_of("good"),
            Some(PluginState::Active),
            "一个插件挂掉不该拖住后面的"
        );
        // 失败不算「还在等」,ready 只关心挂起的
        kernel.ready().expect("失败不是 pending");
    }

    #[test]
    fn loading_the_same_name_twice_is_ignored() {
        let kernel = Kernel::new();
        kernel.load(Provider {
            name: "dup",
            service: "s1",
        });
        kernel.load(Provider {
            name: "dup",
            service: "s2",
        });
        assert_eq!(kernel.len(), 1);
        assert_eq!(kernel.records()[0].provides, vec!["s1"]);
    }

    #[test]
    fn plugins_see_events_emitted_by_the_kernel() {
        let kernel = Kernel::new();
        let hits = Arc::new(Mutex::new(Vec::new()));
        let h = hits.clone();
        kernel.context().on(move |e| {
            if let Event::PluginLoaded { name } = e {
                h.lock().unwrap().push(name.clone());
            }
        });
        kernel.load(Provider {
            name: "watched",
            service: "x",
        });
        assert_eq!(hits.lock().unwrap().as_slice(), ["watched"]);
    }

    /// 记录 apply / dispose 顺序的插件,卸载相关的测试共用。
    struct Lifecycle {
        name: &'static str,
        log: Arc<Mutex<Vec<String>>>,
    }
    impl Plugin for Lifecycle {
        fn name(&self) -> &str {
            self.name
        }
        fn apply(&self, _ctx: &Context) -> Result<()> {
            self.log.lock().unwrap().push(format!("load {}", self.name));
            Ok(())
        }
        fn dispose(&self, _ctx: &Context) -> Result<()> {
            self.log.lock().unwrap().push(format!("drop {}", self.name));
            Ok(())
        }
    }

    /// 收集内核发出的 `Event::Log` 行 —— 「以前静默」的那几条修复靠它断言。
    fn log_sink(kernel: &Kernel) -> Arc<Mutex<Vec<String>>> {
        let lines = Arc::new(Mutex::new(Vec::new()));
        let sink = lines.clone();
        kernel.context().on(move |e| {
            if let Event::Log { message, .. } = e {
                sink.lock().unwrap().push(message.clone());
            }
        });
        lines
    }

    #[test]
    fn dropping_the_last_handle_disposes_plugins() {
        let log = Arc::new(Mutex::new(Vec::new()));
        let handle = {
            let kernel = Kernel::new();
            kernel.load(Lifecycle {
                name: "x",
                log: log.clone(),
            });
            kernel.clone()
        };
        assert_eq!(
            log.lock().unwrap().as_slice(),
            ["load x"],
            "还有别的句柄活着时不能提前卸载"
        );

        drop(handle);
        assert_eq!(
            log.lock().unwrap().as_slice(),
            ["load x", "drop x"],
            "最后一个句柄消失 = 内核没人要了,清理由 Drop 负责 —— \
             少了它,生产代码里就没人触发 dispose"
        );
    }

    #[test]
    fn failed_plugin_is_still_disposed() {
        struct Bad {
            log: Arc<Mutex<Vec<String>>>,
        }
        impl Plugin for Bad {
            fn name(&self) -> &str {
                "bad"
            }
            fn apply(&self, _ctx: &Context) -> Result<()> {
                Err(PluginError::new("bad", "故意失败"))
            }
            fn dispose(&self, _ctx: &Context) -> Result<()> {
                self.log.lock().unwrap().push("drop bad".into());
                Ok(())
            }
        }

        let log = Arc::new(Mutex::new(Vec::new()));
        let kernel = Kernel::new();
        kernel.load(Bad { log: log.clone() });
        assert!(matches!(
            kernel.state_of("bad"),
            Some(PluginState::Failed(_))
        ));

        kernel.dispose();
        assert_eq!(
            log.lock().unwrap().as_slice(),
            ["drop bad"],
            "apply 失败的插件也必须拿到一次 dispose,好释放已经占下的资源"
        );

        // 幂等:再卸一次不能重复回调
        kernel.dispose();
        assert_eq!(log.lock().unwrap().len(), 1, "dispose 必须幂等");
    }

    #[test]
    fn panic_in_apply_does_not_wedge_the_kernel() {
        struct Boom;
        impl Plugin for Boom {
            fn name(&self) -> &str {
                "boom"
            }
            fn apply(&self, _ctx: &Context) -> Result<()> {
                panic!("apply 崩了");
            }
        }

        let kernel = Kernel::new();
        // 触发 apply panic 得在别的线程里 —— 主线程上它会直接把这个测试打死
        let writer = kernel.clone();
        let crashed = std::thread::spawn(move || {
            writer.load(Boom);
        })
        .join();
        assert!(
            crashed.is_err(),
            "apply panic 必须照旧抛出去,不能被吞成一条 Failed"
        );

        assert!(
            matches!(kernel.state_of("boom"), Some(PluginState::Failed(_))),
            "panic 的记录要落到 Failed;卡在 Loading 的话界面上会冒充 Pending 并报「缺 []」,实际是 {:?}",
            kernel.state_of("boom")
        );

        // 关键回归点:resolving 没复位的话,下面两次 load 里的 resolve 会在
        // 第一行直接返回,两个插件永远停在 Pending。
        kernel.load(Provider {
            name: "after-boom",
            service: "after",
        });
        assert_eq!(
            kernel.state_of("after-boom"),
            Some(PluginState::Active),
            "panic 之后内核必须还能装载新插件"
        );
        kernel.load(Provider {
            name: "later",
            service: "later-svc",
        });
        assert_eq!(
            kernel.state_of("later"),
            Some(PluginState::Active),
            "panic 之后内核必须还能接着自动装载"
        );
    }

    #[test]
    fn duplicate_plugin_name_is_reported() {
        let kernel = Kernel::new();
        let lines = log_sink(&kernel);

        kernel.load(Provider {
            name: "dup",
            service: "s1",
        });
        kernel.load(Provider {
            name: "dup",
            service: "s2",
        });

        assert_eq!(kernel.len(), 1, "同名只装第一个");
        let lines = lines.lock().unwrap();
        assert_eq!(
            lines.len(),
            1,
            "被忽略的那次注册必须留一条日志,不能静默: {lines:?}"
        );
        assert!(lines[0].contains("dup"), "日志里要点名是谁重名");
    }

    #[test]
    fn service_provided_outside_apply_is_reported() {
        let kernel = Kernel::new();
        let lines = log_sink(&kernel);

        // 宿主直接 provide:没有「正在装谁」这个上下文,归属就是未知
        kernel.provide("host-svc", 7u8);
        {
            let lines = lines.lock().unwrap();
            assert_eq!(
                lines.len(),
                1,
                "归属未知的服务必须留一条日志,不能静默: {lines:?}"
            );
            assert!(lines[0].contains("host-svc"));
        }

        // 插件在 apply 里 provide:有主,不该再报未知
        lines.lock().unwrap().clear();
        kernel.load(Provider {
            name: "p",
            service: "svc",
        });
        assert!(
            lines.lock().unwrap().is_empty(),
            "插件 apply 里 provide 的服务是有主的,不该报未知"
        );
        assert_eq!(
            kernel.records()[0].provides,
            vec!["svc"],
            "有主的服务要照旧记进 provides"
        );
    }
}