Skip to main content

winx_code_agent/daemon/
socket.rs

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