//! One instance per daemon (M5b spec, section 9a). Every daemon removes a socket it finds at its //! path before it binds, so a second copy started by mistake would take the sockets of the one //! that runs. Before it touches them, a daemon takes an exclusive lock on its lock file with //! `lock`, and keeps the `InstanceLock` until the process ends. use std::fs::{DirBuilder, File, OpenOptions, TryLockError}; use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt}; use std::path::Path; /// The runbook entry for both ways `lock` can fail. pub const RUNBOOK: &str = "see docs/runbook.md#already-running"; /// A held lock: the open lock file. The system releases the lock when the file is closed, which is /// when this is dropped or the process ends, however it ends. The file itself stays: removing it /// would let a second copy lock a new file while the first still runs. #[derive(Debug)] #[must_use = "the lock is released when this value is dropped"] pub struct InstanceLock { _file: File, } /// Why `lock` did not take the lock. #[derive(Debug)] pub enum LockError { /// Another open file holds the lock: another instance is running. Held, /// The lock file, or its directory, could not be created or opened, or the lock failed. Io(std::io::Error), } impl std::fmt::Display for LockError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { LockError::Held => write!(f, "another instance holds the lock"), LockError::Io(e) => write!(f, "{e}"), } } } impl std::error::Error for LockError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { LockError::Held => None, LockError::Io(e) => Some(e), } } } impl LockError { /// The line a daemon prints when it does not start: `role` is its name, `path` the lock file. pub fn line(&self, role: &str, path: &Path) -> String { // `Held`: "{role}: another {role} is running (it holds {path}); {RUNBOOK}". // `Io(e)`: "{role}: cannot take the lock {path}: {e}; {RUNBOOK}". // Each with `format!`, the path shown with `path.display()`. match self { LockError::Held => format!( "{role}: another {role} is running (it holds {}); {RUNBOOK}", path.display() ), LockError::Io(e) => format!( "{role}: cannot take the lock {path}: {e}; {RUNBOOK}", path = path.display() ), } } } /// Take the exclusive lock on `path`, creating the file with mode 0600, and its directory with /// mode 0700 when it is missing, as the daemons make their socket directories. The lock is tried /// once, never waited for. pub fn lock(path: &Path) -> Result { // 1. If `path.parent()` is `Some(dir)`: `DirBuilder::new().recursive(true).mode(0o700) // .create(dir)`. 2. `OpenOptions::new().write(true).create(true).truncate(false) // .mode(0o600).open(path)`. An error of either is `LockError::Io`, through `map_err`. // 3. `file.try_lock()`: `Ok(())` is `Ok(InstanceLock { _file: file })`, // `Err(TryLockError::WouldBlock)` is `Err(LockError::Held)`, and // `Err(TryLockError::Error(e))` is `Err(LockError::Io(e))`. if let Some(dir) = path.parent() { DirBuilder::new() .recursive(true) .mode(0o700) .create(dir) .map_err(LockError::Io)?; } let file = OpenOptions::new() .write(true) .create(true) .truncate(false) .mode(0o600) .open(path) .map_err(LockError::Io)?; match file.try_lock() { Ok(()) => Ok(InstanceLock { _file: file }), Err(TryLockError::WouldBlock) => Err(LockError::Held), Err(TryLockError::Error(e)) => Err(LockError::Io(e)), } }