1use crate::ProcessLock;
3use std::{
4 io,
5 os::unix::{
6 fs::{FileTypeExt, MetadataExt, PermissionsExt},
7 net::UnixListener,
8 },
9 path::{Path, PathBuf},
10 time::Duration,
11};
12
13pub use super::Bound;
14#[derive(Debug)]
16#[non_exhaustive]
17pub enum BindError {
18 Directory(io::Error),
20 Bind(io::Error),
22 Unlink(io::Error),
24}
25pub const MAX_PATH_BYTES: usize = match crate::socket_path::MAX_SOCKET_PATH_BYTES {
28 Some(limit) => limit,
29 None => unreachable!(),
30};
31
32pub fn check_path(socket: &Path) -> io::Result<()> {
39 crate::socket_path::check_socket_path(socket)
40}
41fn lock_path(path: &Path) -> PathBuf {
42 let mut path = path.as_os_str().to_owned();
43 path.push(".bind.lock");
44 path.into()
45}
46pub use crate::ensure_private_dir;
48fn bind_private(socket: &Path) -> io::Result<UnixListener> {
49 let listener = crate::spawn_lock::without_spawns(|| UnixListener::bind(socket))?;
51 std::fs::set_permissions(socket, std::fs::Permissions::from_mode(0o600))?;
52 Ok(listener)
53}
54pub fn acquire(socket: &Path) -> Result<Bound<UnixListener>, BindError> {
70 check_path(socket).map_err(BindError::Bind)?;
73 ensure_private_dir(
74 socket
75 .parent()
76 .ok_or_else(|| BindError::Directory(io::ErrorKind::InvalidInput.into()))?,
77 )
78 .map_err(BindError::Directory)?;
79 let Some(_lock) = ProcessLock::try_acquire(lock_path(socket)).map_err(BindError::Bind)? else {
80 return Ok(Bound::AlreadyRunning);
81 };
82 match bind_private(socket) {
83 Ok(listener) => return Ok(Bound::Won(listener)),
84 Err(error) if error.kind() == io::ErrorKind::AddrInUse => {}
85 Err(error) => return Err(BindError::Bind(error)),
86 }
87 let metadata = std::fs::symlink_metadata(socket).map_err(BindError::Bind)?;
88 if !metadata.file_type().is_socket() {
89 return Err(BindError::Bind(io::ErrorKind::AlreadyExists.into()));
90 }
91 let probe = socket2::Socket::new(socket2::Domain::UNIX, socket2::Type::STREAM, None)
92 .map_err(BindError::Bind)?;
93 let address = socket2::SockAddr::unix(socket).map_err(BindError::Bind)?;
94 match probe.connect_timeout(&address, Duration::from_millis(250)) {
95 Ok(()) => return Ok(Bound::AlreadyRunning),
96 Err(error)
97 if matches!(
98 error.kind(),
99 io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock
100 ) =>
101 {
102 return Ok(Bound::AlreadyRunning);
103 }
104 Err(error) if error.kind() == io::ErrorKind::ConnectionRefused => {}
105 Err(error) => return Err(BindError::Bind(error)),
106 }
107 std::fs::remove_file(socket).map_err(BindError::Unlink)?;
108 bind_private(socket)
109 .map(Bound::Won)
110 .map_err(BindError::Bind)
111}
112
113pub struct SocketGuard {
116 path: PathBuf,
117 device: u64,
118 inode: u64,
119}
120impl SocketGuard {
121 pub fn capture(path: &Path) -> io::Result<Self> {
123 let metadata = std::fs::symlink_metadata(path)?;
124 if !metadata.file_type().is_socket() {
125 return Err(io::ErrorKind::InvalidInput.into());
126 }
127 Ok(Self {
128 path: path.to_owned(),
129 device: metadata.dev(),
130 inode: metadata.ino(),
131 })
132 }
133}
134impl Drop for SocketGuard {
135 fn drop(&mut self) {
136 let Ok(Some(_lock)) = ProcessLock::try_acquire(lock_path(&self.path)) else {
137 return;
138 };
139 if std::fs::symlink_metadata(&self.path).is_ok_and(|metadata| {
140 metadata.file_type().is_socket()
141 && metadata.dev() == self.device
142 && metadata.ino() == self.inode
143 }) {
144 let _ = std::fs::remove_file(&self.path);
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use std::io::Write;
153
154 fn temp_dir(name: &str) -> std::path::PathBuf {
155 let mut p = std::env::temp_dir();
156 p.push(format!("daemon-single-{}-{name}", std::process::id()));
157 let _ = std::fs::remove_dir_all(&p);
158 p
159 }
160
161 #[test]
162 fn a_path_fits_up_to_the_limit_and_not_one_byte_more() {
163 let fits = format!("/{}", "a".repeat(MAX_PATH_BYTES - 1));
164 assert_eq!(fits.len(), MAX_PATH_BYTES);
165 check_path(Path::new(&fits)).unwrap();
166
167 let error = check_path(Path::new(&format!("{fits}b"))).unwrap_err();
168 assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
169 let message = error.to_string();
170 assert!(
171 message.contains(&format!(
172 "{} bytes, but a Unix socket path can be at most {MAX_PATH_BYTES}",
173 MAX_PATH_BYTES + 1
174 )),
175 "{message}"
176 );
177 }
178
179 #[test]
180 fn the_limit_matches_this_platform_socket_address() {
181 let expected = if cfg!(any(target_os = "linux", target_os = "android")) {
182 107
183 } else {
184 103
185 };
186 assert_eq!(MAX_PATH_BYTES, expected);
187 }
188
189 #[test]
190 fn the_limit_is_the_one_the_os_enforces() {
191 let dir = temp_dir("limit");
192 std::fs::create_dir_all(&dir).unwrap();
193 let prefix = dir.join("s").as_os_str().len();
194 let at_limit = dir.join(format!("s{}", "a".repeat(MAX_PATH_BYTES - prefix)));
196 assert_eq!(at_limit.as_os_str().len(), MAX_PATH_BYTES);
197 UnixListener::bind(&at_limit).expect("the limit itself binds");
198 let past = dir.join(format!("s{}", "a".repeat(MAX_PATH_BYTES - prefix + 1)));
199 assert!(
200 UnixListener::bind(&past).is_err(),
201 "one byte past the limit must not bind"
202 );
203 let _ = std::fs::remove_dir_all(&dir);
204 }
205
206 #[test]
207 fn an_overlong_path_is_refused_before_anything_is_created() {
208 let dir = temp_dir("overlong");
209 let sock = dir.join("a".repeat(MAX_PATH_BYTES));
210 match acquire(&sock) {
211 Err(BindError::Bind(error)) => {
212 assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
213 assert!(error.to_string().contains("at most"), "{error}");
214 }
215 other => panic!("expected a bind error, got {other:?}"),
216 }
217 assert!(
218 !dir.exists(),
219 "no directory may be created for a path that cannot bind"
220 );
221 }
222
223 #[test]
224 fn the_first_binder_wins() {
225 let dir = temp_dir("first");
226 let sock = dir.join("b.sock");
227 match acquire(&sock).unwrap() {
228 Bound::Won(_l) => {}
229 other => panic!("expected to win, got {other:?}"),
230 }
231 let _ = std::fs::remove_dir_all(&dir);
232 }
233
234 #[test]
235 fn a_second_binder_stands_down_while_the_first_is_listening() {
236 let dir = temp_dir("second");
237 let sock = dir.join("b.sock");
238 let _first = match acquire(&sock).unwrap() {
239 Bound::Won(l) => l,
240 other => panic!("first should win, got {other:?}"),
241 };
242
243 match acquire(&sock).unwrap() {
244 Bound::AlreadyRunning => {}
245 other => panic!("second should stand down, got {other:?}"),
246 }
247
248 let _ = std::fs::remove_dir_all(&dir);
249 }
250
251 #[test]
252 fn a_stale_socket_left_by_a_dead_broker_is_reclaimed() {
253 let dir = temp_dir("stale");
257 std::fs::create_dir_all(&dir).unwrap();
258 let sock = dir.join("b.sock");
259
260 {
267 let never_listened =
268 socket2::Socket::new(socket2::Domain::UNIX, socket2::Type::STREAM, None).unwrap();
269 never_listened
270 .bind(&socket2::SockAddr::unix(&sock).unwrap())
271 .unwrap();
272 }
273 assert!(sock.exists(), "fixture should leave the inode behind");
274
275 match acquire(&sock).unwrap() {
276 Bound::Won(_l) => {}
277 other => panic!("a stale socket should be reclaimed, got {other:?}"),
278 }
279
280 let _ = std::fs::remove_dir_all(&dir);
281 }
282
283 #[test]
284 fn a_live_broker_is_never_unlinked() {
285 let dir = temp_dir("nounlink");
288 let sock = dir.join("b.sock");
289 let _first = match acquire(&sock).unwrap() {
290 Bound::Won(l) => l,
291 other => panic!("{other:?}"),
292 };
293
294 let before = std::fs::metadata(&sock).unwrap();
295 let _ = acquire(&sock).unwrap();
296 let after = std::fs::metadata(&sock).unwrap();
297
298 use std::os::unix::fs::MetadataExt;
299 assert_eq!(
300 before.ino(),
301 after.ino(),
302 "the live broker's socket was replaced"
303 );
304
305 let _ = std::fs::remove_dir_all(&dir);
306 }
307
308 #[test]
309 fn a_regular_file_at_the_socket_path_is_preserved() {
310 let dir = temp_dir("regular");
312 std::fs::create_dir_all(&dir).unwrap();
313 let sock = dir.join("b.sock");
314 let mut f = std::fs::File::create(&sock).unwrap();
315 f.write_all(b"not a socket").unwrap();
316 drop(f);
317
318 assert!(acquire(&sock).is_err());
319 assert_eq!(std::fs::read(&sock).unwrap(), b"not a socket");
320
321 let _ = std::fs::remove_dir_all(&dir);
322 }
323
324 #[test]
325 fn the_socket_is_owner_only() {
326 let dir = temp_dir("perms");
327 let sock = dir.join("b.sock");
328 let _l = match acquire(&sock).unwrap() {
329 Bound::Won(l) => l,
330 other => panic!("{other:?}"),
331 };
332
333 let mode = std::fs::metadata(&sock).unwrap().permissions().mode() & 0o777;
334 assert_eq!(mode, 0o600, "socket is 0o{mode:o}, expected 0o600");
335
336 let _ = std::fs::remove_dir_all(&dir);
337 }
338
339 #[test]
340 fn the_parent_directory_is_owner_only_even_if_it_already_existed() {
341 let dir = temp_dir("dirperms");
345 std::fs::create_dir_all(&dir).unwrap();
346 std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).unwrap();
347
348 let sock = dir.join("b.sock");
349 let _l = acquire(&sock).unwrap();
350
351 let mode = std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777;
352 assert_eq!(mode, 0o700, "directory left at 0o{mode:o}");
353
354 let _ = std::fs::remove_dir_all(&dir);
355 }
356
357 #[test]
358 fn many_concurrent_acquirers_produce_exactly_one_winner() {
359 use std::sync::Arc;
362 use std::sync::atomic::{AtomicUsize, Ordering};
363
364 let dir = temp_dir("herd");
365 std::fs::create_dir_all(&dir).unwrap();
366 let sock = Arc::new(dir.join("b.sock"));
367
368 let won = Arc::new(AtomicUsize::new(0));
369 let stood_down = Arc::new(AtomicUsize::new(0));
370 let failed = Arc::new(AtomicUsize::new(0));
371
372 let acquired = Arc::new(std::sync::Barrier::new(15));
373 let handles: Vec<_> = (0..15)
374 .map(|_| {
375 let sock = Arc::clone(&sock);
376 let won = Arc::clone(&won);
377 let stood_down = Arc::clone(&stood_down);
378 let failed = Arc::clone(&failed);
379 let acquired = Arc::clone(&acquired);
380 std::thread::spawn(move || {
381 let outcome = acquire(&sock);
382 match &outcome {
383 Ok(Bound::Won(_)) => {
384 won.fetch_add(1, Ordering::SeqCst);
385 }
386 Ok(Bound::AlreadyRunning) => {
387 stood_down.fetch_add(1, Ordering::SeqCst);
388 }
389 Err(_) => {
390 failed.fetch_add(1, Ordering::SeqCst);
391 }
392 }
393 acquired.wait();
396 drop(outcome);
397 })
398 })
399 .collect();
400 for h in handles {
401 h.join().unwrap();
402 }
403
404 assert_eq!(failed.load(Ordering::SeqCst), 0, "some acquirers errored");
405 assert_eq!(
406 won.load(Ordering::SeqCst),
407 1,
408 "expected exactly one winner, got {} (stood down: {})",
409 won.load(Ordering::SeqCst),
410 stood_down.load(Ordering::SeqCst)
411 );
412 assert_eq!(stood_down.load(Ordering::SeqCst), 14);
413
414 let _ = std::fs::remove_dir_all(&dir);
415 }
416}