ek-station 0.4.3

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
//! # station —— 装配器:`StationConfig` → Supervisor + 插件 → 待 starter。
//!
//! boot 只做装配(**不调 start**):调用方在这窗口可挂探针/额外插件,
//! 然后自己 `MainEngine::start()`(宿主按 deps 拓扑序装配并广播 `ePlugUp`)。
//! headless main 与整套测试都走这一条路径 —— 装配时序在一条代码路径上,
//! "launcher 勾选启动 = headless 同一逻辑"是字面成立的(方案 DV-5)。

use std::sync::Arc;

use ek_core::error::PluginError;
use ek_core::main_engine::MainEngine;
use ek_core::plugin::Plugin;

use crate::factory::PlugKind;
use crate::supervisor::Supervisor;

/// 站配置(纯清单,定盘口径:宿主零配置 —— 不读配置文件,需配置的插件其
/// 配置面归插件自身,见 ek-ui DESIGN BD-UI-19)。
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StationConfig {
    /// 启用的工厂名清单(svctl 自身不在此列,由 boot 自动装配)。
    pub plugins: Vec<String>,
}

impl StationConfig {
    /// 演示清单:`cfg`(托底,BD-CFG-20 缺省必含)+ dm + oms(纯逻辑,
    /// 不连任何真实通道)。
    #[must_use]
    pub fn demo() -> Self {
        Self {
            plugins: vec!["cfg".to_string(), "dm".to_string(), "oms".to_string()],
        }
    }
}

/// 装配期错误(硬报错给 main;tests 逐类断言)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StationError {
    /// 请求的工厂不在表内(DV-2:编译期表,拼写错启动即红)。
    UnknownKind(String),
    /// 宿主拒绝(重名 `Duplicate`/名字非法 `IllegalName`/已闭停 `Stopped`)。
    Core(PluginError),
    /// 配置托底错误文本(载入 IO/格式;BD-CFG-4 无 `.bak` 可救才走到这)。
    /// 收 String 保持 StationError 的 Clone/PartialEq 形(内部 io::Error 不拾)。
    Cfg(String),
    /// 插件配置面拒收(apply_config Err 错因原样透出)。
    ApplyRejected {
        /// 段 ns。
        ns: String,
        /// 原因。
        reason: String,
    },
    /// 两插件撞 ns(BD-CFG 段唯一性;boot 全表预筛)。
    NsDuplicate(String),
}

impl std::fmt::Display for StationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            StationError::UnknownKind(k) => write!(f, "unknown factory kind: {k}"),
            StationError::Core(e) => write!(f, "host rejected: {e:?}"),
            StationError::Cfg(e) => write!(f, "cfg load failed: {e}"),
            StationError::ApplyRejected { ns, reason } => {
                write!(f, "cfg apply rejected [{ns}]: {reason}")
            }
            StationError::NsDuplicate(ns) => write!(f, "config ns duplicate: {ns}"),
        }
    }
}

impl std::error::Error for StationError {}

impl From<PluginError> for StationError {
    fn from(e: PluginError) -> Self {
        StationError::Core(e)
    }
}

/// 装配监督者(`svctl`)与启用清单;调用方随后 `engine.start()`。
///
/// 与 [`boot_with_cfg`](Self::boot) 同一路径但不做配置回灌(零 Load 存取,
/// 存量调用面与测试零移植 —— 回灌要求显式给 Store,生产 main/ek-ui 走带面)。
///
/// # Errors
/// 未知工厂名([`StationError::UnknownKind`]);宿主拒绝(重名等,透传)。
pub fn boot(
    engine: &Arc<MainEngine>,
    kinds: &[PlugKind],
    cfg: &StationConfig,
) -> Result<(), StationError> {
    boot_with_cfg(engine, kinds, cfg, None)
}

