Skip to main content

cfgd_core/util/
apply_lock.rs

1use crate::errors;
2
3/// Platform-specific lock file type.
4/// Unix: `nix::fcntl::Flock` (safe RAII flock, unlocks on drop).
5/// Windows: plain `File` (LockFileEx releases on handle close).
6#[cfg(unix)]
7type LockFile = nix::fcntl::Flock<std::fs::File>;
8#[cfg(windows)]
9type LockFile = std::fs::File;
10
11/// RAII guard that releases the apply lock when dropped.
12#[derive(Debug)]
13pub struct ApplyLockGuard {
14    _file: LockFile,
15    _path: std::path::PathBuf,
16}
17
18impl Drop for ApplyLockGuard {
19    fn drop(&mut self) {
20        // Clear the PID so stale reads aren't confusing.
21        // Lock is released when LockFile is dropped.
22        if let Err(e) = self._file.set_len(0) {
23            tracing::debug!(path = ?self._path, error = %e, "failed to clear apply-lock PID on drop");
24        }
25    }
26}
27
28/// Acquire an exclusive apply lock via `flock()`.
29///
30/// The lock file is created at `state_dir/apply.lock`. Uses non-blocking
31/// `LOCK_EX | LOCK_NB` — returns `StateError::ApplyLockHeld` if another
32/// process holds the lock. The lock is released automatically when the guard
33/// is dropped.
34#[cfg(unix)]
35pub fn acquire_apply_lock(state_dir: &std::path::Path) -> errors::Result<ApplyLockGuard> {
36    use std::io::Write;
37
38    std::fs::create_dir_all(state_dir)?;
39    let lock_path = state_dir.join("apply.lock");
40
41    let file = std::fs::OpenOptions::new()
42        .create(true)
43        .truncate(false)
44        .read(true)
45        .write(true)
46        .open(&lock_path)?;
47
48    let mut locked = nix::fcntl::Flock::lock(file, nix::fcntl::FlockArg::LockExclusiveNonblock)
49        .map_err(|(_file, errno)| {
50            if errno == nix::errno::Errno::EWOULDBLOCK {
51                let holder = std::fs::read_to_string(&lock_path).unwrap_or_default();
52                errors::CfgdError::from(errors::StateError::ApplyLockHeld {
53                    holder: format!("pid {}", holder.trim()),
54                })
55            } else {
56                errors::CfgdError::from(std::io::Error::from(errno))
57            }
58        })?;
59
60    // Write our PID to the lock file
61    locked.set_len(0)?;
62    write!(locked, "{}", std::process::id())?;
63    locked.sync_all()?;
64
65    Ok(ApplyLockGuard {
66        _file: locked,
67        _path: lock_path,
68    })
69}
70
71/// Acquire an exclusive apply lock via `LockFileEx`.
72///
73/// The lock file is created at `state_dir/apply.lock`. Uses
74/// `LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY` — returns
75/// `StateError::ApplyLockHeld` if another process holds the lock. The lock is
76/// released automatically when the guard is dropped (file handle closed).
77#[cfg(windows)]
78pub fn acquire_apply_lock(state_dir: &std::path::Path) -> errors::Result<ApplyLockGuard> {
79    use std::io::Write;
80    use std::os::windows::io::AsRawHandle;
81    use windows_sys::Win32::Storage::FileSystem::{
82        LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY, LockFileEx,
83    };
84
85    std::fs::create_dir_all(state_dir)?;
86    let lock_path = state_dir.join("apply.lock");
87
88    let file = std::fs::OpenOptions::new()
89        .create(true)
90        .truncate(false)
91        .read(true)
92        .write(true)
93        .open(&lock_path)?;
94
95    let handle = file.as_raw_handle() as windows_sys::Win32::Foundation::HANDLE;
96    // SAFETY: `OVERLAPPED` is a plain-old-data struct of integers and a
97    // handle field; the all-zero bit pattern is the documented "no event,
98    // offset 0" initial value for synchronous-style LockFileEx calls.
99    let mut overlapped: windows_sys::Win32::System::IO::OVERLAPPED = unsafe { std::mem::zeroed() };
100    // SAFETY: `handle` is a valid, open, owned Win32 file handle derived
101    // from `file`, which outlives the call. `&mut overlapped` points to a
102    // stack-local, aligned, writable OVERLAPPED struct. The lock byte
103    // range (offset 0, length 1) is fixed and valid. Non-blocking lock
104    // (LOCKFILE_FAIL_IMMEDIATELY) avoids indefinite wait.
105    let ret = unsafe {
106        LockFileEx(
107            handle,
108            LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
109            0,
110            1,
111            0,
112            &mut overlapped,
113        )
114    };
115    if ret == 0 {
116        let err = std::io::Error::last_os_error();
117        // ERROR_LOCK_VIOLATION (33) = lock held by another process
118        if err.raw_os_error() == Some(33) {
119            let holder = std::fs::read_to_string(&lock_path).unwrap_or_default();
120            return Err(errors::StateError::ApplyLockHeld {
121                holder: format!("pid {}", holder.trim()),
122            }
123            .into());
124        }
125        return Err(err.into());
126    }
127
128    let mut f = file;
129    f.set_len(0)?;
130    write!(f, "{}", std::process::id())?;
131    f.sync_all()?;
132
133    Ok(ApplyLockGuard {
134        _file: f,
135        _path: lock_path,
136    })
137}