ek-station 0.2.6

Station for the ek ecosystem: headless plugin host bootstrap - assembles ek-core MainEngine from a compile-time plugin factory table, runs the svctl supervisor service (list/start/remove over bus ask) and a headless main (readiness marker + graceful close).
Documentation
//! # SupervisorPlugin —— 全系统唯一持 `Arc<MainEngine>` 的插件(BD-UI-2)。
//!
//! 启停/摘除只有宿主装配方能做(`PluginContext` 刻意无 add/remove 出口);
//! 本插件把这条特权面收进一个标准 ask 服务(`svctl.*` 三口,wire 契约见
//! [`crate::wire`]),UI/运维插件都是它的客户端,插件之间不互加。
//!
//! 收发形态照抄 DmPlugin 先例:routes = `eReq.<svctl>`;`on_mail` 里
//! Envelope 解码 → 逐契约 arm 命中即回;不持任何业务锁(宿主调用点
//! `add_plugin/remove_plugin` 是宿主自身同步的,宿主纪律:管理线程并发安全)。

use std::borrow::Cow;
use std::sync::{Arc, OnceLock};

use ek_core::context::PluginContext;
use ek_core::envelope::{Envelope, ReplyErr, e_req_type};
use ek_core::main_engine::MainEngine;
use ek_core::plugin::Plugin;
use ek_event::Event;

use crate::factory::PlugKind;
use crate::wire::{self, ListReq, ListRes, PlugRow, RemoveReq, RemoveRes, StartReq, StartRes};

/// 注册名(`svctl`):生态内唯一,不与业务插件竞争。
pub const NAME: &str = "svctl";

/// 宿主管控插件(注册进 [MainEngine] 后,`svctl.*` 服务即可跨插件 ask)。
pub struct Supervisor {
    engine: Arc<MainEngine>,
    kinds: Vec<PlugKind>,
    ctx: OnceLock<PluginContext>,
}

impl Supervisor {
    /// 持引擎句柄 + 工厂表快照(start 期注入;后续仅读)。
    #[must_use]
    pub fn new(engine: Arc<MainEngine>, kinds: Vec<PlugKind>) -> Self {
        Self {
            engine,
            kinds,
            ctx: OnceLock::new(),
        }
    }

    /// `eReq.<svctl>`:命中即应答并 return;全部不合 → 协议错(与 DmPlugin 同形)。
    fn on_request(&self, event: &Event) {
        let Some(env) = event.data.downcast_ref::<Envelope>() else {
            return; // 错入 type 的兜底(静默,宿主既有语义)
        };
        macro_rules! arm {
            ($ty:ty, $res:ty, $body:expr) => {
                if let Some(req) = env.payload.downcast_ref::<$ty>() {
                    let res: $res = $body(req);
                    let _ = env.reply.send(Box::new(Ok::<$res, ReplyErr>(res)));
                    return;
                }
            };
        }
        arm!(ListReq, ListRes, |_: &ListReq| self.rows());
        arm!(StartReq, StartRes, |r: &StartReq| self.start_by(r));
        arm!(RemoveReq, RemoveRes, |r: &RemoveReq| self.remove_by(r));
        // 载荷不是任何已声明契约 → 回包协议错(宿主既有语义)
        let _ = env
            .reply
            .send(Box::new(Err::<(), ReplyErr>(ReplyErr::InvalidPayload)));
    }

    /// list:注册序逐行(名字/版本/状态/deps/邮箱丢失/panic 计数)。
    fn rows(&self) -> ListRes {
        self.engine
            .plugin_names()
            .iter()
            .map(|name| {
                let info = self.engine.get_plugin(name);
                let stats = self.engine.mail_stats(name);
                PlugRow {
                    name: name.clone(),
                    version: info
                        .as_ref()
                        .map_or_else(String::new, |i| i.version.clone()),
                    state: info
                        .as_ref()
                        .map_or_else(String::new, |i| format!("{:?}", i.state)),
                    deps: info.map_or_else(Vec::new, |i| i.deps),
                    spilled: stats.map_or(0, |s| s.spilled),
                    panics: stats.map_or(0, |s| s.panics),
                }
            })
            .collect()
    }

    /// start:工厂表查名 → 构造(命名实例走 `build_named`)→ `add_plugin`
    /// (start 期状态下宿主立即装配)。
    ///
    /// 多账号面:`req.name` 非空 = 以该注册名建实例(工厂须支持命名);
    /// 空 = 旧行为(工厂名即注册名)。回带的名字一律取**插件自报的 `name()`**,
    /// 不再假设"工厂名 == 注册名"(0.3.0 之前这里写死工厂名,多实例下必错)。
    fn start_by(&self, req: &StartReq) -> StartRes {
        let Some(kind) = self.kinds.iter().find(|k| k.name == req.factory) else {
            return StartRes::Rejected {
                reason: format!("unknown factory: {}", req.factory),
            };
        };
        let want = req.name.trim();
        let built = if want.is_empty() {
            (kind.build)()
        } else {
            match &kind.build_named {
                Some(f) => f(want),
                // 不支持命名的工厂:明确回拒,不静默退回零参构造
                // (否则调用方以为建出了第二个实例,实际是撞名或建了个缺省名)。
                None => {
                    return StartRes::Rejected {
                        reason: format!("factory {} does not support named instances", req.factory),
                    };
                }
            }
        };
        // 注册名以插件自报为准(宿主 valid_plugin_name / Duplicate 仍在此把关)。
        let name = built.name().to_string();
        match self
            .engine
            .add_plugin(crate::factory::DynPlugin::wrap(built))
        {
            Ok(()) => {} // add_plugin 立即 launch_one(宿主 0.2.0 动态注入语义)
            Err(e) => {
                return StartRes::Rejected {
                    reason: format!("{e:?}"),
                };
            }
        }
        StartRes::Started { name }
    }

    /// remove:直接交给宿主(五步闭停 + 摘表 + `ePlugDown` 广播)。
    fn remove_by(&self, req: &RemoveReq) -> RemoveRes {
        match self.engine.remove_plugin(req.name.as_str()) {
            Ok(()) => RemoveRes::Removed {
                name: req.name.clone(),
            },
            Err(ek_core::error::PluginError::UnknownName(_)) => RemoveRes::NoSuchName {
                name: req.name.clone(),
            },
            Err(e) => RemoveRes::NoSuchName {
                name: format!("{e:?}"),
            },
        }
    }
}

impl Plugin for Supervisor {
    fn name(&self) -> &str {
        NAME
    }

    fn routes(&self) -> Vec<Cow<'static, str>> {
        vec![Cow::Owned(e_req_type(NAME))]
    }

    fn version(&self) -> Cow<'static, str> {
        // F6 陷阱:默认实现展开出 ek-core 的版本;必须显式覆写 env!(CARGO_PKG_VERSION)。
        Cow::Borrowed(env!("CARGO_PKG_VERSION"))
    }

    fn services(&self) -> Vec<ek_core::service::ServiceDesc> {
        wire::svctl_services()
    }

    fn on_init(&self, ctx: &PluginContext) {
        // 纪律:接线期不 put/不订阅;缓存句柄即可(本插件不自主发事件)。
        let _ = self.ctx.set(ctx.clone());
    }

    fn on_mail(&self, event: &Event) {
        if event.event_type == e_req_type(NAME) {
            self.on_request(event);
        }
    }
}