use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::ipc::process::ProcessIdentity;
pub(crate) const RECORD_FILE: &str = "dashboard.launch.json";
pub const DETACHED_ENV: &str = "LEAN_CTX_COCKPIT_DETACHED";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LaunchRecord {
pub(crate) schema_version: u32,
pub pid: u32,
pub(crate) identity: Option<ProcessIdentity>,
pub host: String,
pub port: u16,
pub base_path: String,
pub(crate) version: String,
pub detached: bool,
}
impl LaunchRecord {
pub fn of_this_process(host: &str, port: u16, base_path: &str) -> Self {
let pid = std::process::id();
Self {
schema_version: 1,
pid,
identity: crate::ipc::process::identity(pid),
host: host.to_string(),
port,
base_path: base_path.to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
detached: std::env::var_os(DETACHED_ENV).is_some(),
}
}
#[must_use]
pub fn identity(&self) -> Option<&ProcessIdentity> {
self.identity.as_ref()
}
pub fn process_alive(&self) -> bool {
if !crate::ipc::process::is_alive(self.pid) {
return false;
}
match (&self.identity, crate::ipc::process::identity(self.pid)) {
(Some(recorded), Some(current)) => {
crate::ipc::process::same_process(recorded, ¤t)
}
_ => true,
}
}
}
fn record_path() -> Option<PathBuf> {
crate::core::paths::state_dir()
.ok()
.map(|dir| dir.join(RECORD_FILE))
}
pub fn read_record() -> Option<LaunchRecord> {
let bytes = std::fs::read(record_path()?).ok()?;
if bytes.len() > 16 * 1024 {
return None;
}
serde_json::from_slice::<LaunchRecord>(&bytes)
.ok()
.filter(|record| record.schema_version == 1)
}
pub fn write_record(record: &LaunchRecord) {
let Some(path) = record_path() else {
return;
};
if let Some(dir) = path.parent() {
let _ = std::fs::create_dir_all(dir);
}
if let Ok(bytes) = serde_json::to_vec_pretty(record) {
let _ = crate::core::updater::atomic_write_bytes(&path, &bytes);
}
}
pub fn remove_record_of(pid: u32) {
if read_record().is_some_and(|record| record.pid == pid)
&& let Some(path) = record_path()
{
let _ = std::fs::remove_file(path);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_record_round_trips_and_carries_no_token() {
let record = LaunchRecord::of_this_process("127.0.0.1", 3333, "");
let json = serde_json::to_string(&record).unwrap();
assert!(!json.contains("token"));
assert_eq!(serde_json::from_str::<LaunchRecord>(&json).unwrap(), record);
assert!(record.process_alive());
}
}