1#[cfg(all(not(unix), not(test)))]
6mod api {
7 compile_error!("unsupported platform");
8}
9
10#[cfg(all(unix, not(test)))]
11mod api {
12 use std::io;
13 pub use std::{
14 fs::{File, remove_file},
15 process::id as pid,
16 };
17
18 pub use nix::errno::Errno;
19
20 pub type Flock = nix::fcntl::Flock<File>;
21
22 pub fn open<P: AsRef<std::path::Path>>(path: P) -> io::Result<File> {
23 std::fs::OpenOptions::new()
24 .read(true)
25 .write(true)
26 .create(true)
27 .open(path)
28 }
29 pub fn lock_exclusive(file: File) -> Result<Flock, (File, Errno)> {
30 Flock::lock(file, nix::fcntl::FlockArg::LockExclusiveNonblock)
31 }
32}
33
34#[cfg(test)]
36mod api {
37 use std::{
38 cell::RefCell,
39 io,
40 ops::{Deref, DerefMut},
41 path::Path,
42 };
43
44 pub struct File {
45 data: io::Cursor<Vec<u8>>,
46 }
47 impl File {
48 fn new(data: Vec<u8>) -> Self {
49 Self {
50 data: io::Cursor::new(data),
51 }
52 }
53 pub fn set_len(&mut self, len: usize) -> io::Result<()> {
54 self.data.get_mut().truncate(len);
55 Ok(())
56 }
57 pub fn sync_all(&mut self) -> io::Result<()> {
58 Ok(())
59 }
60 }
61 impl Deref for File {
62 type Target = io::Cursor<Vec<u8>>;
63 fn deref(&self) -> &Self::Target {
64 &self.data
65 }
66 }
67 impl DerefMut for File {
68 fn deref_mut(&mut self) -> &mut Self::Target {
69 &mut self.data
70 }
71 }
72
73 pub struct Flock {
74 file: File,
75 }
76 impl Deref for Flock {
77 type Target = File;
78 fn deref(&self) -> &Self::Target {
79 &self.file
80 }
81 }
82 impl DerefMut for Flock {
83 fn deref_mut(&mut self) -> &mut Self::Target {
84 &mut self.file
85 }
86 }
87
88 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
89 #[repr(i32)]
90 #[non_exhaustive]
91 pub enum Errno {
92 EWOULDBLOCK = 11,
93 EACCES = 13,
94 }
95
96 thread_local! {
97 static PID: RefCell<u32> = RefCell::new(0);
98 static OPEN: RefCell<io::Result<Vec<u8>>> = RefCell::new(Ok(Vec::new()));
99 static LOCK: RefCell<Option<Errno>> = RefCell::new(None);
100 }
101 pub fn mock(current_pid: u32, lockfile_data: io::Result<Vec<u8>>, lock_errno: Option<Errno>) {
102 PID.set(current_pid);
103 OPEN.set(lockfile_data);
104 LOCK.set(lock_errno);
105 }
106 pub fn pid() -> u32 {
107 PID.with(|v| *v.borrow())
108 }
109 pub fn open<P: AsRef<Path>>(_: P) -> io::Result<File> {
110 Ok(File::new(OPEN.replace(Ok(Vec::new()))?))
111 }
112 pub fn lock_exclusive(file: File) -> Result<Flock, (File, Errno)> {
113 match LOCK.take() {
114 None => Ok(Flock { file }),
115 Some(errno) => Err((file, errno)),
116 }
117 }
118 pub fn remove_file<P: AsRef<Path>>(_: P) -> io::Result<()> {
119 Ok(())
120 }
121}
122
123use std::{
124 borrow::Cow,
125 fmt, io,
126 io::{Read, Seek, Write},
127 marker::PhantomData,
128 mem::ManuallyDrop,
129 ops::Deref,
130 path::{Path, PathBuf},
131};
132
133use daemonbit_core::{Acquire, DaemonScope, ScopeKey, Scoped, TryAcquire};
134use daemonbit_rundir::{RuntimeDirectory, ScopedRuntimeDirectory};
135use tracing::warn;
136
137pub struct Lockfile<T> {
138 raw: RawLockfile,
139 _scope: PhantomData<fn(T) -> T>,
140}
141impl<T: DaemonScope> Scoped for Lockfile<T> {
142 fn scope(&self) -> ScopeKey {
143 T::key()
144 }
145}
146impl<T: DaemonScope> Acquire<T> for Lockfile<T> {
147 fn acquire() -> Self {
148 match RuntimeDirectory::<T>::get() {
149 Ok(rundir) => {
150 let raw = RawLockfile::acquire_inner(rundir.join("lock"), Some(rundir.into()));
151 Self {
152 raw,
153 _scope: PhantomData,
154 }
155 }
156 Err((path, source)) => {
157 let path = path.join("lock");
158 panic!("{}", RawLockfileError::AccessFailed { path, source });
159 }
160 }
161 }
162}
163
164impl<T: DaemonScope> TryAcquire<T> for Lockfile<T> {
165 type Error = LockfileError;
166 fn try_acquire() -> Result<Self, Self::Error> {
167 match RuntimeDirectory::<T>::get() {
168 Ok(rundir) => {
169 match RawLockfile::try_acquire_inner(rundir.join("lock"), Some(rundir.into())) {
170 Ok(raw) => Ok(Self {
171 raw,
172 _scope: PhantomData,
173 }),
174 Err(e) => {
175 let scope = T::key();
176 Err(match e {
177 RawLockfileError::AccessFailed { path, source } => {
178 LockfileError::AccessFailed {
179 scope,
180 path,
181 source,
182 }
183 }
184 RawLockfileError::AlreadyLocked { path, pid } => {
185 LockfileError::AlreadyLocked { scope, path, pid }
186 }
187 })
188 }
189 }
190 }
191 Err((path, source)) => {
192 let scope = T::key();
193 let path = path.join("lock");
194 Err(LockfileError::AccessFailed {
195 scope,
196 path,
197 source,
198 })
199 }
200 }
201 }
202}
203impl<T: DaemonScope> Deref for Lockfile<T> {
204 type Target = RawLockfile;
205 fn deref(&self) -> &Self::Target {
206 &self.raw
207 }
208}
209
210pub struct RawLockfile {
211 pid: u32,
212 path: PathBuf,
213 flock: ManuallyDrop<api::Flock>,
214 rundir: Option<ScopedRuntimeDirectory>,
215}
216impl RawLockfile {
217 pub fn acquire_with_path<P: AsRef<Path>>(path: P) -> Self {
218 Self::acquire_inner(path, None)
219 }
220 pub fn try_acquire_with_path<P: AsRef<Path>>(path: P) -> Result<Self, RawLockfileError> {
221 Self::try_acquire_inner(path, None)
222 }
223 fn acquire_inner<P: AsRef<Path>>(path: P, rundir: Option<ScopedRuntimeDirectory>) -> Self {
224 match Self::try_acquire_inner(path, rundir) {
225 Ok(lockfile) => lockfile,
226 Err(e) => panic!("{e}"),
227 }
228 }
229 fn try_acquire_inner<P: AsRef<Path>>(
230 path: P,
231 rundir: Option<ScopedRuntimeDirectory>,
232 ) -> Result<Self, RawLockfileError> {
233 let path = path.as_ref().to_path_buf();
234
235 let file = match api::open(&path) {
236 Ok(file) => file,
237 Err(source) => return Err(RawLockfileError::AccessFailed { path, source }),
238 };
239
240 match api::lock_exclusive(file) {
241 Ok(mut flock) => {
242 let pid = api::pid();
243 let path = UnparsedPid::read(path, &mut *flock)?
244 .parse_checked(pid)
245 .write(pid, &mut *flock)?;
246 let flock = ManuallyDrop::new(flock);
247 Ok(RawLockfile {
248 pid,
249 path,
250 flock,
251 rundir,
252 })
253 }
254 Err((mut file, api::Errno::EWOULDBLOCK)) => Err(UnparsedPid::read(path, &mut file)?
255 .parse()
256 .into_already_locked_error()),
257 Err((_, errno)) => {
258 #[allow(clippy::unnecessary_cast)]
259 let source = io::Error::from_raw_os_error(errno as i32);
260 Err(RawLockfileError::AccessFailed { path, source })
261 }
262 }
263 }
264 pub fn pid(&self) -> u32 {
265 self.pid
266 }
267 pub fn path(&self) -> &Path {
268 &self.path
269 }
270}
271impl fmt::Debug for RawLockfile {
272 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273 f.debug_struct("Lockfile")
274 .field("pid", &self.pid)
275 .field("path", &self.path)
276 .finish()
277 }
278}
279impl Drop for RawLockfile {
280 fn drop(&mut self) {
281 if let Err(error) = api::remove_file(&self.path) {
282 warn!(?error, "cannot remove lockfile")
283 }
284 unsafe { ManuallyDrop::drop(&mut self.flock) };
285 self.rundir.take();
286 }
287}
288
289#[derive(Debug)]
290struct ParsedPid {
291 path: PathBuf,
292 pid: Option<u32>,
293}
294impl ParsedPid {
295 fn parse(path: PathBuf, input: &[u8]) -> Result<Self, (PathBuf, io::Error)> {
296 if input.is_empty() {
297 return Ok(Self { path, pid: None });
298 }
299
300 let result = match input.iter().all(u8::is_ascii_digit) {
301 true => Ok(input),
302 false => Err(io::Error::new(io::ErrorKind::InvalidData, "digit expected")),
303 }
304 .and_then(|input| std::str::from_utf8(input).or_invalid_data())
305 .and_then(|s| s.parse::<u32>().or_invalid_data());
306
307 match result {
308 Ok(pid) => Ok(Self {
309 path,
310 pid: Some(pid),
311 }),
312 Err(source) => Err((path, source)),
313 }
314 }
315 fn write(self, pid: u32, output: &mut api::File) -> Result<PathBuf, RawLockfileError> {
316 match output
317 .rewind()
318 .and_then(|_| output.set_len(0))
319 .and_then(|_| {
320 let pid = pid.to_string();
321 output.write_all(pid.as_bytes())
322 })
323 .and_then(|_| output.sync_all())
324 {
325 Ok(_) => Ok(self.path),
326 Err(source) => Err(RawLockfileError::AccessFailed {
327 path: self.path,
328 source,
329 }),
330 }
331 }
332 fn into_already_locked_error(self) -> RawLockfileError {
333 RawLockfileError::AlreadyLocked {
334 path: self.path,
335 pid: self.pid,
336 }
337 }
338}
339
340struct UnparsedPid {
341 path: PathBuf,
342 data: Vec<u8>,
343}
344impl UnparsedPid {
345 fn read(path: PathBuf, input: &mut api::File) -> Result<Self, RawLockfileError> {
346 match input.rewind().and_then(|_| {
347 let mut data = Vec::<u8>::with_capacity(10);
348 input.read_to_end(&mut data).map(|_| data)
349 }) {
350 Ok(data) => Ok(Self { path, data }),
351 Err(source) => Err(RawLockfileError::AccessFailed { path, source }),
352 }
353 }
354 fn parse_checked(self, current_pid: u32) -> ParsedPid {
355 match ParsedPid::parse(self.path, self.data.trim_ascii()) {
356 Ok(parsed) => {
357 match parsed.pid {
358 Some(written_pid) if written_pid != current_pid => {
359 warn!(written_pid, current_pid, lockfile = %parsed.path.display(), "encountered stale lockfile");
360 }
361 _ => {}
362 }
363 parsed
364 }
365 Err((path, error)) => {
366 warn!(current_pid, lockfile = %path.display(), %error, "encountered invalid lockfile");
367 ParsedPid { path, pid: None }
368 }
369 }
370 }
371 fn parse(self) -> ParsedPid {
372 ParsedPid::parse(self.path, self.data.trim_ascii())
373 .unwrap_or_else(|(path, _)| ParsedPid { path, pid: None })
374 }
375}
376
377trait OrInvalidData: Sized {
378 type Output;
379 fn or_invalid_data(self) -> io::Result<Self::Output>;
380}
381impl<T, E> OrInvalidData for Result<T, E>
382where
383 E: std::error::Error + Send + Sync + 'static,
384{
385 type Output = T;
386 fn or_invalid_data(self) -> io::Result<T> {
387 match self {
388 Ok(v) => Ok(v),
389 Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e)),
390 }
391 }
392}
393
394#[derive(Debug)]
395pub enum LockfileError {
396 AccessFailed {
397 scope: ScopeKey,
398 path: PathBuf,
399 source: io::Error,
400 },
401 AlreadyLocked {
402 scope: ScopeKey,
403 path: PathBuf,
404 pid: Option<u32>,
405 },
406}
407impl Scoped for LockfileError {
408 fn scope(&self) -> ScopeKey {
409 match *self {
410 Self::AccessFailed { scope, .. } => scope,
411 Self::AlreadyLocked { scope, .. } => scope,
412 }
413 }
414}
415impl LockfileError {
416 pub fn path(&self) -> &Path {
417 use LockfileError::*;
418 let (AccessFailed { path, .. } | AlreadyLocked { path, .. }) = self;
419 path
420 }
421 pub fn into_path(self) -> PathBuf {
422 use LockfileError::*;
423 let (AccessFailed { path, .. } | AlreadyLocked { path, .. }) = self;
424 path
425 }
426}
427impl fmt::Display for LockfileError {
428 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
429 match *self {
430 Self::AccessFailed {
431 ref path,
432 ref source,
433 ..
434 } => write!(f, "access failed: {}: {source}", path.display()),
435 Self::AlreadyLocked { ref path, pid, .. } => {
436 let pid = match pid {
437 Some(pid) => Cow::Owned(pid.to_string()),
438 None => Cow::Borrowed("unknown"),
439 };
440 write!(f, "already locked: {}: {pid}", path.display())
441 }
442 }
443 }
444}
445impl std::error::Error for LockfileError {}
446
447#[derive(Debug)]
448pub enum RawLockfileError {
449 AccessFailed { path: PathBuf, source: io::Error },
450 AlreadyLocked { path: PathBuf, pid: Option<u32> },
451}
452impl RawLockfileError {
453 pub fn path(&self) -> &Path {
454 use RawLockfileError::*;
455 let (AccessFailed { path, .. } | AlreadyLocked { path, .. }) = self;
456 path
457 }
458 pub fn into_path(self) -> PathBuf {
459 use RawLockfileError::*;
460 let (AccessFailed { path, .. } | AlreadyLocked { path, .. }) = self;
461 path
462 }
463}
464impl fmt::Display for RawLockfileError {
465 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
466 match *self {
467 Self::AccessFailed {
468 ref path,
469 ref source,
470 } => write!(f, "access failed: {}: {source}", path.display()),
471 Self::AlreadyLocked { ref path, pid } => {
472 let pid = match pid {
473 Some(pid) => Cow::Owned(pid.to_string()),
474 None => Cow::Borrowed("unknown"),
475 };
476 write!(f, "already locked: {}: {pid}", path.display())
477 }
478 }
479 }
480}
481impl std::error::Error for RawLockfileError {}
482
483#[cfg(test)]
484mod tests {
485 use claims::assert_matches;
486 use daemonbit_test::capture_tracing;
487
488 use super::*;
489
490 macro_rules! pid {
491 ($pid:expr) => {{
492 let pid = ($pid).to_string();
493 pid.as_bytes().to_vec()
494 }};
495 }
496
497 macro_rules! unparsed {
498 ($pid:expr) => {
499 UnparsedPid {
500 path: PathBuf::new(),
501 data: $pid,
502 }
503 };
504 }
505
506 macro_rules! parsed {
507 ($pid:expr) => {
508 ParsedPid {
509 path: PathBuf::new(),
510 pid: $pid,
511 }
512 };
513 }
514
515 const CURRENT_PID: u32 = 123;
516 const FOREIGN_PID: u32 = 456;
517 const INVALID_DATA: &[u8] = b"invalid";
518
519 #[test]
520 fn parse_empty() {
521 let result = ParsedPid::parse(PathBuf::new(), b"");
522 assert_matches!(result, Ok(ParsedPid { pid: None, .. }));
523 }
524
525 #[test]
526 fn parse_invalid_utf8() {
527 let result = ParsedPid::parse(PathBuf::new(), b"\xff");
528 assert_matches!(result, Err((_, e)) if e.kind() == io::ErrorKind::InvalidData);
529 }
530
531 #[test]
532 fn parse_garbage() {
533 let result = ParsedPid::parse(PathBuf::new(), b"123 pid");
534 assert_matches!(result, Err((_, e)) if e.kind() == io::ErrorKind::InvalidData);
535 }
536
537 #[test]
538 fn parse_negative_number() {
539 let result = ParsedPid::parse(PathBuf::new(), b"-123");
540 assert_matches!(result, Err((_, e)) if e.kind() == io::ErrorKind::InvalidData);
541 }
542
543 #[test]
544 fn parse_zero() {
545 let result = ParsedPid::parse(PathBuf::new(), b"0");
546 assert_matches!(result, Ok(ParsedPid { pid: Some(0), .. }));
547 }
548
549 #[test]
550 fn parse_positive_number_signed() {
551 let result = ParsedPid::parse(PathBuf::new(), b"+123");
552 assert_matches!(result, Err((_, e)) if e.kind() == io::ErrorKind::InvalidData);
553 }
554
555 #[test]
556 fn parse_positive_number_unsigned() {
557 let result = ParsedPid::parse(PathBuf::new(), b"123");
558 assert_matches!(result, Ok(ParsedPid { pid: Some(123), .. }));
559 }
560
561 #[test]
562 fn try_parse_different_pid_warns_stale() {
563 let parsed = capture_tracing(|| unparsed!(pid!(FOREIGN_PID)).parse_checked(CURRENT_PID));
564 assert!(parsed.logged("encountered stale lockfile"));
565 assert_matches!(parsed.pid, Some(FOREIGN_PID));
566 }
567
568 #[test]
569 fn try_parse_same_pid_do_not_warn() {
570 let parsed = capture_tracing(|| unparsed!(pid!(CURRENT_PID)).parse_checked(CURRENT_PID));
571 assert!(!parsed.logged("encountered stale lockfile"));
572 assert_matches!(parsed.pid, Some(CURRENT_PID));
573 }
574
575 #[test]
576 fn try_parse_empty_do_not_warn() {
577 let parsed = capture_tracing(|| unparsed!(Vec::new()).parse_checked(CURRENT_PID));
578 assert!(!parsed.logged("encountered stale lockfile"));
579 assert_matches!(parsed.pid, None);
580 }
581
582 #[test]
583 fn try_parse_unparseable_pid_warns_invalid() {
584 let parsed =
585 capture_tracing(|| unparsed!(INVALID_DATA.to_vec()).parse_checked(CURRENT_PID));
586 assert!(parsed.logged("encountered invalid lockfile"));
587 assert_matches!(parsed.pid, None);
588 }
589
590 #[test]
591 fn into_already_locked_error_with_different_pid() {
592 let error = parsed!(Some(FOREIGN_PID)).into_already_locked_error();
593 assert_matches!(
594 error,
595 RawLockfileError::AlreadyLocked {
596 pid: Some(FOREIGN_PID),
597 ..
598 }
599 );
600 }
601
602 #[test]
603 fn into_already_locked_error_with_none() {
604 let error = parsed!(None).into_already_locked_error();
605 assert_matches!(error, RawLockfileError::AlreadyLocked { pid: None, .. });
606 }
607
608 #[test]
609 fn lockfile_open_writes_pid() {
610 api::mock(CURRENT_PID, Ok(pid!(FOREIGN_PID)), None);
611 let lockfile = RawLockfile::try_acquire_with_path("").unwrap();
612 assert_eq!(lockfile.pid, CURRENT_PID);
613 assert_eq!(lockfile.flock.get_ref(), &pid!(CURRENT_PID));
614 }
615
616 #[test]
617 fn lockfile_open_not_found() {
618 api::mock(
619 CURRENT_PID,
620 Err(io::Error::new(
621 io::ErrorKind::NotFound,
622 "no such file or directory",
623 )),
624 None,
625 );
626 let result = RawLockfile::try_acquire_with_path("");
627 assert_matches!(result, Err(RawLockfileError::AccessFailed { .. }));
628 }
629
630 #[test]
631 fn lockfile_open_already_locked() {
632 api::mock(
633 CURRENT_PID,
634 Ok(pid!(FOREIGN_PID)),
635 Some(api::Errno::EWOULDBLOCK),
636 );
637 let result = RawLockfile::try_acquire_with_path("");
638 assert_matches!(
639 result,
640 Err(RawLockfileError::AlreadyLocked {
641 pid: Some(FOREIGN_PID),
642 ..
643 })
644 );
645 }
646
647 #[test]
648 fn lockfile_open_other_lock_error() {
649 api::mock(CURRENT_PID, Ok(pid!(FOREIGN_PID)), Some(api::Errno::EACCES));
650 let result = RawLockfile::try_acquire_with_path("");
651 assert_matches!(result, Err(RawLockfileError::AccessFailed { .. }));
652 }
653}