use crate::privileged;
use anyhow::{Context, Result};
use std::fs::{File, OpenOptions, TryLockError};
use std::path::Path;
pub enum Mode {
Shared,
Exclusive,
}
pub struct StateLock {
_file: Option<File>,
}
impl StateLock {
pub fn acquire(prefix: &Path, mode: &Mode) -> Result<Self> {
Self::acquire_with(
prefix,
mode,
privileged::Policy::for_prefix(prefix),
&mut |s| eprintln!("{s}"),
)
}
pub fn acquire_with(
prefix: &Path,
mode: &Mode,
policy: privileged::Policy,
notice: &mut dyn FnMut(&str),
) -> Result<Self> {
Self::acquire_impl(prefix, mode, policy, notice, true)
.map(|lock| lock.expect("blocking acquisition always returns a lock"))
}
#[cfg_attr(not(feature = "tui"), allow(dead_code))]
pub fn try_acquire_with(
prefix: &Path,
mode: &Mode,
policy: privileged::Policy,
notice: &mut dyn FnMut(&str),
) -> Result<Option<Self>> {
Self::acquire_impl(prefix, mode, policy, notice, false)
}
#[cfg_attr(not(feature = "tui"), allow(dead_code))]
pub fn preparation_needs_privilege(prefix: &Path) -> bool {
let path = prefix.join("share/cargo-lbin/lock");
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.or_else(|_| OpenOptions::new().read(true).open(&path))
.is_err()
}
fn acquire_impl(
prefix: &Path,
mode: &Mode,
policy: privileged::Policy,
notice: &mut dyn FnMut(&str),
block: bool,
) -> Result<Option<Self>> {
let path = prefix.join("share/cargo-lbin/lock");
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.or_else(|_| OpenOptions::new().read(true).open(&path));
let file = match (file, mode) {
(Ok(f), _) => f,
(Err(_), Mode::Exclusive) => {
notice(&format!(
"initializing state for {}: creating {}",
prefix.display(),
path.display()
));
privileged::ensure_lock_file(policy, &path)
.with_context(|| format!("preparing state lock {}", path.display()))?;
OpenOptions::new()
.read(true)
.open(&path)
.with_context(|| format!("opening state lock {}", path.display()))?
}
(Err(_), Mode::Shared) => {
notice(&format!(
"warning: cannot open {} — proceeding without a state lock \
(the file is created by the first mutation)",
path.display()
));
return Ok(Some(Self { _file: None }));
}
};
let probe = match mode {
Mode::Shared => file.try_lock_shared(),
Mode::Exclusive => file.try_lock(),
};
match probe {
Ok(()) => {}
Err(TryLockError::WouldBlock) if !block => return Ok(None),
Err(TryLockError::WouldBlock) => {
notice("another cargo-lbin instance holds the state lock; waiting...");
match mode {
Mode::Shared => file.lock_shared(),
Mode::Exclusive => file.lock(),
}
.context("acquiring state lock")?;
}
Err(TryLockError::Error(e)) => return Err(e).context("acquiring state lock"),
}
Ok(Some(Self { _file: Some(file) }))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn try_variant_yields_instead_of_waiting() {
let prefix = std::env::temp_dir().join("cargo-lbin-test-trylock");
let _ = std::fs::remove_dir_all(&prefix);
let quiet = |_: &str| {};
assert!(!StateLock::preparation_needs_privilege(&prefix));
let held = StateLock::acquire_with(
&prefix,
&Mode::Exclusive,
privileged::Policy {
sudo: privileged::Sudo::Forbidden,
screen: privileged::Screen::Inherited,
},
&mut { quiet },
)
.unwrap();
let advisory = StateLock::try_acquire_with(
&prefix,
&Mode::Shared,
privileged::Policy {
sudo: privileged::Sudo::Forbidden,
screen: privileged::Screen::Inherited,
},
&mut { quiet },
)
.unwrap();
assert!(advisory.is_none(), "shared try must yield to exclusive");
drop(held);
let advisory = StateLock::try_acquire_with(
&prefix,
&Mode::Shared,
privileged::Policy {
sudo: privileged::Sudo::Forbidden,
screen: privileged::Screen::Inherited,
},
&mut { quiet },
)
.unwrap();
assert!(advisory.is_some(), "free lock acquires without waiting");
let _ = std::fs::remove_dir_all(&prefix);
}
}