use std::fs::{File, OpenOptions};
use std::io;
use std::path::{Path, PathBuf};
use fs4::fs_std::FileExt;
#[derive(Debug, Clone)]
pub struct LockHolder {
pub pid: u32,
pub kind: String, }
impl LockHolder {
fn serialize(&self) -> String {
format!("{}\n{}\n", self.pid, self.kind)
}
fn deserialize(s: &str) -> Option<Self> {
let mut lines = s.lines();
let pid: u32 = lines.next()?.parse().ok()?;
let kind = lines.next().unwrap_or("unknown").to_string();
Some(Self { pid, kind })
}
}
#[derive(Debug)]
pub struct SessionLock {
_file: File,
path: PathBuf,
}
impl SessionLock {
pub fn release_and_remove(self) {
let path = self.path.clone();
drop(self);
let _ = std::fs::remove_file(path);
}
pub fn try_acquire(dir: &Path, id: &str, holder: LockHolder) -> Result<Self, SessionLockError> {
if let Ok(Some(successor)) = crate::session::read_compacted_into_in_dir(dir, id) {
return Err(SessionLockError::CompactedInto {
session_id: id.to_string(),
successor_id: successor,
});
}
let path = dir.join(format!("{}.lock", id));
std::fs::create_dir_all(dir).map_err(|e| SessionLockError::Io(e, path.clone()))?;
let file = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&path)
.map_err(|e| SessionLockError::Io(e, path.clone()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
}
if FileExt::try_lock_exclusive(&file).map_err(|e| SessionLockError::Io(e, path.clone()))? {
use std::io::Write;
let _ = file.set_len(0);
let _ = (&file).write_all(holder.serialize().as_bytes());
Ok(Self { _file: file, path })
} else {
let body = std::fs::read_to_string(&path).unwrap_or_default();
let existing = LockHolder::deserialize(&body);
Err(SessionLockError::Held {
session_id: id.to_string(),
holder: existing,
})
}
}
pub fn path(&self) -> &Path {
&self.path
}
}
pub fn lock_path(dir: &Path, id: &str) -> PathBuf {
dir.join(format!("{}.lock", id))
}
pub fn sessions_dir() -> PathBuf {
crate::config::get_active_config_dir().join("sessions")
}
#[derive(Debug)]
pub enum SessionLockError {
Io(io::Error, PathBuf),
Held {
session_id: String,
holder: Option<LockHolder>,
},
CompactedInto {
session_id: String,
successor_id: String,
},
}
impl std::fmt::Display for SessionLockError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e, path) => write!(f, "session lock I/O error on {}: {}", path.display(), e),
Self::Held {
session_id,
holder: Some(h),
} => write!(
f,
"session {} is live in another process (pid {}, {}) \
— use `synaps --attach {}`, or `synaps daemon sessions`",
session_id, h.pid, h.kind, session_id
),
Self::Held {
session_id,
holder: None,
} => write!(
f,
"session {} is live in another process \
— use `synaps --attach {}`, or `synaps daemon sessions`",
session_id, session_id
),
Self::CompactedInto {
session_id,
successor_id,
} => write!(
f,
"session {} was compacted into {} \
— continue {} instead",
session_id, successor_id, successor_id
),
}
}
}
impl std::error::Error for SessionLockError {}
#[cfg(test)]
mod tests {
use super::*;
fn holder(kind: &str) -> LockHolder {
LockHolder {
pid: std::process::id(),
kind: kind.to_string(),
}
}
#[test]
fn take_and_refuse() {
let dir = tempfile::tempdir().unwrap();
let lock = SessionLock::try_acquire(dir.path(), "abc123", holder("tui")).unwrap();
let err = SessionLock::try_acquire(dir.path(), "abc123", holder("tui")).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("abc123"), "error names the session: {msg}");
assert!(
msg.contains(&format!("pid {}", std::process::id())),
"error names the pid: {msg}"
);
drop(lock);
SessionLock::try_acquire(dir.path(), "abc123", holder("tui")).unwrap();
}
#[test]
fn different_sessions_independent() {
let dir = tempfile::tempdir().unwrap();
let _a = SessionLock::try_acquire(dir.path(), "aaa", holder("tui")).unwrap();
let _b = SessionLock::try_acquire(dir.path(), "bbb", holder("tui")).unwrap();
}
#[test]
fn stale_lock_from_dead_process_does_not_block() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("stale.lock");
std::fs::write(&path, "99999\ndaemon\n").unwrap();
let _lock = SessionLock::try_acquire(dir.path(), "stale", holder("tui")).unwrap();
}
}