Skip to main content

ek_ui/
launcher.rs

1//! `--pick` 主流程(launcher 控制器):选窗 → 写清单 → spawn 子进程(`ek-ui --from
2//! <清单> --ready-file <标记>`)→ 就绪等待 → 成功放手 / 失败聚合退出。
3//!
4//! 线程形貌:窗口线程只画与翻位;spawn+wait 在工作线程(30s 不冻结 UI);
5//! 结果经共享状态(`RunShared`)回槽,App 见果即关窗。
6
7use std::path::PathBuf;
8use std::sync::{Arc, Mutex};
9use std::time::Duration;
10
11use crate::launch::{DEFAULT_READY_TIMEOUT, LaunchFail, launch, launch_workdir, unique_marker};
12use crate::pick::write_pick_in;
13
14/// launcher 各方共一回的小状态(应用体和工作线程各持一份 Arc)。
15#[derive(Default)]
16pub struct RunShared {
17    /// 工作线程句柄(收尸用;None = 未 spawn)。
18    pub handle: Mutex<Option<std::thread::JoinHandle<()>>>,
19    /// 一次性结果(None = 未了 / 用户未触发)。
20    pub outcome: Mutex<Option<Result<(), LaunchFail>>>,
21}
22
23/// `--pick` 进程入口。返回 Ok = 子进程成功脱管(或用户取消/未触发);Err = 失败文本。
24pub fn run_pick_launcher() -> Result<(), String> {
25    let program = std::env::current_exe().map_err(|e| format!("current_exe: {e:?}"))?;
26    let shared = Arc::new(RunShared::default());
27    let app = crate::launcher_ui::PickApp::new(program.clone(), shared.clone());
28
29    let opts = eframe::NativeOptions {
30        viewport: egui::ViewportBuilder::default()
31            .with_inner_size([460.0, 460.0])
32            .with_title("ek 启动选择"),
33        ..Default::default()
34    };
35    let window = eframe::run_native(
36        "ek-launch",
37        opts,
38        Box::new(|cc| {
39            crate::fonts::apply_cjk_fonts(&cc.egui_ctx); // BD-UI-26:CJK 回退,中文不乱码
40            Ok(Box::new(app) as Box<dyn eframe::App>)
41        }),
42    );
43
44    // 工作线程收尾(无论窗口如何关)。
45    if let Some(h) = shared.handle.lock().unwrap().take() {
46        let _ = h.join();
47    }
48    window.map_err(|e| format!("pick window: {e:?}"))?;
49
50    match shared.outcome.lock().unwrap().take() {
51        None => Ok(()),                     // 取消 / 空窗关闭:无子进程
52        Some(Ok(())) => Ok(()),             // spawn 成功,子进程分离脱管
53        Some(Err(e)) => Err(e.to_string()), // 失败聚合(spawn/退出/超时)
54    }
55}
56
57/// 工作线程体:写清单 → spawn ek-ui(--from/--ready-file)→ wait_ready。
58/// 每次启动新清单文件 + 新标记(唯一名:进程 id + 纳秒)。
59pub fn launch_worker(shared: &RunShared, names: &[String], program: PathBuf) {
60    let dir = launch_workdir();
61    let names_ref: Vec<&str> = names.iter().map(String::as_str).collect();
62    let outcome = (|| -> Result<(), LaunchFail> {
63        let pick_path = write_pick_in(&dir, &names_ref)?;
64        let marker = unique_marker(&dir);
65        let args = vec![
66            "--from".to_string(),
67            pick_path.to_string_lossy().into_owned(),
68            "--ready-file".to_string(),
69            marker.to_string_lossy().into_owned(),
70        ];
71        launch(&program, &args, &marker, DEFAULT_READY_TIMEOUT)
72    })();
73    *shared.outcome.lock().unwrap() = Some(outcome);
74}
75
76/// update 轮询节拍(spawned 状态下看槽)。
77pub const POLL: Duration = Duration::from_millis(200);