use std::fs::{File, TryLockError};
use std::path::Path;
use crate::error::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FileLocking {
#[default]
Enabled,
Disabled,
BestEffort,
}
fn parse_env(value: &str) -> Option<FileLocking> {
let v = value.trim();
if v.eq_ignore_ascii_case("FALSE")
|| v == "0"
|| v.eq_ignore_ascii_case("NO")
|| v.eq_ignore_ascii_case("OFF")
{
Some(FileLocking::Disabled)
} else if v.eq_ignore_ascii_case("BEST_EFFORT") {
Some(FileLocking::BestEffort)
} else if v.eq_ignore_ascii_case("TRUE")
|| v == "1"
|| v.eq_ignore_ascii_case("YES")
|| v.eq_ignore_ascii_case("ON")
{
Some(FileLocking::Enabled)
} else {
None
}
}
fn resolve(requested: FileLocking) -> FileLocking {
std::env::var("HDF5_USE_FILE_LOCKING")
.ok()
.and_then(|v| parse_env(&v))
.unwrap_or(requested)
}
pub(crate) fn acquire_exclusive(
handle: &File,
requested: FileLocking,
path: &Path,
) -> Result<(), Error> {
let mode = resolve(requested);
if mode == FileLocking::Disabled {
return Ok(());
}
match handle.try_lock() {
Ok(()) => Ok(()),
Err(TryLockError::WouldBlock) => Err(Error::FileLocked(format!(
"{}: file is already locked by another process. If a previous writer \
crashed, the OS lock is released automatically (try again); a leftover \
on-disk SWMR flag can be cleared with File::clear_swmr_flag. Set \
HDF5_USE_FILE_LOCKING=FALSE or pass FileLocking::Disabled to bypass locking.",
path.display(),
))),
Err(TryLockError::Error(e)) => match mode {
FileLocking::BestEffort => Ok(()),
_ => Err(Error::Io(e)),
},
}
}
pub(crate) fn clear_swmr_flag_at(path: &Path) -> Result<(), Error> {
use crate::signature;
use crate::superblock::Superblock;
use std::fs::OpenOptions;
use std::io::{Read, Seek, SeekFrom, Write};
let mut w = OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(Error::Io)?;
acquire_exclusive(&w, FileLocking::Enabled, path)?;
let mut data = Vec::new();
w.read_to_end(&mut data).map_err(Error::Io)?;
let sig = signature::find_signature(&data)?;
let mut sb = Superblock::parse(&data, sig)?;
if sb.version < 2 {
return Ok(());
}
if sb.consistency_flags == 0 {
return Ok(());
}
sb.consistency_flags = 0;
let bytes = sb.serialize();
w.seek(SeekFrom::Start(sig as u64)).map_err(Error::Io)?;
w.write_all(&bytes).map_err(Error::Io)?;
w.sync_data().map_err(Error::Io)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_env_recognizes_disable_values() {
for v in ["FALSE", "false", "0", "No", "off", " false "] {
assert_eq!(parse_env(v), Some(FileLocking::Disabled), "value {v:?}");
}
}
#[test]
fn parse_env_recognizes_enable_and_best_effort() {
for v in ["TRUE", "true", "1", "Yes", "on"] {
assert_eq!(parse_env(v), Some(FileLocking::Enabled), "value {v:?}");
}
assert_eq!(parse_env("BEST_EFFORT"), Some(FileLocking::BestEffort));
assert_eq!(parse_env("best_effort"), Some(FileLocking::BestEffort));
}
#[test]
fn parse_env_unrecognized_is_none() {
assert_eq!(parse_env(""), None);
assert_eq!(parse_env("maybe"), None);
assert_eq!(parse_env("2"), None);
}
#[test]
fn default_is_enabled() {
assert_eq!(FileLocking::default(), FileLocking::Enabled);
}
}