/// 装配内核(0.4.1 抽取):监督者 + 清单插件全 add;返回 svctl 柄给
/// 复原入口复用(记档:字段与序一码一面,不许旁路复制)。
fn assemble(
    engine: &Arc<MainEngine>,
    kinds: &[PlugKind],
    cfg: &StationConfig,
    store: Option<&ek_cfg::Store>,
) -> Result<Arc<Supervisor>, StationError> {
    let sup = Arc::new(Supervisor::new(
        Arc::clone(engine),
        kinds.to_vec(),
        store.cloned(),
    ));
    // 先全量构造(unknown kind 即报,BD-U4 语义前进到这里仍早于任何 add)。
    let mut built: Vec<Arc<dyn Plugin>> = Vec::with_capacity(cfg.plugins.len());
    for name in &cfg.plugins {
        let kind = kinds
            .iter()
            .find(|k| k.name == *name)
            .ok_or_else(|| StationError::UnknownKind(name.clone()))?;
        built.push((kind.build)());
    }
    warn_if_cfg_missing(kinds, cfg);
    crate::cfgload::ns_probe_all(&built)?;
    engine.add_plugin(Arc::clone(&sup))?;
    for plugin in built {
        crate::cfgload::apply_persisted(plugin.as_ref(), store)?;
        engine.add_plugin(crate::factory::DynPlugin::wrap(plugin))?;
    }
    Ok(sup)
}

/// 工厂名清单缺省口径:必须含 `cfg`(BD-CFG-20);缺失 = stderr 显形警告
/// (软集中允许缺席,save 由调用方直调兜底,但缺席面要看得见)。
fn warn_if_cfg_missing(kinds: &[PlugKind], cfg: &StationConfig) {
    if !cfg.plugins.iter().any(|n| n == "cfg") && kinds.iter().any(|k| k.name == "cfg") {
        eprintln!(
            "[ek-cfg] 启用清单不含 cfg:配置持久化在运行期以调用方直调兜底(BD-CFG-10/20),托底盘不在路"
        );
    }
}

/// 装配 + 配置回灌(载入单点,cfgload::apply_persisted)。
///
/// `store = Some(&Store)`:对每个**有配置面**的插件在 add 之前执行
/// 「载入 → 直调 apply_config」(on_start 前;BD-CFG-6/16);`None` = 纯装配。
///
/// # Errors
/// 同 [`boot`];另加:`Cfg`(载入失败)、`ApplyRejected`(插件拒收)、
/// `NsDuplicate`(全表 ns 撞名预筛)。
pub fn boot_with_cfg(
    engine: &Arc<MainEngine>,
    kinds: &[PlugKind],
    cfg: &StationConfig,
    store: Option<&ek_cfg::Store>,
) -> Result<(), StationError> {
    assemble(engine, kinds, cfg, store).map(|_| ())
}

/// **守护复原入口(0.4.1)**:装配 + 回灌 + **按档案自动起连已存网关**。
///
/// 语义(2026-10-28 用户裁定「无界面的,就自动连接」的落点):
/// - 装配面与 [`boot_with_cfg`] 完全同码(同 [`assemble`]);
/// - 随后清点:存档根里 `ctp.*` 段 = 账户名单;`ui` 段的 `auto_connect.<网关>`
///   键 = "1" 才起连(**键缺 = 关**,勾选住在界面那侧,守护只读不作主);
/// - 起连 = 复用 `Supervisor::start_by`(装配侧回灌该段 → 实例 on_start
///   解密密库引用并自动连接 —— 与桌面手点同一条链),**不新增编排协议**;
/// - 引擎 start 由调用方随后执行(命名实例与既有插件一起按拓扑序 launch,
///   与「desktop 里手点连接」的时序同构);
/// - 桌面/演示**不走这一条**(它们走 [`boot_with_cfg`] —— 手点才连,
///   巡回判据零影响)。
///
/// 返回:实际发起起连的网关名单(测试/就绪日志用)。
///
/// # Errors
/// 同 [`boot_with_cfg`]。
pub fn boot_restore_gateways(
    engine: &Arc<MainEngine>,
    kinds: &[PlugKind],
    cfg: &StationConfig,
    store: Option<&ek_cfg::Store>,
) -> Result<Vec<String>, StationError> {
    let sup = assemble(engine, kinds, cfg, store)?;
    match store {
        Some(st) => Ok(crate::supervisor::restore_spawn_gateways(&sup, st)),
        None => Ok(Vec::new()),
    }
}