winx_code_agent/daemon/
socket.rs1#[cfg(unix)]
2use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};
3use std::path::{Path, PathBuf};
4
5#[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#[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#[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#[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#[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}