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"
);
}
}