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 { if let Ok(p) = std::env::var("SPACESH_SOCK") { if !p.is_empty() { return Ok(PathBuf::from(p)); } } Ok(spacesh_dir()?.join("sock")) } pub fn lock_path() -> Result { if let Ok(p) = std::env::var("SPACESH_LOCK") { if !p.is_empty() { return Ok(PathBuf::from(p)); } } 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 _serial = crate::test_support::serial(); 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 _serial = crate::test_support::serial(); // Use a private lock path so a real running daemon (which holds the // global ~/.spacesh/daemon.lock) can't make this test flake. let tmp = std::env::temp_dir().join("spacesh-lock-exclusive-test.lock"); std::env::set_var("SPACESH_LOCK", &tmp); 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); std::env::remove_var("SPACESH_LOCK"); let _ = std::fs::remove_file(&tmp); } #[test] fn socket_path_honors_env_override() { let _serial = crate::test_support::serial(); // Note: set/remove around the assertion; serialized against the other // env-sensitive lifecycle test via the crate's serial() lock. std::env::set_var("SPACESH_SOCK", "/tmp/spacesh-test-override.sock"); let p = socket_path().unwrap(); std::env::remove_var("SPACESH_SOCK"); assert_eq!(p, std::path::PathBuf::from("/tmp/spacesh-test-override.sock")); } }