98 lines
3.8 KiB
Rust
98 lines
3.8 KiB
Rust
//! 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<InstanceLock, LockError> {
|
|
// 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)),
|
|
}
|
|
}
|