Skip to main content

aether_lspd/
socket_path.rs

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