ek-ui 0.2.21

ek 生态常驻主界面(egui/eframe):插件即界面——观测页 + 日志页,车头与 UI 同插件
Documentation
//! `--pick` 主流程(launcher 控制器):选窗 → 写清单 → spawn 子进程(`ek-ui --from
//! <清单> --ready-file <标记>`)→ 就绪等待 → 成功放手 / 失败聚合退出。
//!
//! 线程形貌:窗口线程只画与翻位;spawn+wait 在工作线程(30s 不冻结 UI);
//! 结果经共享状态(`RunShared`)回槽,App 见果即关窗。

use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use crate::launch::{DEFAULT_READY_TIMEOUT, LaunchFail, launch, launch_workdir, unique_marker};
use crate::pick::write_pick_in;

/// launcher 各方共一回的小状态(应用体和工作线程各持一份 Arc)。
#[derive(Default)]
pub struct RunShared {
    /// 工作线程句柄(收尸用;None = 未 spawn)。
    pub handle: Mutex<Option<std::thread::JoinHandle<()>>>,
    /// 一次性结果(None = 未了 / 用户未触发)。
    pub outcome: Mutex<Option<Result<(), LaunchFail>>>,
}

/// `--pick` 进程入口。返回 Ok = 子进程成功脱管(或用户取消/未触发);Err = 失败文本。
pub fn run_pick_launcher() -> Result<(), String> {
    let program = std::env::current_exe().map_err(|e| format!("current_exe: {e:?}"))?;
    let shared = Arc::new(RunShared::default());
    let app = crate::launcher_ui::PickApp::new(program.clone(), shared.clone());

    let opts = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_inner_size([460.0, 460.0])
            .with_title("ek 启动选择"),
        ..Default::default()
    };
    let window = eframe::run_native(
        "ek-launch",
        opts,
        Box::new(|cc| {
            crate::fonts::apply_cjk_fonts(&cc.egui_ctx); // BD-UI-26:CJK 回退,中文不乱码
            Ok(Box::new(app) as Box<dyn eframe::App>)
        }),
    );

    // 工作线程收尾(无论窗口如何关)。
    if let Some(h) = shared.handle.lock().unwrap().take() {
        let _ = h.join();
    }
    window.map_err(|e| format!("pick window: {e:?}"))?;

    match shared.outcome.lock().unwrap().take() {
        None => Ok(()),                     // 取消 / 空窗关闭:无子进程
        Some(Ok(())) => Ok(()),             // spawn 成功,子进程分离脱管
        Some(Err(e)) => Err(e.to_string()), // 失败聚合(spawn/退出/超时)
    }
}

/// 工作线程体:写清单 → spawn ek-ui(--from/--ready-file)→ wait_ready。
/// 每次启动新清单文件 + 新标记(唯一名:进程 id + 纳秒)。
pub fn launch_worker(shared: &RunShared, names: &[String], program: PathBuf) {
    let dir = launch_workdir();
    let names_ref: Vec<&str> = names.iter().map(String::as_str).collect();
    let outcome = (|| -> Result<(), LaunchFail> {
        let pick_path = write_pick_in(&dir, &names_ref)?;
        let marker = unique_marker(&dir);
        let args = vec![
            "--from".to_string(),
            pick_path.to_string_lossy().into_owned(),
            "--ready-file".to_string(),
            marker.to_string_lossy().into_owned(),
        ];
        launch(&program, &args, &marker, DEFAULT_READY_TIMEOUT)
    })();
    *shared.outcome.lock().unwrap() = Some(outcome);
}

/// update 轮询节拍(spawned 状态下看槽)。
pub const POLL: Duration = Duration::from_millis(200);