//! `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()); }