aether_lspd/
socket_path.rs1use crate::language_catalog::LanguageId;
2use crate::language_catalog::socket_identity_for_language;
3use std::path::{Path, PathBuf};
4
5#[doc = include_str!("docs/socket_path.md")]
6pub fn socket_path(workspace_root: &Path, language: LanguageId) -> PathBuf {
7 let socket_dir = get_socket_dir();
8 let socket_name = generate_socket_name(workspace_root, language);
9 socket_dir.join(socket_name)
10}
11
12pub fn ensure_socket_dir(workspace_root: &Path, language: LanguageId) -> std::io::Result<PathBuf> {
14 let path = socket_path(workspace_root, language);
15 if let Some(parent) = path.parent() {
16 std::fs::create_dir_all(parent)?;
17 }
18 Ok(path)
19}
20
21pub fn lockfile_path(socket_path: &Path) -> PathBuf {
23 socket_path.with_extension("lock")
24}
25
26pub fn log_file_path(socket_path: &Path) -> PathBuf {
30 socket_path.with_extension("log")
31}
32
33fn get_socket_dir() -> PathBuf {
37 if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
38 return PathBuf::from(runtime_dir).join("aether-lspd");
39 }
40
41 let uid = get_uid();
42 PathBuf::from(format!("/tmp/aether-lspd-{uid}"))
43}
44
45fn generate_socket_name(workspace_root: &Path, language: LanguageId) -> String {
47 use sha2::{Digest, Sha256};
48
49 let canonical = workspace_root.canonicalize().unwrap_or_else(|_| workspace_root.to_path_buf());
50
51 let path_bytes = canonical.as_os_str().as_encoded_bytes();
52 let hash = Sha256::digest(path_bytes);
53 let short_hash = u64::from_be_bytes(hash[..8].try_into().unwrap());
55
56 format!("lsp-{}-{:016x}.sock", socket_identity_for_language(language), short_hash)
57}
58
59#[cfg(unix)]
61fn get_uid() -> u32 {
62 unsafe { libc::getuid() }
63}
64
65#[cfg(not(unix))]
66fn get_uid() -> u32 {
67 0
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn test_socket_path_deterministic() {
76 let workspace = Path::new("/tmp/test-workspace");
77 let path1 = socket_path(workspace, LanguageId::Rust);
78 let path2 = socket_path(workspace, LanguageId::Rust);
79 assert_eq!(path1, path2);
80 }
81
82 #[test]
83 fn test_socket_path_different_languages() {
84 let workspace = Path::new("/tmp/test-workspace");
85 let rust_path = socket_path(workspace, LanguageId::Rust);
86 let python_path = socket_path(workspace, LanguageId::Python);
87 assert_ne!(rust_path, python_path);
88 }
89
90 #[test]
91 fn test_socket_path_different_workspaces() {
92 let workspace1 = Path::new("/tmp/workspace1");
93 let workspace2 = Path::new("/tmp/workspace2");
94 let path1 = socket_path(workspace1, LanguageId::Rust);
95 let path2 = socket_path(workspace2, LanguageId::Rust);
96 assert_ne!(path1, path2);
97 }
98
99 #[test]
100 fn test_socket_path_contains_language() {
101 let workspace = Path::new("/tmp/test-workspace");
102 let path = socket_path(workspace, LanguageId::Rust);
103 let filename = path.file_name().unwrap().to_str().unwrap();
104 assert!(filename.contains("rust"));
105 assert!(std::path::Path::new(filename).extension().is_some_and(|ext| ext.eq_ignore_ascii_case("sock")));
106 }
107
108 #[test]
109 fn test_lockfile_path() {
110 let socket = PathBuf::from("/tmp/aether-lspd-1000/lsp-rust-abc123.sock");
111 let lockfile = lockfile_path(&socket);
112 assert_eq!(lockfile, PathBuf::from("/tmp/aether-lspd-1000/lsp-rust-abc123.lock"));
113 }
114
115 #[test]
116 fn socket_path_uses_shared_server_identity() {
117 let workspace = Path::new("/tmp/test-workspace");
118 assert_eq!(socket_path(workspace, LanguageId::TypeScript), socket_path(workspace, LanguageId::TypeScriptReact));
119 assert_eq!(socket_path(workspace, LanguageId::C), socket_path(workspace, LanguageId::Cpp));
120 }
121}