use std::fs::{File, TryLockError};
pub const DATABASE_INODE_LOCK_OFFSET: i64 = i64::MAX - 1;
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "macos",
target_os = "ios"
))]
#[allow(unsafe_code, clippy::incompatible_msrv)]
pub fn try_lock_database_inode(file: &File) -> Result<(), TryLockError> {
use std::os::fd::AsRawFd;
let lock = libc::flock {
l_type: libc::c_short::try_from(libc::F_WRLCK).unwrap_or(1),
l_whence: libc::c_short::try_from(libc::SEEK_SET).unwrap_or(0),
l_start: libc::off_t::try_from(DATABASE_INODE_LOCK_OFFSET).unwrap_or(libc::off_t::MAX - 1),
l_len: 1,
l_pid: 0,
};
let rc = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_OFD_SETLK, &lock) };
if rc == 0 {
return Ok(());
}
let error = std::io::Error::last_os_error();
match error.raw_os_error() {
Some(code) if code == libc::EAGAIN || code == libc::EACCES => Err(TryLockError::WouldBlock),
_ => Err(TryLockError::Error(error)),
}
}
#[cfg(windows)]
#[allow(unsafe_code)]
pub fn rename_database_candidate_no_replace(
from: &std::path::Path,
to: &std::path::Path,
) -> std::io::Result<()> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Storage::FileSystem::{MOVEFILE_WRITE_THROUGH, MoveFileExW};
fn nul_terminated(path: &std::path::Path) -> std::io::Result<Vec<u16>> {
let mut encoded = path.as_os_str().encode_wide().collect::<Vec<_>>();
if encoded.contains(&0) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"database replacement path contains an embedded NUL",
));
}
encoded.push(0);
Ok(encoded)
}
let from = nul_terminated(from)?;
let to = nul_terminated(to)?;
let moved = unsafe { MoveFileExW(from.as_ptr(), to.as_ptr(), MOVEFILE_WRITE_THROUGH) };
if moved == 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(())
}
}
#[cfg(all(
unix,
not(any(
target_os = "linux",
target_os = "android",
target_os = "macos",
target_os = "ios"
))
))]
#[allow(clippy::incompatible_msrv)]
pub fn try_lock_database_inode(file: &File) -> Result<(), TryLockError> {
file.try_lock()
}
#[cfg(windows)]
#[allow(
unsafe_code,
non_snake_case,
clippy::items_after_statements,
clippy::incompatible_msrv
)]
pub fn try_lock_database_inode(file: &File) -> Result<(), TryLockError> {
use std::os::windows::io::AsRawHandle;
#[repr(C)]
struct Overlapped {
internal: usize,
internal_high: usize,
offset: u32,
offset_high: u32,
h_event: isize,
}
const LOCKFILE_FAIL_IMMEDIATELY: u32 = 0x0000_0001;
const LOCKFILE_EXCLUSIVE_LOCK: u32 = 0x0000_0002;
const ERROR_LOCK_VIOLATION: i32 = 33;
#[link(name = "kernel32")]
unsafe extern "system" {
fn LockFileEx(
hFile: isize,
dwFlags: u32,
dwReserved: u32,
nNumberOfBytesToLockLow: u32,
nNumberOfBytesToLockHigh: u32,
lpOverlapped: *mut Overlapped,
) -> i32;
}
#[allow(clippy::cast_sign_loss)]
let offset = DATABASE_INODE_LOCK_OFFSET as u64;
let mut overlapped = Overlapped {
internal: 0,
internal_high: 0,
offset: (offset & 0xFFFF_FFFF) as u32,
offset_high: (offset >> 32) as u32,
h_event: 0,
};
let ok = unsafe {
LockFileEx(
file.as_raw_handle() as isize,
LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
0,
1,
0,
&raw mut overlapped,
)
};
if ok != 0 {
return Ok(());
}
let error = std::io::Error::last_os_error();
match error.raw_os_error() {
Some(ERROR_LOCK_VIOLATION) => Err(TryLockError::WouldBlock),
_ => Err(TryLockError::Error(error)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn temp_file(dir: &std::path::Path) -> File {
let path = dir.join("inode-lock-test.db");
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.expect("open temp lock file");
file.write_all(b"x").expect("seed file");
file
}
#[test]
fn lock_is_exclusive_across_open_file_descriptions() {
let dir = tempfile::tempdir().expect("tempdir");
let first = temp_file(dir.path());
let second = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(dir.path().join("inode-lock-test.db"))
.expect("second open");
try_lock_database_inode(&first).expect("first lock must succeed");
assert!(
matches!(
try_lock_database_inode(&second),
Err(TryLockError::WouldBlock)
),
"second open file description must observe the held lock"
);
drop(first);
try_lock_database_inode(&second).expect("lock must be acquirable after the holder closes");
}
#[test]
fn relock_on_same_file_succeeds() {
let dir = tempfile::tempdir().expect("tempdir");
let file = temp_file(dir.path());
try_lock_database_inode(&file).expect("initial lock");
try_lock_database_inode(&file).expect("re-lock on the same open file description");
}
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "macos",
target_os = "ios"
))]
#[test]
#[allow(unsafe_code)]
fn coexists_with_sqlite_engine_record_locks() {
use std::os::fd::AsRawFd;
let dir = tempfile::tempdir().expect("tempdir");
let authority = temp_file(dir.path());
try_lock_database_inode(&authority).expect("authority lock");
let engine = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(dir.path().join("inode-lock-test.db"))
.expect("engine open");
let lock = libc::flock {
l_type: libc::c_short::try_from(libc::F_WRLCK).unwrap_or(1),
l_whence: libc::c_short::try_from(libc::SEEK_SET).unwrap_or(0),
l_start: 0x4000_0000,
l_len: 2,
l_pid: 0,
};
let rc = unsafe { libc::fcntl(engine.as_raw_fd(), libc::F_OFD_SETLK, &lock) };
assert_eq!(
rc,
0,
"engine-range record lock must not observe the inode authority: {}",
std::io::Error::last_os_error()
);
}
}