use std::fmt;
use std::fs::{File, OpenOptions};
use std::io;
use std::os::fd::AsRawFd;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Sharing {
Serve,
Alone,
Beside,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Work {
Serve,
TenantCreate,
TenantList,
TenantSetRetention,
TenantRemove,
KeyCreate,
KeyList,
KeyRevoke,
RekeyTenant,
RekeyMaster,
AuditExport,
Check,
}
impl Work {
pub fn sharing(self) -> Sharing {
match self {
Work::Serve => Sharing::Serve,
Work::TenantCreate
| Work::TenantRemove
| Work::RekeyTenant
| Work::RekeyMaster
| Work::Check => Sharing::Alone,
Work::TenantList
| Work::TenantSetRetention
| Work::KeyCreate
| Work::KeyList
| Work::KeyRevoke
| Work::AuditExport => Sharing::Beside,
}
}
}
#[derive(Debug)]
pub struct DataDirLock {
_held: Vec<File>,
}
#[derive(Debug)]
pub enum LockError {
InUse(Sharing),
Io(io::Error),
}
impl fmt::Display for LockError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LockError::InUse(Sharing::Serve) => f.write_str(
"the data directory is in use by another vcon-server or a command that runs alone",
),
LockError::InUse(Sharing::Alone) => f.write_str(
"the data directory is in use; stop the server and any other command first",
),
LockError::InUse(Sharing::Beside) => {
f.write_str("the data directory is in use by a command that runs alone")
}
LockError::Io(e) => write!(f, "the data directory cannot be locked: {e}"),
}
}
}
pub fn lock(dir: &Path, sharing: Sharing) -> Result<DataDirLock, LockError> {
let take = |name: &str, how| {
flock(&dir.join(name), how).map_err(|e| match e.kind() {
io::ErrorKind::WouldBlock => LockError::InUse(sharing),
_ => LockError::Io(e),
})
};
let held = match sharing {
Sharing::Serve => vec![take(SERVERS, libc::LOCK_EX)?],
Sharing::Alone => vec![
take(SERVERS, libc::LOCK_EX)?,
take(COMMANDS, libc::LOCK_EX)?,
],
Sharing::Beside => vec![take(COMMANDS, libc::LOCK_SH)?],
};
Ok(DataDirLock { _held: held })
}
const SERVERS: &str = "lock";
const COMMANDS: &str = "lock.commands";
fn flock(path: &Path, how: libc::c_int) -> io::Result<File> {
let file = OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(path)?;
if unsafe { libc::flock(file.as_raw_fd(), how | libc::LOCK_NB) } != 0 {
return Err(io::Error::last_os_error());
}
Ok(file)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rekey_check_and_tenant_create_and_remove_run_alone() {
for (work, sharing) in [
(Work::Serve, Sharing::Serve),
(Work::TenantCreate, Sharing::Alone),
(Work::TenantList, Sharing::Beside),
(Work::TenantSetRetention, Sharing::Beside),
(Work::TenantRemove, Sharing::Alone),
(Work::KeyCreate, Sharing::Beside),
(Work::KeyList, Sharing::Beside),
(Work::KeyRevoke, Sharing::Beside),
(Work::RekeyTenant, Sharing::Alone),
(Work::RekeyMaster, Sharing::Alone),
(Work::AuditExport, Sharing::Beside),
(Work::Check, Sharing::Alone),
] {
assert_eq!(work.sharing(), sharing, "{work:?}");
}
}
#[test]
fn who_may_run_beside_whom() {
use Sharing::{Alone, Beside, Serve};
for (first, second, allowed) in [
(Serve, Serve, false),
(Serve, Alone, false),
(Serve, Beside, true),
(Alone, Serve, false),
(Alone, Alone, false),
(Alone, Beside, false),
(Beside, Serve, true),
(Beside, Alone, false),
(Beside, Beside, true),
] {
let dir = tempfile::tempdir().expect("temp dir");
let held = lock(dir.path(), first).expect("the first lock");
match lock(dir.path(), second) {
Ok(_) => assert!(allowed, "{second:?} beside {first:?}"),
Err(LockError::InUse(asked)) => {
assert!(!allowed, "{second:?} refused beside {first:?}");
assert_eq!(asked, second);
}
Err(e) => panic!("{first:?} then {second:?}: {e}"),
}
drop(held);
assert!(lock(dir.path(), second).is_ok(), "{second:?} once free");
}
}
#[test]
fn a_refused_lock_holds_nothing() {
let dir = tempfile::tempdir().expect("temp dir");
let beside = lock(dir.path(), Sharing::Beside).expect("beside");
assert!(lock(dir.path(), Sharing::Alone).is_err());
let server = lock(dir.path(), Sharing::Serve).expect("serve beside");
drop((beside, server));
assert!(lock(dir.path(), Sharing::Alone).is_ok());
}
#[test]
fn each_refusal_says_what_holds_the_directory() {
for (sharing, words) in [
(Sharing::Serve, "another vcon-server or a command"),
(Sharing::Alone, "stop the server"),
(Sharing::Beside, "runs alone"),
] {
let text = LockError::InUse(sharing).to_string();
assert!(text.contains(words), "{sharing:?}: {text}");
}
}
#[test]
fn a_data_directory_that_cannot_hold_a_lock_file_is_an_error() {
let missing = Path::new("/nonexistent/vcon-server-data");
for sharing in [Sharing::Serve, Sharing::Alone, Sharing::Beside] {
assert!(matches!(lock(missing, sharing), Err(LockError::Io(_))));
}
}
}