1use std::path::{Path, PathBuf};
28
29use crate::Result;
30
31#[cfg(unix)]
33const PRIMARY_LOCK_DIR: &str = "/var/lock";
34
35#[cfg(unix)]
38const FALLBACK_LOCK_DIR: &str = "/tmp";
39
40#[derive(Debug)]
43pub struct UucpLock {
44 path: PathBuf,
45 active: bool,
48}
49
50impl UucpLock {
51 pub fn acquire(device_path: &str) -> Result<Self> {
62 #[cfg(not(unix))]
63 {
64 let _ = device_path;
65 Ok(Self {
66 path: PathBuf::new(),
67 active: false,
68 })
69 }
70 #[cfg(unix)]
71 {
72 match Self::acquire_in(device_path, Path::new(PRIMARY_LOCK_DIR)) {
73 Ok(lock) => Ok(lock),
74 Err(crate::Error::Io(err)) if can_fallback(&err) => {
75 tracing::warn!(
76 primary = PRIMARY_LOCK_DIR,
77 fallback = FALLBACK_LOCK_DIR,
78 error = %err,
79 "UUCP lock falling back to per-user directory",
80 );
81 Self::acquire_in(device_path, Path::new(FALLBACK_LOCK_DIR))
82 }
83 Err(other) => Err(other),
84 }
85 }
86 }
87
88 #[cfg_attr(not(unix), allow(unused_variables))]
97 pub fn acquire_in(device_path: &str, lock_dir: &Path) -> Result<Self> {
98 #[cfg(not(unix))]
99 {
100 Ok(Self {
101 path: PathBuf::new(),
102 active: false,
103 })
104 }
105 #[cfg(unix)]
106 {
107 use std::fs::{self, OpenOptions};
108 use std::io::Write;
109
110 let basename = basename_of(device_path);
111 let lock_path = lock_dir.join(format!("LCK..{basename}"));
112
113 if lock_path.exists() {
115 match read_pid(&lock_path) {
116 Ok(pid) if pid_is_alive(pid) => {
117 return Err(crate::Error::AlreadyLocked {
118 device: device_path.to_string(),
119 pid,
120 lock_file: lock_path,
121 });
122 }
123 _ => {
128 let _ = fs::remove_file(&lock_path);
129 }
130 }
131 }
132
133 let mut f = OpenOptions::new()
136 .write(true)
137 .create_new(true)
138 .open(&lock_path)?;
139
140 #[allow(clippy::cast_possible_wrap)]
142 let pid = std::process::id() as i32;
143 writeln!(f, "{pid:>10}")?;
144 f.sync_all()?;
145
146 Ok(Self {
147 path: lock_path,
148 active: true,
149 })
150 }
151 }
152
153 #[must_use]
155 pub fn lock_file_path(&self) -> &Path {
156 &self.path
157 }
158}
159
160impl Drop for UucpLock {
161 fn drop(&mut self) {
162 if self.active {
163 let _ = std::fs::remove_file(&self.path);
164 }
165 }
166}
167
168#[cfg(unix)]
171fn basename_of(path: &str) -> &str {
172 path.rsplit('/').next().unwrap_or(path)
173}
174
175#[cfg(unix)]
177fn read_pid(lock_path: &Path) -> Result<i32> {
178 let content = std::fs::read_to_string(lock_path)?;
179 content
180 .trim()
181 .parse::<i32>()
182 .map_err(|err| crate::Error::InvalidLock(format!("{err} in {content:?}")))
183}
184
185#[cfg(unix)]
189fn pid_is_alive(pid: i32) -> bool {
190 use nix::sys::signal::kill;
191 use nix::unistd::Pid;
192 matches!(kill(Pid::from_raw(pid), None), Ok(()))
193}
194
195#[cfg(unix)]
200fn can_fallback(err: &std::io::Error) -> bool {
201 matches!(
202 err.kind(),
203 std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::NotFound
204 )
205}
206
207#[cfg(test)]
208#[cfg(unix)]
209mod tests {
210 use super::*;
211 use std::fs;
212 use tempfile::tempdir;
213
214 #[test]
215 fn basename_strips_directory_components() {
216 assert_eq!(basename_of("/dev/ttyUSB0"), "ttyUSB0");
217 assert_eq!(basename_of("ttyS1"), "ttyS1");
218 assert_eq!(basename_of("/a/b/c/d"), "d");
219 }
220
221 #[test]
222 fn acquire_in_creates_lock_file_at_uucp_path() {
223 let dir = tempdir().unwrap();
224 let lock = UucpLock::acquire_in("/dev/ttyUSB0", dir.path()).unwrap();
225 let expected = dir.path().join("LCK..ttyUSB0");
226 assert_eq!(lock.lock_file_path(), &expected);
227 assert!(expected.exists(), "lock file should exist on disk");
228 }
229
230 #[test]
231 fn acquire_in_writes_pid_in_uucp_format() {
232 let dir = tempdir().unwrap();
233 let _lock = UucpLock::acquire_in("/dev/ttyUSB0", dir.path()).unwrap();
234 let content = fs::read_to_string(dir.path().join("LCK..ttyUSB0")).unwrap();
235 assert_eq!(content.len(), 11, "expected 10 PID chars + LF: {content:?}");
237 assert!(content.ends_with('\n'), "trailing LF: {content:?}");
238 let parsed: i32 = content.trim().parse().expect("decimal PID");
239 #[allow(clippy::cast_possible_wrap)]
240 let our_pid = std::process::id() as i32;
241 assert_eq!(parsed, our_pid);
242 }
243
244 #[test]
245 fn drop_removes_lock_file() {
246 let dir = tempdir().unwrap();
247 let path = {
248 let lock = UucpLock::acquire_in("/dev/ttyUSB0", dir.path()).unwrap();
249 lock.lock_file_path().to_path_buf()
250 };
251 assert!(!path.exists(), "lock file should be removed on Drop");
252 }
253
254 #[test]
255 fn second_acquire_for_same_device_reports_already_locked() {
256 let dir = tempdir().unwrap();
257 let _first = UucpLock::acquire_in("/dev/ttyUSB0", dir.path()).unwrap();
258 let err = UucpLock::acquire_in("/dev/ttyUSB0", dir.path()).unwrap_err();
259 match err {
260 crate::Error::AlreadyLocked { device, pid, .. } => {
261 assert_eq!(device, "/dev/ttyUSB0");
262 #[allow(clippy::cast_possible_wrap)]
263 let our_pid = std::process::id() as i32;
264 assert_eq!(pid, our_pid);
265 }
266 other => panic!("expected AlreadyLocked, got {other:?}"),
267 }
268 }
269
270 #[test]
271 fn stale_lock_with_dead_pid_is_overwritten() {
272 let dir = tempdir().unwrap();
273 let lock_path = dir.path().join("LCK..ttyUSB0");
274 fs::write(&lock_path, "1999999999\n").unwrap();
277
278 let lock = UucpLock::acquire_in("/dev/ttyUSB0", dir.path()).unwrap();
279 let content = fs::read_to_string(lock.lock_file_path()).unwrap();
280 let parsed: i32 = content.trim().parse().unwrap();
281 #[allow(clippy::cast_possible_wrap)]
282 let our_pid = std::process::id() as i32;
283 assert_eq!(parsed, our_pid);
284 }
285
286 #[test]
287 fn stale_lock_with_garbage_content_is_overwritten() {
288 let dir = tempdir().unwrap();
289 let lock_path = dir.path().join("LCK..ttyUSB0");
290 fs::write(&lock_path, "not-a-pid\n").unwrap();
291
292 let lock = UucpLock::acquire_in("/dev/ttyUSB0", dir.path()).unwrap();
293 #[allow(clippy::cast_possible_wrap)]
295 let our_pid = std::process::id() as i32;
296 let content = fs::read_to_string(lock.lock_file_path()).unwrap();
297 assert_eq!(content.trim().parse::<i32>().unwrap(), our_pid);
298 }
299
300 #[test]
301 fn unrelated_lock_files_are_left_alone() {
302 let dir = tempdir().unwrap();
303 let other = dir.path().join("LCK..ttyS9");
304 fs::write(&other, "1\n").unwrap();
305 let _lock = UucpLock::acquire_in("/dev/ttyUSB0", dir.path()).unwrap();
306 assert!(other.exists(), "we must not touch unrelated lock files");
307 }
308}