diff --git a/crates/spaceshd/src/lifecycle.rs b/crates/spaceshd/src/lifecycle.rs new file mode 100644 index 0000000..5ed2da6 --- /dev/null +++ b/crates/spaceshd/src/lifecycle.rs @@ -0,0 +1,70 @@ +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); + } +} diff --git a/crates/spaceshd/src/main.rs b/crates/spaceshd/src/main.rs index 83ba4c1..cdb9fd3 100644 --- a/crates/spaceshd/src/main.rs +++ b/crates/spaceshd/src/main.rs @@ -1,3 +1,5 @@ +mod lifecycle; + fn main() { println!("spaceshd skeleton"); }