use std::path::PathBuf; use anyhow::{Context, Result}; /// `~/.spacesh` directory, created if missing. pub fn spacesh_dir() -> Result { let home = dirs::home_dir().context("no home dir")?; let dir = home.join(".spacesh"); std::fs::create_dir_all(&dir)?; Ok(dir) } pub fn socket_path() -> Result { Ok(spacesh_dir()?.join("sock")) } pub fn lock_path() -> Result { Ok(spacesh_dir()?.join("daemon.lock")) } /// Hold the single-instance lock for the lifetime of the daemon. pub struct InstanceLock { _file: std::fs::File, } /// Acquire the exclusive daemon lock. Returns `Ok(None)` if another live daemon holds it. pub fn acquire_instance_lock() -> Result> { use fs2::FileExt; let file = std::fs::OpenOptions::new() .create(true) .write(true) .open(lock_path()?)?; match file.try_lock_exclusive() { Ok(()) => Ok(Some(InstanceLock { _file: file })), Err(_) => Ok(None), } } /// If a stale socket file exists but no daemon answers, remove it so we can bind. pub fn clear_stale_socket() -> Result<()> { let path = socket_path()?; if path.exists() { // We hold the instance lock, so any existing socket is stale. std::fs::remove_file(&path)?; } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn paths_live_under_spacesh_dir() { let dir = spacesh_dir().unwrap(); assert!(socket_path().unwrap().starts_with(&dir)); assert!(lock_path().unwrap().starts_with(&dir)); } #[test] fn lock_is_exclusive_within_process() { let first = acquire_instance_lock().unwrap(); assert!(first.is_some(), "first acquire should succeed"); // A second attempt from the same process on the same fd path: // fs2 advisory locks are per-handle; opening a new handle and locking // should fail while `first` is held. let second = acquire_instance_lock().unwrap(); assert!(second.is_none(), "second acquire should be blocked"); drop(first); } }