use std::fs::File;
use std::io;
use std::path::Path;
#[cfg(unix)]
mod unix;
#[cfg(unix)]
use unix as platform;
#[cfg(windows)]
mod windows;
#[cfg(windows)]
use windows as platform;
pub fn try_lock_exclusive(file: &File) -> io::Result<bool> {
platform::try_lock_exclusive(file)
}
pub fn same_file(file: &File, path: &Path) -> io::Result<bool> {
platform::same_file(file, path)
}
pub fn rename_no_replace(from: &Path, to: &Path) -> io::Result<()> {
platform::rename_no_replace(from, to)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Termination {
name: &'static str,
exit_code: u8,
closing: bool,
}
impl Termination {
#[must_use]
pub const fn new(name: &'static str, exit_code: u8, closing: bool) -> Self {
Self {
name,
exit_code,
closing,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
self.name
}
#[must_use]
pub const fn exit_code(self) -> u8 {
self.exit_code
}
#[must_use]
pub const fn closing(self) -> bool {
self.closing
}
}
pub fn on_termination(handler: impl Fn(Termination) + Send + Sync + 'static) -> io::Result<()> {
platform::on_termination(Box::new(handler))
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct ProcessMemory {
pub resident_bytes: Option<u64>,
pub peak_resident_bytes: Option<u64>,
}
#[must_use]
pub fn process_memory() -> ProcessMemory {
#[cfg(any(unix, windows))]
{
platform::process_memory()
}
#[cfg(not(any(unix, windows)))]
{
ProcessMemory::default()
}
}
#[must_use]
pub fn thread_cpu_work() -> Option<u64> {
#[cfg(any(unix, windows))]
{
platform::thread_cpu_work()
}
#[cfg(not(any(unix, windows)))]
{
None
}
}
pub fn write_diagnostic(line: std::fmt::Arguments<'_>) {
use std::io::Write;
let _ = writeln!(io::stderr().lock(), "{line}");
}
#[macro_export]
macro_rules! diagnostic {
($($argument:tt)*) => {
$crate::write_diagnostic(::std::format_args!($($argument)*))
};
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{self, OpenOptions};
#[test]
#[cfg(any(target_os = "linux", target_os = "macos", windows))]
fn the_process_reports_its_resident_memory() {
let memory = process_memory();
let resident = memory.resident_bytes.expect("resident bytes");
let peak = memory.peak_resident_bytes.expect("peak resident bytes");
assert!(resident > 1 << 20, "{memory:?}");
assert!(peak >= resident, "{memory:?}");
}
#[test]
#[cfg(any(unix, windows))]
fn the_thread_counts_the_work_it_does_and_not_the_time_it_sleeps() {
let started = thread_cpu_work().expect("thread work");
std::thread::sleep(std::time::Duration::from_millis(200));
let slept = thread_cpu_work().expect("thread work") - started;
let mut state = 1_u64;
let busy = std::time::Instant::now();
while busy.elapsed() < std::time::Duration::from_millis(200) {
state = std::hint::black_box(state.wrapping_mul(6_364_136_223_846_793_005));
}
let worked = thread_cpu_work().expect("thread work") - started - slept;
assert!(worked > slept * 4, "slept {slept}, worked {worked}");
}
fn scratch(name: &str) -> std::path::PathBuf {
let path = std::env::temp_dir().join(format!("alpha2-os-{}-{name}", std::process::id()));
let _ = fs::remove_file(&path);
path
}
fn open(path: &Path) -> File {
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)
.expect("open")
}
static FORK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn one_handle_at_a_time_holds_the_lock_and_closing_it_releases_it() {
let _fork = FORK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let path = scratch("lock");
let first = open(&path);
let second = open(&path);
assert!(try_lock_exclusive(&first).expect("lock"));
assert!(!try_lock_exclusive(&second).expect("second lock"));
drop(first);
assert!(try_lock_exclusive(&second).expect("lock after release"));
let _ = fs::remove_file(&path);
}
#[test]
fn another_handle_writes_the_whole_of_a_locked_file() {
use std::io::{Read, Write};
let path = scratch("write");
let lock = open(&path);
assert!(try_lock_exclusive(&lock).expect("lock"));
let mut writer = File::create(&path).expect("writer truncates and opens");
writer
.write_all(&[7_u8; 4096])
.expect("write under the lock");
drop(writer);
let mut read = Vec::new();
File::open(&path)
.expect("reader")
.read_to_end(&mut read)
.expect("read under the lock");
assert_eq!(read, vec![7_u8; 4096]);
drop(lock);
let _ = fs::remove_file(&path);
}
const LOCK_PROBE: &str = "ALPHA2_OS_LOCK_PROBE";
fn another_process_takes_the_lock(path: &Path) -> bool {
let status = std::process::Command::new(std::env::current_exe().expect("test binary"))
.args(["--exact", "tests::lock_probe_child", "--test-threads=1"])
.env(LOCK_PROBE, path)
.stdout(std::process::Stdio::null())
.status()
.expect("run the lock probe");
match status.code() {
Some(0) => true,
Some(3) => false,
_ => panic!("the lock probe failed: {status}"),
}
}
#[test]
fn lock_probe_child() {
let Some(path) = std::env::var_os(LOCK_PROBE) else {
return;
};
let locked = try_lock_exclusive(&open(Path::new(&path))).expect("probe lock");
std::process::exit(if locked { 0 } else { 3 });
}
#[test]
fn the_lock_stays_with_its_handle_when_another_handle_closes() {
let _fork = FORK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let path = scratch("other-handle");
let lock = open(&path);
assert!(try_lock_exclusive(&lock).expect("lock"));
drop(File::create(&path).expect("another handle opens and closes"));
drop(open(&path));
assert!(
!another_process_takes_the_lock(&path),
"closing another handle released the lock"
);
drop(lock);
assert!(
another_process_takes_the_lock(&path),
"closing the handle that holds the lock releases it"
);
let _ = fs::remove_file(&path);
}
#[test]
#[cfg(target_os = "linux")]
fn the_lock_covers_only_one_byte_no_write_reaches() {
use std::os::fd::AsRawFd;
let path = scratch("one-byte");
let lock = open(&path);
assert!(try_lock_exclusive(&lock).expect("lock"));
let other = open(&path);
let probe = |start: i64, len: i64| {
let mut query: libc::flock = unsafe { std::mem::zeroed() };
query.l_type = libc::c_short::try_from(libc::F_WRLCK).expect("F_WRLCK");
query.l_whence = libc::c_short::try_from(libc::SEEK_SET).expect("SEEK_SET");
query.l_start = start;
query.l_len = len;
let asked =
unsafe { libc::fcntl(other.as_raw_fd(), libc::F_OFD_GETLK, &raw mut query) };
assert_eq!(asked, 0, "F_OFD_GETLK: {}", io::Error::last_os_error());
query
};
let everything_below = probe(0, i64::MAX);
assert_eq!(
i32::from(everything_below.l_type),
libc::F_UNLCK,
"no byte a write can reach is locked"
);
let last = probe(0, 0);
assert_eq!(
i32::from(last.l_type),
libc::F_WRLCK,
"the lock is an OFD lock"
);
assert_eq!(last.l_start, i64::MAX);
drop(lock);
let _ = fs::remove_file(&path);
}
#[test]
fn a_path_moved_away_and_created_again_is_another_file() {
let (path, moved) = (scratch("identity"), scratch("identity-moved"));
let file = open(&path);
assert!(same_file(&file, &path).expect("same"));
fs::rename(&path, &moved).expect("move away");
assert!(!same_file(&file, &path).expect("missing"));
drop(open(&path));
assert!(!same_file(&file, &path).expect("replaced"));
assert!(same_file(&file, &moved).expect("moved"));
let _ = fs::remove_file(&path);
let _ = fs::remove_file(&moved);
}
#[test]
fn a_removed_file_still_open_is_not_the_file_its_path_names() {
let path = scratch("identity-removed");
let file = open(&path);
fs::remove_file(&path).expect("remove");
assert!(!same_file(&file, &path).expect("removed"));
drop(file);
let again = open(&path);
assert!(same_file(&again, &path).expect("created again"));
drop(again);
let _ = fs::remove_file(&path);
}
#[test]
fn a_rename_moves_a_file_to_a_free_name_and_never_replaces_one() {
let (from, to) = (scratch("rename-from"), scratch("rename-to"));
fs::write(&from, b"export").expect("from");
fs::write(&to, b"someone else's").expect("to");
let refused = rename_no_replace(&from, &to).expect_err("an existing file is not replaced");
assert_eq!(refused.kind(), io::ErrorKind::AlreadyExists);
assert_eq!(fs::read(&to).expect("to"), b"someone else's");
assert_eq!(fs::read(&from).expect("from"), b"export");
fs::remove_file(&to).expect("free the name");
rename_no_replace(&from, &to).expect("rename to a free name");
assert_eq!(fs::read(&to).expect("to"), b"export");
assert!(!from.exists());
let _ = fs::remove_file(&to);
}
}