Files
spaceshell/crates/spaceshd/src/lifecycle.rs
T
2026-06-09 19:57:11 +07:00

71 lines
2.1 KiB
Rust

use std::path::PathBuf;
use anyhow::{Context, Result};
/// `~/.spacesh` directory, created if missing.
pub fn spacesh_dir() -> Result<PathBuf> {
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<PathBuf> {
Ok(spacesh_dir()?.join("sock"))
}
pub fn lock_path() -> Result<PathBuf> {
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<Option<InstanceLock>> {
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);
}
}