lean_ctx/lsp/
port_discovery.rs1use std::time::Duration;
9
10use serde::Deserialize;
11
12#[derive(Debug, Clone, Deserialize)]
14pub struct PortFile {
15 pub port: u16,
16 pub token: String,
17 pub pid: u32,
18 #[serde(default)]
19 pub project_root: String,
20 #[serde(default)]
21 pub ide_version: String,
22}
23
24pub fn project_hash(project_root: &str) -> String {
26 use std::fmt::Write as _;
27
28 use sha2::{Digest, Sha256};
29 let canonical = std::fs::canonicalize(project_root).map_or_else(
30 |_| project_root.to_string(),
31 |p| p.to_string_lossy().to_string(),
32 );
33 let digest = Sha256::digest(canonical.as_bytes());
34 let mut hex = String::with_capacity(16);
35 for b in digest.iter().take(8) {
36 let _ = write!(hex, "{b:02x}");
37 }
38 hex
39}
40
41pub fn port_file_path(project_root: &str) -> Option<std::path::PathBuf> {
45 let dir = crate::core::data_dir::lean_ctx_data_dir().ok()?;
46 Some(dir.join(format!("jetbrains-{}.port", project_hash(project_root))))
47}
48
49pub fn read_port_file(project_root: &str) -> Option<PortFile> {
51 use std::io::Read;
52 const MAX_BYTES: u64 = 64 * 1024;
54 let path = port_file_path(project_root)?;
55 let mut text = String::new();
56 std::fs::File::open(path)
57 .ok()?
58 .take(MAX_BYTES + 1)
59 .read_to_string(&mut text)
60 .ok()?;
61 if text.len() as u64 > MAX_BYTES {
62 return None;
63 }
64 serde_json::from_str(&text).ok()
65}
66
67pub fn pid_alive(pid: u32) -> bool {
70 #[cfg(target_os = "linux")]
71 {
72 std::path::Path::new(&format!("/proc/{pid}")).exists()
73 }
74 #[cfg(not(target_os = "linux"))]
75 {
76 let _ = pid;
77 true
78 }
79}
80
81pub fn health_ok(pf: &PortFile) -> bool {
84 let url = format!("http://127.0.0.1:{}/health", pf.port);
85 ureq::get(&url)
86 .config()
87 .timeout_global(Some(Duration::from_millis(300)))
88 .build()
89 .header("X-LeanCtx-Token", &pf.token)
90 .call()
91 .is_ok()
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn project_hash_is_stable_and_16_hex() {
100 let h1 = project_hash("/some/project");
101 let h2 = project_hash("/some/project");
102 assert_eq!(h1, h2, "hash must be deterministic");
103 assert_eq!(h1.len(), 16, "expected 16 hex chars (8 bytes)");
104 assert!(h1.chars().all(|c| c.is_ascii_hexdigit()));
105 }
106
107 #[test]
108 fn port_file_absent_for_unlikely_root() {
109 assert!(read_port_file("/nonexistent/lean-ctx/project/xyz").is_none());
111 }
112
113 #[test]
114 fn project_hash_matches_known_vector() {
115 assert_eq!(project_hash("/some/project"), "a0317725f24b01df");
118 }
119
120 #[test]
121 fn port_file_path_honors_data_dir_env() {
122 let _lock = crate::core::data_dir::test_env_lock();
123 let dir = std::env::temp_dir().join("lc_jb_portfile_env");
124 crate::test_env::set_var("LEAN_CTX_DATA_DIR", dir.to_str().unwrap());
125 let p = port_file_path("/some/project").unwrap();
126 crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
127 assert_eq!(p, dir.join("jetbrains-a0317725f24b01df.port"));
128 }
129}