lean_ctx/core/
cockpit_launch_record.rs1use std::path::PathBuf;
15
16use serde::{Deserialize, Serialize};
17
18use crate::ipc::process::ProcessIdentity;
19
20pub(crate) const RECORD_FILE: &str = "dashboard.launch.json";
22pub const DETACHED_ENV: &str = "LEAN_CTX_COCKPIT_DETACHED";
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct LaunchRecord {
28 pub(crate) schema_version: u32,
29 pub pid: u32,
30 pub(crate) identity: Option<ProcessIdentity>,
32 pub host: String,
33 pub port: u16,
34 pub base_path: String,
35 pub(crate) version: String,
36 pub detached: bool,
39}
40
41impl LaunchRecord {
42 pub fn of_this_process(host: &str, port: u16, base_path: &str) -> Self {
44 let pid = std::process::id();
45 Self {
46 schema_version: 1,
47 pid,
48 identity: crate::ipc::process::identity(pid),
49 host: host.to_string(),
50 port,
51 base_path: base_path.to_string(),
52 version: env!("CARGO_PKG_VERSION").to_string(),
53 detached: std::env::var_os(DETACHED_ENV).is_some(),
54 }
55 }
56
57 #[must_use]
59 pub fn identity(&self) -> Option<&ProcessIdentity> {
60 self.identity.as_ref()
61 }
62
63 pub fn process_alive(&self) -> bool {
67 if !crate::ipc::process::is_alive(self.pid) {
68 return false;
69 }
70 match (&self.identity, crate::ipc::process::identity(self.pid)) {
71 (Some(recorded), Some(current)) => {
72 crate::ipc::process::same_process(recorded, ¤t)
73 }
74 _ => true,
75 }
76 }
77}
78
79fn record_path() -> Option<PathBuf> {
80 crate::core::paths::state_dir()
81 .ok()
82 .map(|dir| dir.join(RECORD_FILE))
83}
84
85pub fn read_record() -> Option<LaunchRecord> {
86 let bytes = std::fs::read(record_path()?).ok()?;
87 if bytes.len() > 16 * 1024 {
88 return None;
89 }
90 serde_json::from_slice::<LaunchRecord>(&bytes)
91 .ok()
92 .filter(|record| record.schema_version == 1)
93}
94
95pub fn write_record(record: &LaunchRecord) {
96 let Some(path) = record_path() else {
97 return;
98 };
99 if let Some(dir) = path.parent() {
100 let _ = std::fs::create_dir_all(dir);
101 }
102 if let Ok(bytes) = serde_json::to_vec_pretty(record) {
103 let _ = crate::core::updater::atomic_write_bytes(&path, &bytes);
104 }
105}
106
107pub fn remove_record_of(pid: u32) {
109 if read_record().is_some_and(|record| record.pid == pid)
110 && let Some(path) = record_path()
111 {
112 let _ = std::fs::remove_file(path);
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn the_record_round_trips_and_carries_no_token() {
122 let record = LaunchRecord::of_this_process("127.0.0.1", 3333, "");
123 let json = serde_json::to_string(&record).unwrap();
124 assert!(!json.contains("token"));
125 assert_eq!(serde_json::from_str::<LaunchRecord>(&json).unwrap(), record);
126 assert!(record.process_alive());
127 }
128}