Files
ElyOffice/crates/ely-platform/src/lock.rs
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

95 lines
2.9 KiB
Rust

//! An OS-level exclusive file lock, released with its holder.
use std::{fs::File, io, path::Path};
pub struct FileLock(File);
impl FileLock {
/// The lock, or None while another process holds it.
pub fn try_take(path: &Path) -> io::Result<Option<Self>> {
let file = open(path)?;
match lock(&file, false) {
Ok(()) => Ok(Some(Self(file))),
Err(error) if busy(&error) => Ok(None),
Err(error) => Err(error),
}
}
/// The lock, once another holder lets go.
pub fn take(path: &Path) -> io::Result<Self> {
let file = open(path)?;
lock(&file, true)?;
Ok(Self(file))
}
}
fn open(path: &Path) -> io::Result<File> {
std::fs::OpenOptions::new().write(true).create(true).truncate(false).open(path)
}
#[cfg(unix)]
fn lock(file: &File, wait: bool) -> io::Result<()> {
use std::os::unix::io::AsRawFd;
let how = if wait { libc::LOCK_EX } else { libc::LOCK_EX | libc::LOCK_NB };
match unsafe { libc::flock(file.as_raw_fd(), how) } {
0 => Ok(()),
_ => Err(io::Error::last_os_error()),
}
}
#[cfg(unix)]
fn busy(error: &io::Error) -> bool {
error.kind() == io::ErrorKind::WouldBlock
}
#[cfg(unix)]
impl Drop for FileLock {
fn drop(&mut self) {
use std::os::unix::io::AsRawFd;
unsafe { libc::flock(self.0.as_raw_fd(), libc::LOCK_UN) };
}
}
#[cfg(windows)]
fn lock(file: &File, wait: bool) -> io::Result<()> {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::{LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY, LockFileEx};
let flags = if wait { LOCKFILE_EXCLUSIVE_LOCK } else { LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY };
let mut overlapped = unsafe { std::mem::zeroed() };
match unsafe { LockFileEx(file.as_raw_handle(), flags, 0, u32::MAX, u32::MAX, &mut overlapped) } {
0 => Err(io::Error::last_os_error()),
_ => Ok(()),
}
}
#[cfg(windows)]
fn busy(error: &io::Error) -> bool {
const ERROR_LOCK_VIOLATION: i32 = 33;
error.raw_os_error() == Some(ERROR_LOCK_VIOLATION)
}
#[cfg(windows)]
impl Drop for FileLock {
fn drop(&mut self) {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::UnlockFileEx;
let mut overlapped = unsafe { std::mem::zeroed() };
unsafe { UnlockFileEx(self.0.as_raw_handle(), 0, u32::MAX, u32::MAX, &mut overlapped) };
}
}
#[cfg(test)]
mod tests {
use super::FileLock;
#[test]
fn a_held_lock_refuses_a_second_try_until_released() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("held.lock");
let held = FileLock::take(&path).unwrap();
assert!(FileLock::try_take(&path).unwrap().is_none(), "flock is per open file");
drop(held);
assert!(FileLock::try_take(&path).unwrap().is_some());
}
}