ek-station 0.2.0

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
//! ek-station headless 主程序。
//!
//! 用法:`ek-station [--from <file>] [--ready-file <path>]`
//! - 缺省:演示清单(`StationConfig::demo()` = dm + oms)。
//! - `--from <file>`:简单文本清单文件,一行一个工厂名,`#` 起注释行
//!   (清单即结构面,定盘口径:宿主不读 toml 配置)。
//! - `--ready-file <path>`:装配并 start 成功后写 `ready\n`(launcher/S1c
//!   就绪协议;装配失败阶段不写 —— launcher 的 boot-wait 靠它区分成败)。
//!
//! 退出策略(v1,Windows 控制台无 std 信号面):stdin 读到 `q`/`quit` → 优雅
//! `close_all` 后退出 0;**EOF 与 stdin 不可用同归常驻**(launcher 的
//! `spawn_detached` 用 stdin=null ⇒ 子进程起手即 EOF,靠 keep-alive 活到被 kill
//! ——EOF 若走优雅退,launcher 子进程会秒退,协议即碎;实测钉死于此)。
//! Windows `SetConsoleCtrlHandler` 需三方或 unsafe,std 面没有 → 记 BD-UI-9,
//! v1 不做信号级优雅面(五步闭停主要在 `q`/launcher 路径生效)。

use std::io::BufRead;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::Arc;
use std::time::Duration;

use ek_core::main_engine::MainEngine;
use ek_station::{StationConfig, boot, default_kinds};

fn main() -> ExitCode {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let mut cfg = StationConfig::demo();
    let mut ready: Option<PathBuf> = None;

    let mut it = args.iter();
    while let Some(flag) = it.next() {
        match flag.as_str() {
            "--from" => match it.next() {
                Some(path) => match read_plugin_list(Path::new(path)) {
                    Ok(list) => cfg.plugins = list,
                    Err(e) => {
                        eprintln!("ek-station: --from {path}: {e}");
                        return ExitCode::from(2);
                    }
                },
                None => {
                    eprintln!("ek-station: --from needs a path");
                    return ExitCode::from(2);
                }
            },
            "--ready-file" => match it.next() {
                Some(path) => ready = Some(PathBuf::from(path)),
                None => {
                    eprintln!("ek-station: --ready-file needs a path");
                    return ExitCode::from(2);
                }
            },
            other => {
                eprintln!("ek-station: unknown flag {other} (use --from/--ready-file)");
                return ExitCode::from(2);
            }
        }
    }

    // 装配 + 直启(headless main 不挂探针,start 就在这里)。
    let engine = Arc::new(MainEngine::new(None));
    let kinds = default_kinds();
    if let Err(e) = boot(&engine, &kinds, &cfg) {
        eprintln!("ek-station: boot failed: {e}");
        return ExitCode::from(1);
    }
    if let Err(e) = engine.start() {
        eprintln!("ek-station: start failed: {e:?}");
        engine.close_all(Duration::from_secs(3)); // () 返回;失败仅 stderr 告警位(宿主留档)
        return ExitCode::from(1);
    }
    let names = engine.plugin_names();
    eprintln!("ek-station: running (plugins: {})", names.join(", "));

    if let Some(path) = ready {
        if let Err(e) = std::fs::write(path, "ready\n") {
            eprintln!("ek-station: --ready-file write failed: {e:?}");
        }
    }

    // stdin 策略:`q`/`quit` → 优雅收场;EOF/不可用 → 常驻(detached 子进程的
    // stdin=null 起手即 EOF,必须走常驻,见模块头退出策略)。
    let stdin = std::io::stdin();
    let mut line = String::new();
    let mut graceful = false;
    loop {
        line.clear();
        match stdin.lock().read_line(&mut line) {
            Ok(0) => break, // 管道 EOF
            Ok(_) => {
                let t = line.trim();
                if t == "q" || t == "quit" {
                    graceful = true;
                    break;
                }
            }
            Err(_) => break,
        }
    }
    if graceful {
        engine.close_all(Duration::from_secs(3));
        eprintln!("ek-station: closed");
        ExitCode::SUCCESS
    } else {
        // 无控制台(detached)/错误流:stdin 不可用 → 常驻心跳,进程外 kill 终止。
        eprintln!("ek-station: stdin unavailable; keep-alive loop");
        loop {
            std::thread::sleep(Duration::from_secs(60));
        }
    }
}

/// 简单清单:一行一工厂名(`#` 起注释;空白行忽略)—— 语义地面在
/// `ek_station::pickfile`(BD-UI-15:S3 抽世,bin 与 launcher 共此一处)。
fn read_plugin_list(path: &Path) -> Result<Vec<String>, String> {
    ek_station::pickfile::read(path).map_err(|e| e.to_string())
}