Skip to main content

winx_code_agent/daemon/
socket.rs

1use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};
2use std::path::{Path, PathBuf};
3
4/// One socket location that may contain a Winx control daemon.
5#[derive(Clone, Debug, Eq, PartialEq)]
6pub struct DaemonSocketCandidate {
7    pub path: PathBuf,
8    pub sources: Vec<&'static str>,
9    pub selected: bool,
10}
11
12fn socket_under(runtime_dir: &Path) -> PathBuf {
13    runtime_dir.join("winx/winxd.sock")
14}
15
16fn fallback_socket(uid: u32) -> PathBuf {
17    PathBuf::from(format!("/tmp/winx-{uid}/winxd.sock"))
18}
19
20fn canonical_user_runtime_dir(uid: u32) -> PathBuf {
21    PathBuf::from(format!("/run/user/{uid}"))
22}
23
24/// Only trust an implicit runtime directory owned exclusively by this user.
25fn usable_runtime_dir(path: &Path, uid: u32) -> bool {
26    let Ok(metadata) = std::fs::metadata(path) else { return false };
27    let mode = metadata.permissions().mode();
28    metadata.is_dir() && metadata.uid() == uid && mode.trailing_zeros() >= 6
29}
30
31fn select_socket_path(
32    explicit: Option<PathBuf>,
33    xdg_runtime_dir: Option<PathBuf>,
34    canonical_runtime_dir: Option<PathBuf>,
35    uid: u32,
36) -> PathBuf {
37    explicit
38        .or_else(|| xdg_runtime_dir.map(|path| socket_under(&path)))
39        .or_else(|| canonical_runtime_dir.map(|path| socket_under(&path)))
40        .unwrap_or_else(|| fallback_socket(uid))
41}
42
43/// Resolve the canonical control-daemon socket without creating filesystem state.
44///
45/// Launchers that do not inherit `XDG_RUNTIME_DIR` still converge on
46/// `/run/user/<uid>` when the private user runtime directory is available. This
47/// prevents one installation from silently creating independent daemons under
48/// both `/run/user` and `/tmp`.
49pub fn default_socket_path() -> PathBuf {
50    let uid = crate::os::unix::effective_uid();
51    let explicit =
52        std::env::var_os("WINX_SOCKET").filter(|value| !value.is_empty()).map(PathBuf::from);
53    let xdg_runtime_dir = std::env::var_os("XDG_RUNTIME_DIR")
54        .filter(|value| !value.is_empty())
55        .map(PathBuf::from)
56        .filter(|path| usable_runtime_dir(path, uid));
57    let canonical_runtime_dir = canonical_user_runtime_dir(uid);
58    let canonical_runtime_dir =
59        usable_runtime_dir(&canonical_runtime_dir, uid).then_some(canonical_runtime_dir);
60    select_socket_path(explicit, xdg_runtime_dir, canonical_runtime_dir, uid)
61}
62
63fn push_candidate(
64    candidates: &mut Vec<DaemonSocketCandidate>,
65    path: PathBuf,
66    source: &'static str,
67    selected: &Path,
68) {
69    if let Some(existing) = candidates.iter_mut().find(|candidate| candidate.path == path) {
70        if !existing.sources.contains(&source) {
71            existing.sources.push(source);
72        }
73        existing.selected |= existing.path == selected;
74        return;
75    }
76    candidates.push(DaemonSocketCandidate {
77        selected: path == selected,
78        path,
79        sources: vec![source],
80    });
81}
82
83/// Enumerate canonical and legacy socket locations for diagnostics.
84pub fn socket_candidates() -> Vec<DaemonSocketCandidate> {
85    let uid = crate::os::unix::effective_uid();
86    let selected = default_socket_path();
87    let mut candidates = Vec::with_capacity(4);
88
89    if let Some(path) = std::env::var_os("WINX_SOCKET").filter(|value| !value.is_empty()) {
90        push_candidate(&mut candidates, PathBuf::from(path), "WINX_SOCKET", &selected);
91    }
92    if let Some(path) = std::env::var_os("XDG_RUNTIME_DIR").filter(|value| !value.is_empty()) {
93        push_candidate(
94            &mut candidates,
95            socket_under(&PathBuf::from(path)),
96            "XDG_RUNTIME_DIR",
97            &selected,
98        );
99    }
100
101    push_candidate(
102        &mut candidates,
103        socket_under(&canonical_user_runtime_dir(uid)),
104        "canonical_user_runtime",
105        &selected,
106    );
107    push_candidate(&mut candidates, fallback_socket(uid), "legacy_tmp_fallback", &selected);
108    candidates
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn explicit_socket_always_wins() {
117        let selected = select_socket_path(
118            Some(PathBuf::from("/custom/winx.sock")),
119            Some(PathBuf::from("/xdg")),
120            Some(PathBuf::from("/run/user/42")),
121            42,
122        );
123        assert_eq!(selected, PathBuf::from("/custom/winx.sock"));
124    }
125
126    #[test]
127    fn missing_xdg_converges_on_canonical_user_runtime() {
128        let selected = select_socket_path(None, None, Some(PathBuf::from("/run/user/42")), 42);
129        assert_eq!(selected, PathBuf::from("/run/user/42/winx/winxd.sock"));
130    }
131
132    #[test]
133    fn tmp_is_only_the_last_resort() {
134        assert_eq!(
135            select_socket_path(None, None, None, 42),
136            PathBuf::from("/tmp/winx-42/winxd.sock")
137        );
138    }
139
140    #[test]
141    fn duplicate_candidate_paths_merge_their_sources() {
142        let selected = Path::new("/run/user/42/winx/winxd.sock");
143        let mut candidates = Vec::new();
144        push_candidate(&mut candidates, selected.to_path_buf(), "XDG_RUNTIME_DIR", selected);
145        push_candidate(&mut candidates, selected.to_path_buf(), "canonical_user_runtime", selected);
146        assert_eq!(candidates.len(), 1);
147        assert!(candidates[0].selected);
148        assert_eq!(candidates[0].sources.len(), 2);
149    }
150}