#[cfg(feature = "json")]
use crate::atomic::atomic_write;
use crate::error::{FsError, Result};
#[cfg(feature = "json")]
use serde::{Deserialize, Serialize};
#[cfg(not(unix))]
use std::fs::TryLockError;
use std::fs::{File, OpenOptions};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "json",
serde(try_from = "OwnerRecord", into = "OwnerRecord")
)]
pub struct LockOwner {
pub pid: u32,
pub hostname: String,
pub label: String,
pub started_unix_ms: u64,
}
impl LockOwner {
pub fn current(label: &str) -> Self {
Self {
pid: std::process::id(),
hostname: hostname(),
label: label.to_string(),
started_unix_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0),
}
}
pub fn started(&self) -> SystemTime {
UNIX_EPOCH + Duration::from_millis(self.started_unix_ms)
}
pub fn describe(&self) -> String {
format!(
"pid {} on {} ({}) since unix-ms {}",
self.pid, self.hostname, self.label, self.started_unix_ms
)
}
}
#[cfg(feature = "json")]
#[derive(Serialize, Deserialize)]
struct OwnerRecord {
pid: u32,
hostname: String,
#[serde(default)]
label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
started_unix_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
started: Option<String>,
}
#[cfg(feature = "json")]
impl TryFrom<OwnerRecord> for LockOwner {
type Error = String;
fn try_from(record: OwnerRecord) -> std::result::Result<Self, String> {
let started_unix_ms = match (record.started_unix_ms, record.started.as_deref()) {
(Some(ms), _) => ms,
(None, Some(text)) => parse_rfc3339_ms(text)
.ok_or_else(|| format!("start time {text:?} is not RFC 3339 UTC"))?,
(None, None) => return Err("owner record has no start time".into()),
};
Ok(Self {
pid: record.pid,
hostname: record.hostname,
label: record.label,
started_unix_ms,
})
}
}
#[cfg(feature = "json")]
impl From<LockOwner> for OwnerRecord {
fn from(owner: LockOwner) -> Self {
Self {
started: Some(format_rfc3339_secs(owner.started_unix_ms)),
started_unix_ms: Some(owner.started_unix_ms),
pid: owner.pid,
hostname: owner.hostname,
label: owner.label,
}
}
}
#[derive(Clone, Debug)]
pub struct LockOptions {
pub timeout: Duration,
pub poll: Duration,
pub label: String,
}
impl Default for LockOptions {
fn default() -> Self {
Self {
timeout: Duration::ZERO,
poll: Duration::from_millis(50),
label: String::new(),
}
}
}
impl LockOptions {
pub fn wait(timeout: Duration) -> Self {
Self {
timeout,
..Self::default()
}
}
pub fn labeled(mut self, label: &str) -> Self {
self.label = label.to_string();
self
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LockKind {
#[default]
Fcntl,
Flock,
}
#[derive(Debug)]
pub struct FileLock {
#[cfg_attr(unix, allow(dead_code))]
file: Handle,
path: PathBuf,
owner_path: Option<PathBuf>,
pub recovered_stale: Option<LockOwner>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Mode {
Exclusive,
Shared,
}
#[cfg(unix)]
#[derive(Debug)]
#[allow(dead_code)]
enum Handle {
Fcntl(posix::Handle),
Flock(flock::Handle),
}
#[cfg(not(unix))]
type Handle = File;
pub fn owner_path_for(lock_path: &Path) -> PathBuf {
let mut os = lock_path.as_os_str().to_os_string();
os.push(".owner");
PathBuf::from(os)
}
fn open_lock_file(path: &Path) -> Result<File> {
if let Some(name) = path.file_name() {
crate::atomic::reject_reserved_name(&name.to_string_lossy())?;
}
#[cfg(unix)]
let creating = matches!(
std::fs::symlink_metadata(path),
Err(e) if e.kind() == std::io::ErrorKind::NotFound
);
#[cfg(unix)]
if creating {
let parent = path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or(Path::new("."));
crate::acl::guard_parent_path(parent, path)?;
}
let mut options = OpenOptions::new();
options.read(true).write(true).create(true).truncate(false);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options
.mode(0o600)
.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
}
let file = options.open(path).map_err(|e| {
#[cfg(unix)]
if e.raw_os_error() == Some(libc::ELOOP) {
return FsError::Symlink(path.to_path_buf());
}
FsError::io(path, e)
})?;
#[cfg(unix)]
if creating {
crate::acl::refuse_acl(&file, path)?;
}
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
let attributes = file
.metadata()
.map_err(|e| FsError::io(path, e))?
.file_attributes();
if attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(FsError::Symlink(path.to_path_buf()));
}
}
Ok(file)
}
impl FileLock {
pub fn acquire(path: &Path, options: &LockOptions) -> Result<Self> {
Self::acquire_kind(path, LockKind::Fcntl, options)
}
pub fn acquire_kind(path: &Path, kind: LockKind, options: &LockOptions) -> Result<Self> {
let file = lock_with_retry(path, options, kind, Mode::Exclusive)?;
let recovered_stale = read_owner(&owner_path_for(path));
let owner_path = record_owner(path, &options.label)?;
Ok(Self {
file,
path: path.to_path_buf(),
owner_path,
recovered_stale,
})
}
pub fn try_acquire(path: &Path) -> Result<Self> {
Self::acquire(path, &LockOptions::default())
}
pub fn try_lock_with(path: &Path, kind: LockKind, timeout: Duration) -> Result<Self> {
Self::acquire_kind(path, kind, &LockOptions::wait(timeout))
}
pub fn acquire_shared(path: &Path, options: &LockOptions) -> Result<Self> {
Self::acquire_shared_kind(path, LockKind::Fcntl, options)
}
pub fn acquire_shared_kind(path: &Path, kind: LockKind, options: &LockOptions) -> Result<Self> {
let file = lock_with_retry(path, options, kind, Mode::Shared)?;
Ok(Self {
file,
path: path.to_path_buf(),
owner_path: None,
recovered_stale: None,
})
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn current_owner(lock_path: &Path) -> Option<LockOwner> {
probe_owner(lock_path)
}
}
pub fn probe_owner(lock_path: &Path) -> Option<LockOwner> {
read_owner(&owner_path_for(lock_path))
}
#[cfg(unix)]
fn lock_with_retry(
path: &Path,
options: &LockOptions,
kind: LockKind,
mode: Mode,
) -> Result<Handle> {
let started = Instant::now();
loop {
let granted = match kind {
LockKind::Fcntl => posix::try_lock(path, mode)?.map(Handle::Fcntl),
LockKind::Flock => flock::try_lock(path, mode)?.map(Handle::Flock),
};
if let Some(handle) = granted {
return Ok(handle);
}
wait_or_fail(path, options, started, mode)?;
}
}
#[cfg(not(unix))]
fn lock_with_retry(
path: &Path,
options: &LockOptions,
_kind: LockKind,
mode: Mode,
) -> Result<Handle> {
let file = open_lock_file(path)?;
let started = Instant::now();
loop {
let locked = match mode {
Mode::Exclusive => file.try_lock(),
Mode::Shared => file.try_lock_shared(),
};
match locked {
Ok(()) => return Ok(file),
Err(TryLockError::WouldBlock) => wait_or_fail(path, options, started, mode)?,
Err(TryLockError::Error(e)) => return Err(FsError::io(path, e)),
}
}
}
fn wait_or_fail(path: &Path, options: &LockOptions, started: Instant, mode: Mode) -> Result<()> {
if started.elapsed() >= options.timeout {
let waited_ms = started.elapsed().as_millis() as u64;
return Err(match mode {
Mode::Exclusive if options.timeout.is_zero() => {
let owner = read_owner(&owner_path_for(path))
.map(|o| o.describe())
.unwrap_or_else(|| "an unknown owner".into());
FsError::Locked {
path: path.to_path_buf(),
owner,
}
}
_ => FsError::LockTimeout {
path: path.to_path_buf(),
waited_ms,
},
});
}
std::thread::sleep(options.poll);
Ok(())
}
impl Drop for FileLock {
fn drop(&mut self) {
if let Some(owner) = &self.owner_path {
let _ = std::fs::remove_file(owner);
}
#[cfg(not(unix))]
let _ = self.file.unlock();
}
}
#[cfg(unix)]
mod posix {
use super::{open_lock_file, Mode};
use crate::error::{FsError, Result};
use std::collections::BTreeMap;
use std::fs::{File, Metadata};
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::sync::{Mutex, MutexGuard, OnceLock};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct FileId {
dev: u64,
ino: u64,
}
impl FileId {
fn of(meta: &Metadata) -> Self {
Self {
dev: meta.dev(),
ino: meta.ino(),
}
}
}
struct Entry {
mode: Mode,
holders: usize,
files: Vec<File>,
}
#[derive(Default)]
struct Registry {
entries: BTreeMap<FileId, Entry>,
}
fn registry() -> MutexGuard<'static, Registry> {
static REGISTRY: OnceLock<Mutex<Registry>> = OnceLock::new();
REGISTRY
.get_or_init(|| Mutex::new(Registry::default()))
.lock()
.unwrap_or_else(|p| p.into_inner())
}
#[derive(Debug)]
pub struct Handle {
id: FileId,
}
fn join(entry: &mut Entry, id: FileId, mode: Mode) -> Option<Handle> {
if entry.mode == Mode::Shared && mode == Mode::Shared {
entry.holders += 1;
Some(Handle { id })
} else {
None
}
}
pub fn try_lock(path: &Path, mode: Mode) -> Result<Option<Handle>> {
let mut registry = registry();
let seen = std::fs::symlink_metadata(path)
.ok()
.map(|meta| FileId::of(&meta));
if let Some(seen) = seen {
if let Some(entry) = registry.entries.get_mut(&seen) {
return Ok(join(entry, seen, mode));
}
}
#[cfg(feature = "testing")]
super::testing::after_identity_check_run(path);
let file = open_lock_file(path)?;
let id = FileId::of(&file.metadata().map_err(|e| FsError::io(path, e))?);
if let Some(entry) = registry.entries.get_mut(&id) {
entry.files.push(file);
return Ok(join(entry, id, mode));
}
if !set_lock(&file, mode, path)? {
return Ok(None);
}
registry.entries.insert(
id,
Entry {
mode,
holders: 1,
files: vec![file],
},
);
Ok(Some(Handle { id }))
}
fn set_lock(file: &File, mode: Mode, path: &Path) -> Result<bool> {
match set_record_lock(file, lock_type(mode)) {
Ok(granted) => Ok(granted),
Err(error) => Err(FsError::io(path, error)),
}
}
#[allow(clippy::unnecessary_cast)]
pub(super) fn lock_type(mode: Mode) -> libc::c_short {
match mode {
Mode::Exclusive => libc::F_WRLCK as libc::c_short,
Mode::Shared => libc::F_RDLCK as libc::c_short,
}
}
fn set_record_lock(file: &File, kind: libc::c_short) -> std::io::Result<bool> {
use std::os::fd::AsRawFd;
let request = libc::flock {
l_type: kind,
l_whence: libc::SEEK_SET as _,
l_start: 0,
l_len: 0,
l_pid: 0,
};
loop {
let rc = unsafe {
libc::fcntl(
file.as_raw_fd(),
libc::F_SETLK,
&request as *const libc::flock,
)
};
if rc != -1 {
return Ok(true);
}
let error = std::io::Error::last_os_error();
match error.raw_os_error() {
Some(libc::EINTR) => continue,
Some(libc::EAGAIN) | Some(libc::EACCES) => return Ok(false),
_ => return Err(error),
}
}
}
impl Drop for Handle {
fn drop(&mut self) {
let mut registry = registry();
let last = match registry.entries.get_mut(&self.id) {
Some(entry) => {
entry.holders -= 1;
entry.holders == 0
}
None => false,
};
if last {
drop(registry.entries.remove(&self.id));
}
}
}
}
#[cfg(unix)]
mod flock {
use super::{open_lock_file, Mode};
use crate::error::{FsError, Result};
use std::fs::File;
use std::os::fd::AsRawFd;
use std::path::Path;
#[derive(Debug)]
pub struct Handle {
file: File,
}
pub fn try_lock(path: &Path, mode: Mode) -> Result<Option<Handle>> {
let file = open_lock_file(path)?;
let operation = match mode {
Mode::Exclusive => libc::LOCK_EX,
Mode::Shared => libc::LOCK_SH,
};
loop {
let rc = unsafe { libc::flock(file.as_raw_fd(), operation | libc::LOCK_NB) };
if rc == 0 {
return Ok(Some(Handle { file }));
}
let error = std::io::Error::last_os_error();
match error.raw_os_error() {
Some(libc::EINTR) => continue,
Some(libc::EWOULDBLOCK) => return Ok(None),
_ => return Err(FsError::io(path, error)),
}
}
}
impl Drop for Handle {
fn drop(&mut self) {
unsafe { libc::flock(self.file.as_raw_fd(), libc::LOCK_UN) };
}
}
}
#[cfg(feature = "testing")]
pub mod testing {
use std::cell::RefCell;
use std::path::Path;
type Hook = Box<dyn Fn(&Path)>;
thread_local! {
static AFTER_IDENTITY_CHECK: RefCell<Option<Hook>> = const { RefCell::new(None) };
}
pub struct HookGuard(fn());
impl Drop for HookGuard {
fn drop(&mut self) {
(self.0)();
}
}
pub fn after_identity_check(hook: impl Fn(&Path) + 'static) -> HookGuard {
AFTER_IDENTITY_CHECK.with(|slot| *slot.borrow_mut() = Some(Box::new(hook)));
HookGuard(|| AFTER_IDENTITY_CHECK.with(|slot| *slot.borrow_mut() = None))
}
#[cfg(unix)]
pub(super) fn after_identity_check_run(path: &Path) {
AFTER_IDENTITY_CHECK.with(|slot| {
if let Some(hook) = slot.borrow().as_ref() {
hook(path);
}
});
}
}
pub fn with_lock<T>(path: &Path, options: &LockOptions, f: impl FnOnce() -> T) -> Result<T> {
let _guard = FileLock::acquire(path, options)?;
Ok(f())
}
#[cfg(feature = "json")]
fn record_owner(lock_path: &Path, label: &str) -> Result<Option<PathBuf>> {
let owner_path = owner_path_for(lock_path);
let record = serde_json::to_vec_pretty(&LockOwner::current(label)).expect("owner serializes");
atomic_write(&owner_path, &record)?;
Ok(Some(owner_path))
}
#[cfg(not(feature = "json"))]
fn record_owner(_lock_path: &Path, _label: &str) -> Result<Option<PathBuf>> {
Ok(None)
}
#[cfg(feature = "json")]
fn read_owner(path: &Path) -> Option<LockOwner> {
serde_json::from_slice(&std::fs::read(path).ok()?).ok()
}
#[cfg(not(feature = "json"))]
fn read_owner(_path: &Path) -> Option<LockOwner> {
None
}
#[cfg(feature = "json")]
fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
let y = if month <= 2 { year - 1 } else { year };
let era = y.div_euclid(400);
let yoe = y.rem_euclid(400);
let m = month as i64;
let mp = if m > 2 { m - 3 } else { m + 9 };
let doy = (153 * mp + 2) / 5 + day as i64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
#[cfg(feature = "json")]
fn civil_from_days(days: i64) -> (i64, u32, u32) {
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = (doy - (153 * mp + 2) / 5 + 1) as u32;
let month = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
let year = if month <= 2 {
yoe + era * 400 + 1
} else {
yoe + era * 400
};
(year, month, day)
}
#[cfg(feature = "json")]
fn format_rfc3339_secs(unix_ms: u64) -> String {
let secs = (unix_ms / 1000) as i64;
let (year, month, day) = civil_from_days(secs.div_euclid(86_400));
let second_of_day = secs.rem_euclid(86_400);
format!(
"{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}Z",
second_of_day / 3600,
second_of_day % 3600 / 60,
second_of_day % 60
)
}
#[cfg(feature = "json")]
fn parse_rfc3339_ms(text: &str) -> Option<u64> {
if !text.is_ascii() {
return None;
}
let bytes = text.as_bytes();
if bytes.len() < 20
|| bytes[4] != b'-'
|| bytes[7] != b'-'
|| !matches!(bytes[10], b'T' | b't')
|| bytes[13] != b':'
|| bytes[16] != b':'
{
return None;
}
let number = |s: &str| -> Option<u64> {
if s.is_empty() || !s.bytes().all(|c| c.is_ascii_digit()) {
return None;
}
s.parse().ok()
};
let year = number(&text[0..4])? as i64;
let month = number(&text[5..7])? as u32;
let day = number(&text[8..10])? as u32;
let hour = number(&text[11..13])?;
let minute = number(&text[14..16])?;
let second = number(&text[17..19])?;
if !(1..=12).contains(&month)
|| day == 0
|| day > days_in_month(year, month)
|| hour > 23
|| minute > 59
|| second > 59
{
return None;
}
let mut index = 19;
let mut millis = 0u64;
if bytes.get(index) == Some(&b'.') {
index += 1;
let start = index;
while index < bytes.len() && bytes[index].is_ascii_digit() {
index += 1;
}
if index == start {
return None;
}
let mut kept: String = text[start..index].chars().take(3).collect();
while kept.len() < 3 {
kept.push('0');
}
millis = kept.parse().ok()?;
}
let offset_secs: i64 = match bytes.get(index)? {
b'Z' | b'z' if index + 1 == bytes.len() => 0,
sign @ (b'+' | b'-') if bytes.len() == index + 6 && bytes[index + 3] == b':' => {
let hours = number(&text[index + 1..index + 3])? as i64;
let minutes = number(&text[index + 4..index + 6])? as i64;
if hours > 23 || minutes > 59 {
return None;
}
let magnitude = hours * 3600 + minutes * 60;
if *sign == b'+' {
magnitude
} else {
-magnitude
}
}
_ => return None,
};
let days = days_from_civil(year, month, day);
let utc_secs =
days * 86_400 + hour as i64 * 3600 + minute as i64 * 60 + second as i64 - offset_secs;
if utc_secs < 0 {
return None;
}
Some(utc_secs as u64 * 1000 + millis)
}
#[cfg(feature = "json")]
fn days_in_month(year: i64, month: u32) -> u32 {
let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if leap => 29,
2 => 28,
_ => 0,
}
}
pub fn hostname() -> String {
#[cfg(unix)]
{
let mut buf = [0u8; 256];
if unsafe { libc::gethostname(buf.as_mut_ptr().cast(), buf.len()) } == 0 {
let end = buf.iter().position(|b| *b == 0).unwrap_or(buf.len());
let name = String::from_utf8_lossy(&buf[..end]).trim().to_string();
if !name.is_empty() {
return name;
}
}
}
for var in ["COMPUTERNAME", "HOSTNAME"] {
if let Some(v) = std::env::var_os(var) {
let s = v.to_string_lossy().trim().to_string();
if !s.is_empty() {
return s;
}
}
}
"unknown".into()
}
#[cfg(all(test, unix))]
mod lock_type_tests {
use super::{posix::lock_type, Mode};
#[test]
fn record_lock_types_survive_the_c_short_cast_and_stay_distinct() {
assert_eq!(
i64::from(lock_type(Mode::Exclusive)),
i64::from(libc::F_WRLCK)
);
assert_eq!(i64::from(lock_type(Mode::Shared)), i64::from(libc::F_RDLCK));
assert_ne!(lock_type(Mode::Exclusive), lock_type(Mode::Shared));
}
}
#[cfg(all(test, feature = "json"))]
mod time_tests {
use super::{days_from_civil, format_rfc3339_secs, parse_rfc3339_ms};
#[test]
fn known_dates_round_trip() {
assert_eq!(days_from_civil(1970, 1, 1), 0);
assert_eq!(
format_rfc3339_secs(1_577_836_800_000),
"2020-01-01T00:00:00Z"
);
assert_eq!(
parse_rfc3339_ms("2020-01-01T00:00:00Z"),
Some(1_577_836_800_000)
);
assert_eq!(
format_rfc3339_secs(parse_rfc3339_ms("2024-02-29T23:59:59Z").unwrap()),
"2024-02-29T23:59:59Z"
);
assert_eq!(
format_rfc3339_secs(parse_rfc3339_ms("1999-12-31T23:59:59Z").unwrap()),
"1999-12-31T23:59:59Z"
);
}
#[test]
fn offsets_fractions_and_bad_input() {
assert_eq!(
parse_rfc3339_ms("2020-01-01T01:00:00+01:00"),
parse_rfc3339_ms("2020-01-01T00:00:00Z")
);
assert_eq!(
parse_rfc3339_ms("2020-01-01T00:00:00.5Z"),
Some(1_577_836_800_500)
);
assert_eq!(
parse_rfc3339_ms("2020-01-01t00:00:00.123456z"),
Some(1_577_836_800_123)
);
assert_eq!(parse_rfc3339_ms("2020-02-30T00:00:00Z"), None);
assert_eq!(parse_rfc3339_ms("2019-02-29T00:00:00Z"), None);
assert_eq!(parse_rfc3339_ms("2020-01-01T00:00:00"), None);
assert_eq!(parse_rfc3339_ms("2020-01-01 00:00:00Z"), None);
assert_eq!(parse_rfc3339_ms("2020-01-0\u{e9}T00:00:00Z"), None);
assert_eq!(parse_rfc3339_ms("1969-12-31T23:59:59Z"), None);
assert_eq!(parse_rfc3339_ms("not a time"), None);
}
}