21180ae9e0
lock_is_exclusive_within_process acquired the global ~/.spacesh/daemon.lock, so it flaked whenever a real daemon was running. Add a SPACESH_LOCK env override to lock_path() and point the test at a private temp file under a serial() guard, making the suite deterministic regardless of a live daemon. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
100 lines
3.3 KiB
Rust
100 lines
3.3 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> {
|
|
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<PathBuf> {
|
|
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<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 _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"));
|
|
}
|
|
}
|