Skip to main content

aether_lspd/
socket_path.rs

1use 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
12/// Ensure the socket directory exists and return the socket path
13pub 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
21/// Get the lockfile path corresponding to a socket path
22pub fn lockfile_path(socket_path: &Path) -> PathBuf {
23    socket_path.with_extension("lock")
24}
25
26/// Get the log file path corresponding to a socket path.
27///
28/// The log file lives alongside the socket with a `.log` extension.
29pub fn log_file_path(socket_path: &Path) -> PathBuf {
30    socket_path.with_extension("log")
31}
32
33/// Get the directory where sockets are stored
34///
35/// Uses `XDG_RUNTIME_DIR` if available, otherwise falls back to /tmp/aether-lspd-{uid}
36fn 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
45/// Generate the socket filename from workspace and language
46fn 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    // Use first 8 bytes of SHA256 for a 16-char hex string
54    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/// Get the current user's UID
60#[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}