winx_code_agent/daemon/
socket.rs1use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};
2use std::path::{Path, PathBuf};
3
4#[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
24fn 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
43pub 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
83pub 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}