1use std::fs::{File, OpenOptions};
8use std::path::PathBuf;
9use std::time::{Duration, Instant};
10
11use crate::error::{Error, Result};
12
13#[derive(Debug)]
15pub struct NameLock {
16 _file: File,
17 pub path: PathBuf,
18}
19
20fn lock_dir(project: &str) -> PathBuf {
21 let base = std::env::var_os("XDG_RUNTIME_DIR")
22 .filter(|s| !s.is_empty())
23 .map(PathBuf::from)
24 .unwrap_or_else(|| {
25 std::env::temp_dir().join(format!("isb-{}", rustix::process::getuid().as_raw()))
26 });
27 base.join("isb").join(project)
28}
29
30impl NameLock {
31 pub fn acquire(
34 project: &str,
35 name: &str,
36 wait: Duration,
37 on_wait: &mut dyn FnMut(&PathBuf),
38 ) -> Result<NameLock> {
39 let dir = lock_dir(project);
40 std::fs::create_dir_all(&dir)?;
41 let path = dir.join(format!("{name}.lock"));
42 let file = OpenOptions::new()
43 .create(true)
44 .truncate(false)
45 .write(true)
46 .open(&path)?;
47 let started = Instant::now();
48 let mut told = false;
49 loop {
50 match rustix::fs::flock(&file, rustix::fs::FlockOperation::NonBlockingLockExclusive) {
51 Ok(()) => return Ok(NameLock { _file: file, path }),
52 Err(rustix::io::Errno::WOULDBLOCK) | Err(rustix::io::Errno::INTR) => {}
53 Err(e) => return Err(Error::Io(e.into())),
54 }
55 if !told {
56 on_wait(&path);
57 told = true;
58 }
59 if started.elapsed() >= wait {
60 return Err(Error::invalid(format!(
61 "timed out after {wait:?} waiting for the lock on {name} ({}) held by another isb",
62 path.display()
63 )));
64 }
65 std::thread::sleep(Duration::from_millis(100));
66 }
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn second_holder_waits_then_times_out() {
76 let dir = tempfile::tempdir().unwrap();
77 unsafe { std::env::set_var("XDG_RUNTIME_DIR", dir.path()) };
79 let a = NameLock::acquire("p", "n", Duration::from_secs(1), &mut |_| {}).unwrap();
80 let mut waited = false;
81 let b = NameLock::acquire("p", "n", Duration::from_millis(300), &mut |_| waited = true);
82 assert!(b.is_err());
83 assert!(waited);
84 drop(a);
85 NameLock::acquire("p", "n", Duration::from_secs(1), &mut |_| {}).unwrap();
86 }
87
88 #[test]
89 fn lock_fd_is_cloexec() {
90 let dir = tempfile::tempdir().unwrap();
91 let f = OpenOptions::new()
92 .create(true)
93 .truncate(false)
94 .write(true)
95 .open(dir.path().join("x"))
96 .unwrap();
97 let flags = rustix::io::fcntl_getfd(&f).unwrap();
98 assert!(flags.contains(rustix::io::FdFlags::CLOEXEC));
99 }
100}