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