bxcore, loopd, inferproxy: a lock file keeps each to one instance
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
//! `bxcore::instance`: the lock that keeps a daemon to one instance (M5b spec, section 9a). Two
|
||||
//! opens of the same file in one process are two holders, as two processes are. Do not edit.
|
||||
|
||||
#[path = "support/tmp.rs"]
|
||||
mod tmp;
|
||||
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use bxcore::instance::{LockError, RUNBOOK, lock};
|
||||
use tmp::TempDir;
|
||||
|
||||
/// Gives a directory back its owner's permissions when dropped, so the `TempDir` declared before
|
||||
/// it can be removed even when an assertion fails (tip T44).
|
||||
struct Restore(PathBuf);
|
||||
|
||||
impl Drop for Restore {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::set_permissions(&self.0, std::fs::Permissions::from_mode(0o700));
|
||||
}
|
||||
}
|
||||
|
||||
fn mode(path: &Path) -> u32 {
|
||||
std::fs::metadata(path).unwrap().permissions().mode() & 0o777
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_second_lock_is_held_while_the_first_lives() {
|
||||
let dir = TempDir::new("lock-held");
|
||||
let path = dir.path().join("run").join("loopd.lock");
|
||||
let first = lock(&path).expect("the first lock");
|
||||
for _ in 0..3 {
|
||||
match lock(&path) {
|
||||
Err(LockError::Held) => {}
|
||||
other => panic!("a second lock while the first lives: {other:?}"),
|
||||
}
|
||||
}
|
||||
drop(first);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_lock_is_free_again_once_the_first_is_dropped() {
|
||||
let dir = TempDir::new("lock-free");
|
||||
let path = dir.path().join("run").join("loopd.lock");
|
||||
let first = lock(&path).expect("the first lock");
|
||||
drop(first);
|
||||
let second = lock(&path).expect("the lock after the first was dropped");
|
||||
assert!(
|
||||
matches!(lock(&path), Err(LockError::Held)),
|
||||
"the second holds it now"
|
||||
);
|
||||
drop(second);
|
||||
assert!(path.exists(), "the lock file is left in place");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lock_file_left_behind_is_taken() {
|
||||
// A killed daemon leaves its lock file, unlocked; the next start takes it and keeps its bytes.
|
||||
let dir = TempDir::new("lock-left");
|
||||
let path = dir.write("run/memoryd.lock", "left\n");
|
||||
let held = lock(&path).expect("a lock file nobody holds");
|
||||
assert_eq!(std::fs::read_to_string(&path).unwrap(), "left\n");
|
||||
drop(held);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_lock_file_is_created_with_mode_0600_and_its_directory_0700() {
|
||||
let dir = TempDir::new("lock-mode");
|
||||
let run = dir.path().join("home").join("run");
|
||||
let path = run.join("brokerd.lock");
|
||||
let held = lock(&path).expect("a lock in a directory that does not exist yet");
|
||||
assert_eq!(mode(&path), 0o600, "the lock file");
|
||||
assert_eq!(mode(&run), 0o700, "the directory it made");
|
||||
assert_eq!(mode(&dir.path().join("home")), 0o700, "and the one above");
|
||||
assert_eq!(
|
||||
std::fs::metadata(&path).unwrap().len(),
|
||||
0,
|
||||
"nothing is written"
|
||||
);
|
||||
drop(held);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_directory_that_cannot_be_written_is_an_io_error() {
|
||||
let dir = TempDir::new("lock-ro");
|
||||
let run = dir.path().join("run");
|
||||
std::fs::create_dir(&run).unwrap();
|
||||
let _restore = Restore(run.clone());
|
||||
std::fs::set_permissions(&run, std::fs::Permissions::from_mode(0o500)).unwrap();
|
||||
match lock(&run.join("gatewayd.lock")) {
|
||||
Err(LockError::Io(e)) => {
|
||||
assert_eq!(e.kind(), std::io::ErrorKind::PermissionDenied, "{e}");
|
||||
}
|
||||
other => panic!("a lock in a read-only directory: {other:?}"),
|
||||
}
|
||||
assert!(!run.join("gatewayd.lock").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_path_through_a_file_is_an_io_error() {
|
||||
let dir = TempDir::new("lock-file");
|
||||
let file = dir.write("run", "not a directory\n");
|
||||
match lock(&file.join("loopd.lock")) {
|
||||
Err(LockError::Io(_)) => {}
|
||||
other => panic!("a lock under a file: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_lines_a_daemon_prints() {
|
||||
let path = Path::new("/home/bx/run/loopd.lock");
|
||||
assert_eq!(
|
||||
LockError::Held.line("loopd", path),
|
||||
"loopd: another loopd is running (it holds /home/bx/run/loopd.lock); \
|
||||
see docs/runbook.md#already-running"
|
||||
);
|
||||
let io = LockError::Io(std::io::Error::other("the disk is on fire"));
|
||||
assert_eq!(
|
||||
io.line("inferproxy", Path::new("/run/bx/infer.sock.lock")),
|
||||
"inferproxy: cannot take the lock /run/bx/infer.sock.lock: the disk is on fire; \
|
||||
see docs/runbook.md#already-running"
|
||||
);
|
||||
assert_eq!(RUNBOOK, "see docs/runbook.md#already-running");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_error_displays_and_has_a_source() {
|
||||
use std::error::Error;
|
||||
assert_eq!(
|
||||
LockError::Held.to_string(),
|
||||
"another instance holds the lock"
|
||||
);
|
||||
assert!(LockError::Held.source().is_none());
|
||||
let io = LockError::Io(std::io::Error::other("no room"));
|
||||
assert_eq!(io.to_string(), "no room");
|
||||
assert!(io.source().is_some());
|
||||
}
|
||||
Reference in New Issue
Block a user