Skip to main content

ek_ui/
runtime.rs

1//! 运行时装配:工厂表(ui 工厂注册)、宿主观察线程(关窗→close_all)、
2//! 进程主入口(eframe 主线程 + 宿主观察线程)。
3//!
4//! 线程形貌(S1b 双线程纪律):
5//! - 主线程:eframe run_native(App 只读 [`UiShared`]);
6//! - 观察线程:sleep 轮询 `UiShared::is_stop` → `engine.close_all`;
7//! - 等价 headless 形貌见 ek-station bin(不带 ui 工厂)。
8
9use std::sync::Arc;
10use std::time::Duration;
11
12use ek_core::main_engine::MainEngine;
13use ek_core::plugin::Plugin;
14use ek_dm::plugin::DmConfig;
15use ek_station::{PlugKind, StationConfig, default_kinds};
16
17use crate::plugin::{UI_NAME, UiPlugin};
18use crate::state::UiShared;
19
20/// ek-station 缺省工厂表(dm/oms/ctp;0.1.12 起 sim 已退场)+ `ui` 工厂 +
21/// **rt-on 的 dm 工厂**(S4:UI 宿主观测面需要 `eBarUpdate.` 实时门;
22/// ek-station headless 保持 rt-off 缺省不动 —— 两宿主的 dm 配置差异在各自
23/// 工厂表,互不影响)。
24///
25/// `dm_rt` 是装配期参数(非配置文件;ek-ui 零配置口径,缺省 true);
26/// 多次 build 同享一个 `UiShared`(ui 插件被 svctl 摘/再注,观测不丢)。
27pub fn ui_kinds(shared: &Arc<UiShared>, dm_rt: bool) -> Vec<PlugKind> {
28    let mut kinds: Vec<PlugKind> = default_kinds()
29        .into_iter()
30        .filter(|k| k.name != "dm")
31        .collect();
32    kinds.push(PlugKind {
33        name: "dm",
34        doc: "数据管理器(KlineManager;real_time 更新门开,行情板/K线页取 eBarUpdate./eBar.)",
35        build: Arc::new(move || -> Arc<dyn Plugin> {
36            Arc::new(ek_dm::plugin::DmPlugin::new(DmConfig {
37                real_time: dm_rt,
38                ..DmConfig::minute("dm")
39            }))
40        }),
41        build_named: None,
42    });
43    kinds.push(PlugKind {
44        name: UI_NAME,
45        doc: "常驻观测界面(eLog + eTimer 1s 快照;关窗即 stop)",
46        build: {
47            let shared = Arc::clone(shared);
48            Arc::new(move || -> Arc<dyn Plugin> { Arc::new(UiPlugin::new(Arc::clone(&shared))) })
49        },
50        build_named: None,
51    });
52    kinds
53}
54
55/// 宿主观察线程:窗口不复用的引擎在这里按 stop 位收场(close_all join)。
56///
57/// 纪律:close 顺序也让宿主自己走(逆 start 序),观察线程只送信号不代走步骤。
58pub fn spawn_host_watcher(
59    engine: &Arc<MainEngine>,
60    shared: &Arc<UiShared>,
61) -> std::thread::JoinHandle<()> {
62    let engine = Arc::clone(engine);
63    let shared = Arc::clone(shared);
64    std::thread::spawn(move || {
65        while !shared.is_stop() {
66            std::thread::sleep(Duration::from_millis(120));
67        }
68        shared.log_line("[host] 窗口关闭,close_all …");
69        engine.close_all(Duration::from_secs(3));
70        shared.log_line("[host] 已收场");
71    })
72}
73
74/// eframe App(窗口面):只读 shared 渲染 + 1s 节流重绘。
75struct StationApp {
76    shared: Arc<UiShared>,
77}
78
79impl eframe::App for StationApp {
80    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
81        // 面板组合在 render_body 内(官方纪律:CentralPanel 最后;BD-UI-29)。
82        crate::render_body(ctx, &self.shared);
83        ctx.request_repaint_after(Duration::from_secs(1));
84    }
85}
86
87/// 运行选项(S1c ready 标记 + S4 dm real_time 装配参数;ek-ui 零配置口径,
88/// 无任何配置文件输入 —— 结构只由 StationConfig 清单决定)。
89#[derive(Debug, Clone)]
90pub struct RunOpts {
91    /// launcher 协议:`start 成功后写 "ready\n"` 的标记路径。
92    pub ready_marker: Option<std::path::PathBuf>,
93    /// dm 工厂 real_time 门(装配参数,缺省 true —— UI 宿主观测面要实时门)。
94    pub dm_rt: bool,
95}
96
97impl Default for RunOpts {
98    /// 缺省:无 ready 标记 + **dm_rt = true**(UI 宿主缺省实时门;
99    /// 勿改回 derive(Default) —— bool 缺省 false 会静默关实时门)。
100    fn default() -> Self {
101        Self {
102            ready_marker: None,
103            dm_rt: true,
104        }
105    }
106}
107
108impl RunOpts {
109    /// 空选项(无 ready 标记)。
110    pub fn plain() -> Self {
111        Self::default()
112    }
113}
114
115/// 进程主入口:装配(dm/oms/ctp/ui 按清单)→ start → 就绪标记(可选)
116/// → eframe 主循环 → 观察线程收场。
117pub fn run_station_ui(cfg: &StationConfig, opts: &RunOpts) -> Result<(), String> {
118    let engine = Arc::new(MainEngine::new(None));
119    let shared = Arc::new(UiShared::new());
120
121    ek_station::boot(&engine, &ui_kinds(&shared, opts.dm_rt), cfg)
122        .map_err(|e| format!("boot: {e:?}"))?;
123    // start 失败也要走 close_all(0.4.3 BD-UI-64,审核 P3;与 ek-station headless
124    // main.rs 的 start 失败路径同口径):start 是逐个 launch 的,失败时已有插件在
125    // Running —— 不闭就没有 on_stop(网关侧的会话收尾与流目录处理被跳过)。
126    // boot 失败不用闭:那时插件只登记未 launch,没有 worker 要收。
127    if let Err(e) = engine.start() {
128        engine.close_all(Duration::from_secs(3));
129        return Err(format!("start: {e:?}"));
130    }
131
132    if let Some(marker) = &opts.ready_marker {
133        use std::io::Write;
134        if let Ok(mut f) = std::fs::File::create(marker) {
135            let _ = f.write_all(crate::launch::READY_TEXT.as_bytes());
136        } else {
137            eprintln!("ek-ui: ready marker write failed: {}", marker.display());
138        }
139    }
140
141    let watcher = spawn_host_watcher(&engine, &shared);
142
143    let opts = eframe::NativeOptions {
144        viewport: egui::ViewportBuilder::default()
145            .with_inner_size([1280.0, 860.0])
146            .with_title("ek-station"),
147        ..Default::default()
148    };
149    // 关窗即收:run_native 返回 = 窗口已关 → 置 stop → 观察线程 close_all(BD-UI-11)。
150    let result = eframe::run_native(
151        "ek-station",
152        opts,
153        Box::new(|cc| {
154            crate::fonts::apply_cjk_fonts(&cc.egui_ctx); // BD-UI-26:CJK 回退,中文不乱码
155            Ok(Box::new(StationApp {
156                shared: Arc::clone(&shared),
157            }) as Box<dyn eframe::App>)
158        }),
159    );
160    shared.request_stop();
161    let _ = watcher.join();
162    result.map_err(|e| format!("ui: {e:?}"))
163}