use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use fs2::FileExt;
use crate::error::{AppError, AppResult};
type MetadataWriter = fn(&mut File, &str, Option<(&str, &str)>) -> AppResult<()>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DaemonLeaseOwner {
pub pid: u32,
pub resource: String,
pub executable: PathBuf,
pub instance_id: Option<String>,
pub handoff_token: Option<String>,
}
#[derive(Debug)]
pub struct FileLease {
file: File,
path: PathBuf,
}
impl FileLease {
pub fn acquire(path: PathBuf, resource: &str) -> AppResult<Self> {
Self::acquire_with(path, resource, try_lock_exclusive)
}
pub fn acquire_daemon(
path: PathBuf,
resource: &str,
instance_id: &str,
handoff_token: &str,
) -> AppResult<Self> {
Self::acquire_with_metadata(
path,
resource,
Some((instance_id, handoff_token)),
try_lock_exclusive,
)
}
fn acquire_with(
path: PathBuf,
resource: &str,
try_lock: fn(&File) -> std::io::Result<()>,
) -> AppResult<Self> {
Self::acquire_with_metadata(path, resource, None, try_lock)
}
fn acquire_with_metadata(
path: PathBuf,
resource: &str,
daemon_identity: Option<(&str, &str)>,
try_lock: fn(&File) -> std::io::Result<()>,
) -> AppResult<Self> {
Self::acquire_with_metadata_using(
path,
resource,
daemon_identity,
try_lock,
restrict_lock_permissions,
write_metadata,
)
}
fn acquire_with_metadata_using(
path: PathBuf,
resource: &str,
daemon_identity: Option<(&str, &str)>,
try_lock: fn(&File) -> std::io::Result<()>,
restrict: fn(&File) -> AppResult<()>,
write: MetadataWriter,
) -> AppResult<Self> {
let parent = path.parent().unwrap_or(Path::new("."));
std::fs::create_dir_all(parent)?;
let mut file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(&path)?;
restrict(&file)?;
match try_lock(&file) {
Ok(()) => {
write(&mut file, resource, daemon_identity)?;
Ok(Self { file, path })
}
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
let holder = read_metadata(&mut file);
Err(AppError::Busy {
resource: resource.to_owned(),
holder,
})
}
Err(error) => Err(AppError::Io(error)),
}
}
pub fn path(&self) -> &Path {
&self.path
}
}
fn try_lock_exclusive(file: &File) -> std::io::Result<()> {
file.try_lock_exclusive()
}
impl Drop for FileLease {
fn drop(&mut self) {
let _ = FileExt::unlock(&self.file);
}
}
pub fn daemon_lock_path(common_db_path: &Path) -> PathBuf {
common_db_path
.parent()
.unwrap_or(Path::new("."))
.join("daemon.lock")
}
pub fn database_lock_path(db_path: &Path) -> PathBuf {
let mut value = db_path.as_os_str().to_os_string();
value.push(".lock");
PathBuf::from(value)
}
pub fn held_daemon_owner(path: &Path) -> AppResult<Option<DaemonLeaseOwner>> {
held_daemon_owner_with(
OpenOptions::new().read(true).write(true).open(path),
try_lock_exclusive,
unlock_file,
)
}
fn held_daemon_owner_with(
file: std::io::Result<File>,
try_lock: fn(&File) -> std::io::Result<()>,
unlock: fn(&File) -> std::io::Result<()>,
) -> AppResult<Option<DaemonLeaseOwner>> {
let mut file = match file {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(AppError::Io(error)),
};
match try_lock(&file) {
Ok(()) => {
unlock(&file)?;
Ok(None)
}
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
let metadata = read_metadata_text(&mut file)?;
parse_daemon_owner(&metadata).map(Some)
}
Err(error) => Err(AppError::Io(error)),
}
}
fn unlock_file(file: &File) -> std::io::Result<()> {
FileExt::unlock(file)
}
fn write_metadata(
file: &mut File,
resource: &str,
daemon_identity: Option<(&str, &str)>,
) -> AppResult<()> {
write_metadata_using(
file,
resource,
daemon_identity,
set_file_length,
seek_file,
write_file,
sync_file,
)
}
fn write_metadata_using(
file: &mut File,
resource: &str,
daemon_identity: Option<(&str, &str)>,
set_length: fn(&mut File, u64) -> std::io::Result<()>,
seek: fn(&mut File, SeekFrom) -> std::io::Result<u64>,
write: fn(&mut File, &[u8]) -> std::io::Result<()>,
sync: fn(&File) -> std::io::Result<()>,
) -> AppResult<()> {
let executable = executable_path(std::env::current_exe());
let mut metadata = format!(
"pid={}\nresource={}\nexecutable={}\n",
std::process::id(),
resource,
executable
);
if let Some((instance_id, handoff_token)) = daemon_identity {
metadata.push_str(&format!(
"instance_id={instance_id}\nhandoff_token={handoff_token}\n"
));
}
set_length(file, 0)?;
seek(file, SeekFrom::Start(0))?;
write(file, metadata.as_bytes())?;
sync(file)?;
Ok(())
}
fn set_file_length(file: &mut File, length: u64) -> std::io::Result<()> {
file.set_len(length)
}
fn seek_file(file: &mut File, position: SeekFrom) -> std::io::Result<u64> {
file.seek(position)
}
fn write_file(file: &mut File, bytes: &[u8]) -> std::io::Result<()> {
file.write_all(bytes)
}
fn sync_file(file: &File) -> std::io::Result<()> {
file.sync_all()
}
fn executable_path(result: std::io::Result<PathBuf>) -> String {
result
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_else(|_| "unknown".to_owned())
}
fn read_metadata(file: &mut File) -> String {
match read_metadata_text(file)
.ok()
.filter(|metadata| !metadata.trim().is_empty())
{
Some(metadata) => format!(
" ({})",
metadata
.lines()
.filter(|line| !line.starts_with("handoff_token="))
.collect::<Vec<_>>()
.join("\n")
.trim()
),
None => " (owner metadata unavailable)".to_owned(),
}
}
fn read_metadata_text(file: &mut File) -> AppResult<String> {
read_metadata_text_using(file, seek_file, read_file)
}
fn read_metadata_text_using(
file: &mut File,
seek: fn(&mut File, SeekFrom) -> std::io::Result<u64>,
read: fn(&mut File, &mut String) -> std::io::Result<usize>,
) -> AppResult<String> {
let mut metadata = String::new();
seek(file, SeekFrom::Start(0))?;
read(file, &mut metadata)?;
Ok(metadata)
}
fn read_file(file: &mut File, metadata: &mut String) -> std::io::Result<usize> {
file.read_to_string(metadata)
}
fn parse_daemon_owner(metadata: &str) -> AppResult<DaemonLeaseOwner> {
let value = |name: &str| {
metadata
.lines()
.find_map(|line| line.strip_prefix(&format!("{name}=")))
};
let pid = value("pid")
.ok_or_else(|| AppError::Runtime("daemon lease metadata is missing pid".to_owned()))?
.parse::<u32>()
.map_err(|_| AppError::Runtime("daemon lease metadata has an invalid pid".to_owned()))?;
if pid == 0 {
return Err(AppError::Runtime(
"daemon lease metadata has an invalid pid".to_owned(),
));
}
let executable = value("executable")
.filter(|path| !path.is_empty() && *path != "unknown")
.ok_or_else(|| {
AppError::Runtime("daemon lease metadata is missing executable identity".to_owned())
})?;
Ok(DaemonLeaseOwner {
pid,
resource: value("resource")
.filter(|resource| !resource.is_empty())
.ok_or_else(|| {
AppError::Runtime("daemon lease metadata is missing resource identity".to_owned())
})?
.to_owned(),
executable: PathBuf::from(executable),
instance_id: value("instance_id").map(str::to_owned),
handoff_token: value("handoff_token").map(str::to_owned),
})
}
#[cfg(unix)]
fn restrict_lock_permissions(file: &File) -> AppResult<()> {
restrict_lock_permissions_using(file, file_metadata, set_file_permissions)
}
#[cfg(unix)]
fn restrict_lock_permissions_using(
file: &File,
metadata: fn(&File) -> std::io::Result<std::fs::Metadata>,
set_permissions: fn(&File, std::fs::Permissions) -> std::io::Result<()>,
) -> AppResult<()> {
use std::os::unix::fs::PermissionsExt;
let mut permissions = metadata(file)?.permissions();
permissions.set_mode(0o600);
set_permissions(file, permissions)?;
Ok(())
}
#[cfg(unix)]
fn file_metadata(file: &File) -> std::io::Result<std::fs::Metadata> {
file.metadata()
}
#[cfg(unix)]
fn set_file_permissions(file: &File, permissions: std::fs::Permissions) -> std::io::Result<()> {
file.set_permissions(permissions)
}
#[cfg(not(unix))]
fn restrict_lock_permissions(_: &File) -> AppResult<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[test]
fn leases_are_exclusive_and_released_on_drop() {
let directory = tempfile::tempdir().expect("tempdir");
let path = directory.path().join("daemon.lock");
let first = FileLease::acquire(path.clone(), "test daemon").expect("first lease");
let second = FileLease::acquire(path.clone(), "test daemon");
let _ = second.expect_err("second lease should be busy");
drop(first);
let reacquired = FileLease::acquire(path, "test daemon").expect("reacquire");
assert!(reacquired.path().exists());
}
#[test]
fn an_unlocked_stale_file_is_reusable_without_pid_guessing() {
let directory = tempfile::tempdir().expect("tempdir");
let path = directory.path().join("database.duckdb.lock");
std::fs::write(&path, "pid=999999\nresource=old\n").expect("stale metadata");
let lease = FileLease::acquire(path.clone(), "database").expect("stale lease recovery");
let metadata = std::fs::read_to_string(path).expect("metadata");
assert!(metadata.contains(&format!("pid={}", std::process::id())));
assert!(lease.path().ends_with("database.duckdb.lock"));
}
#[test]
fn held_daemon_identity_requires_a_live_lease() {
let directory = tempfile::tempdir().expect("tempdir");
let path = directory.path().join("daemon.lock");
assert_eq!(held_daemon_owner(&path).expect("missing owner"), None);
let lease =
FileLease::acquire_daemon(path.clone(), "test daemon", "instance-1", "handoff-secret")
.expect("daemon lease");
let owner = held_daemon_owner(&path)
.expect("held owner")
.expect("owner metadata");
assert_eq!(owner.pid, std::process::id());
assert_eq!(owner.resource, "test daemon");
assert_eq!(owner.instance_id.as_deref(), Some("instance-1"));
assert_eq!(owner.handoff_token.as_deref(), Some("handoff-secret"));
assert_eq!(
owner.executable,
std::env::current_exe().expect("executable")
);
let busy =
FileLease::acquire_daemon(path.clone(), "test daemon", "instance-2", "another-secret")
.expect_err("second daemon lease");
let diagnostic = busy.to_string();
assert!(diagnostic.contains("instance_id=instance-1"));
assert!(!diagnostic.contains("handoff-secret"));
#[cfg(unix)]
assert_eq!(
std::fs::metadata(&path)
.expect("metadata")
.permissions()
.mode()
& 0o777,
0o600
);
drop(lease);
assert_eq!(held_daemon_owner(&path).expect("released owner"), None);
}
#[test]
fn lock_parent_creation_and_unexpected_lock_errors_are_preserved() {
let directory = tempfile::tempdir().expect("tempdir");
let nested = directory.path().join("new").join("nested").join("lock");
let lease = FileLease::acquire(nested.clone(), "nested").expect("nested lease");
assert!(nested.exists());
drop(lease);
let error = FileLease::acquire_with(
directory.path().join("unexpected.lock"),
"unexpected",
|_| Err(std::io::Error::other("lock provider failure")),
)
.expect_err("unexpected lock error");
let _ = error;
}
#[test]
fn busy_lock_without_metadata_has_a_safe_fallback_message() {
let directory = tempfile::tempdir().expect("tempdir");
let path = directory.path().join("empty.lock");
let file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(&path)
.expect("lock file");
file.try_lock_exclusive().expect("raw lease");
let error = FileLease::acquire(path, "empty").expect_err("busy raw lease");
let _ = error;
FileExt::unlock(&file).expect("unlock raw lease");
}
#[test]
fn malformed_held_daemon_metadata_fails_closed() {
let directory = tempfile::tempdir().expect("tempdir");
let path = directory.path().join("malformed.lock");
let mut file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(&path)
.expect("lock file");
file.write_all(b"resource=unknown\n")
.expect("metadata write");
file.try_lock_exclusive().expect("raw lease");
assert!(held_daemon_owner(&path).is_err());
FileExt::unlock(&file).expect("unlock raw lease");
}
#[test]
fn daemon_owner_errors_and_required_identity_fields_fail_closed() {
let _ = held_daemon_owner_with(
Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)),
try_lock_exclusive,
unlock_file,
)
.expect_err("permission error should be preserved");
let directory = tempfile::tempdir().expect("tempdir");
let path = directory.path().join("unexpected.lock");
let file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(path)
.expect("lock file");
let _ = held_daemon_owner_with(
Ok(file),
|_| Err(std::io::Error::other("lock failure")),
unlock_file,
)
.expect_err("lock error should be preserved");
assert!(parse_daemon_owner("pid=0\nresource=daemon\nexecutable=/bin/daemon\n").is_err());
assert!(
parse_daemon_owner("pid=not-a-number\nresource=daemon\nexecutable=/bin/daemon\n")
.is_err()
);
assert!(parse_daemon_owner("pid=1\nresource=daemon\n").is_err());
assert!(parse_daemon_owner("pid=1\nexecutable=/bin/daemon\n").is_err());
let directory = tempfile::tempdir().expect("tempdir");
assert!(
FileLease::acquire(
PathBuf::from("/dev/null/coverage-mcp.lock"),
"invalid parent",
)
.is_err()
);
let child_directory = directory.path().join("directory.lock");
std::fs::create_dir(&child_directory).expect("directory lock target");
assert!(FileLease::acquire(child_directory, "directory target").is_err());
let injected_target = directory.path().join("injected.lock");
let _ = FileLease::acquire_with_metadata_using(
injected_target.clone(),
"injected permissions",
None,
try_lock_exclusive,
|_| Err(AppError::Runtime("permission seam failure".to_owned())),
write_metadata,
)
.expect_err("permission seam should fail");
let _ = FileLease::acquire_with_metadata_using(
injected_target,
"injected metadata",
None,
|_| Ok(()),
restrict_lock_permissions,
|_, _, _| Err(AppError::Runtime("metadata seam failure".to_owned())),
)
.expect_err("metadata seam should fail");
#[cfg(unix)]
{
let permission_file = File::create(directory.path().join("permission-seams.lock"))
.expect("permission seam file");
let _ = restrict_lock_permissions_using(
&permission_file,
|_| Err(std::io::Error::other("metadata failure")),
set_file_permissions,
)
.expect_err("metadata failure should surface");
let _ = restrict_lock_permissions_using(&permission_file, file_metadata, |_, _| {
Err(std::io::Error::other("permissions failure"))
})
.expect_err("permissions failure should surface");
}
let mut metadata_file =
File::create(directory.path().join("metadata-seams.lock")).expect("metadata seam file");
let _ = write_metadata_using(
&mut metadata_file,
"seam",
None,
|_, _| Err(std::io::Error::other("length failure")),
seek_file,
write_file,
sync_file,
)
.expect_err("length seam should fail");
let _ = write_metadata_using(
&mut metadata_file,
"seam",
None,
set_file_length,
|_, _| Err(std::io::Error::other("seek failure")),
write_file,
sync_file,
)
.expect_err("seek seam should fail");
let _ = write_metadata_using(
&mut metadata_file,
"seam",
None,
set_file_length,
seek_file,
|_, _| Err(std::io::Error::other("write failure")),
sync_file,
)
.expect_err("write seam should fail");
let _ = write_metadata_using(
&mut metadata_file,
"seam",
None,
set_file_length,
seek_file,
write_file,
|_| Err(std::io::Error::other("sync failure")),
)
.expect_err("sync seam should fail");
let _ = read_metadata_text_using(
&mut metadata_file,
|_, _| Err(std::io::Error::other("seek failure")),
read_file,
)
.expect_err("read seek seam should fail");
let _ = read_metadata_text_using(&mut metadata_file, seek_file, |_, _| {
Err(std::io::Error::other("read failure"))
})
.expect_err("read seam should fail");
let write_only = std::fs::File::create(directory.path().join("write-only.lock"))
.expect("write-only lock");
let _ = held_daemon_owner_with(
Ok(write_only),
|_| Err(std::io::Error::from(std::io::ErrorKind::WouldBlock)),
unlock_file,
)
.expect_err("metadata read failure should surface");
let unlock_file =
std::fs::File::create(directory.path().join("unlock.lock")).expect("unlock lock");
let _ = held_daemon_owner_with(
Ok(unlock_file),
|_| Ok(()),
|_| Err(std::io::Error::other("unlock failure")),
)
.expect_err("unlock failure should surface");
}
#[test]
fn lock_paths_are_stable_and_scoped() {
let common = Path::new("/tmp/coverage/common.duckdb");
assert_eq!(
daemon_lock_path(common),
PathBuf::from("/tmp/coverage/daemon.lock")
);
assert_eq!(
database_lock_path(common),
PathBuf::from("/tmp/coverage/common.duckdb.lock")
);
}
#[test]
fn metadata_uses_a_safe_executable_fallback() {
assert_eq!(
executable_path(Err(std::io::Error::other("executable unavailable"))),
"unknown"
);
assert_eq!(
executable_path(Ok(PathBuf::from("/usr/bin/coverage-mcp"))),
"/usr/bin/coverage-mcp"
);
}
}