ek-station 0.2.7

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
//! pick 清单(Launcher↔bin 的共享地面;S3 自 BD-UI-15 从 bin/ek-ui 抽世统一)。
//!
//! 格式:一行一工厂名;`#` 起注释;空行忽略;行内空白拒(错行号指出);
//! 全空 → Empty;重名/未知名延迟到装配期 boot 硬报错(BD-U4)。
//! 生态纪律:自研 crate 之间以装版引用 —— 本模块发布后,ek-ui 亦走此地面
//! (其 `pick` 门面即本模块的薄转发)。

use std::path::{Path, PathBuf};

/// 清单文件名前缀。
pub const PICK_PREFIX: &str = "ek-pick-";
/// 清单文件名后缀。
pub const PICK_SUFFIX: &str = ".list";

/// 清单读写错(业务枚举;launcher/text bin 面可读可断言)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PickListErr {
    /// 空清单(启动至少一个插件)。
    Empty,
    /// 行内含空白/前缀不然(带行号与内容)。
    BadLine(usize, String),
    /// IO 叙文。
    Io(String),
}

impl std::fmt::Display for PickListErr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Empty => write!(f, "pick list empty (select at least one plugin)"),
            Self::BadLine(n, line) => {
                write!(f, "pick list line {n} has unexpected whitespace: {line:?}")
            }
            Self::Io(e) => write!(f, "pick list io: {e}"),
        }
    }
}

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

/// 唯一文件名(进程 id + 纳秒;同目录并发不撞)。
fn unique_name() -> String {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_nanos() as u64)
        .unwrap_or(0);
    format!("{PICK_PREFIX}{}{}{PICK_SUFFIX}", std::process::id(), nanos)
}

/// 读清单(bin `--from` 面)。
pub fn read(path: &Path) -> Result<Vec<String>, PickListErr> {
    let text = std::fs::read_to_string(path).map_err(|e| PickListErr::Io(e.to_string()))?;
    // BOM 剥离(Notepad 系 UTF-8 的编码叙文,不是内容;不剥会把首行注释打成 BadLine)。
    let text = text.strip_prefix('\u{feff}').unwrap_or(&text);
    let mut names = Vec::new();
    for (i, raw) in text.lines().enumerate() {
        let line = raw.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        if line.chars().any(|c| c.is_whitespace()) {
            return Err(PickListErr::BadLine(i + 1, raw.to_string()));
        }
        names.push(line.to_string());
    }
    if names.is_empty() {
        return Err(PickListErr::Empty);
    }
    Ok(names)
}

/// 写清单到指定目录(launcher 面),返回文件路径。
pub fn write_in(dir: &Path, names: &[&str]) -> Result<PathBuf, PickListErr> {
    if names.is_empty() {
        return Err(PickListErr::Empty);
    }
    for (i, n) in names.iter().enumerate() {
        if n.chars().any(|c| c.is_whitespace()) || n.is_empty() || n.starts_with('#') {
            return Err(PickListErr::BadLine(i + 1, (*n).to_string()));
        }
    }
    std::fs::create_dir_all(dir).map_err(|e| PickListErr::Io(e.to_string()))?;
    let path = dir.join(unique_name());
    let mut text = String::from("# ek launch pick\n");
    for n in names {
        text.push_str(n);
        text.push('\n');
    }
    std::fs::write(&path, text).map_err(|e| PickListErr::Io(e.to_string()))?;
    Ok(path)
}