use std::fs::{File, OpenOptions};
use std::io;
use std::path::Path;
use fs4::fs_std::FileExt;
use serde::{Deserialize, Serialize};
pub use crate::events::registry::{daemon_paths, daemon_paths_in, DaemonPaths};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DaemonInfo {
pub pid: u32,
pub protocol_version: u32,
pub daemon_version: String,
pub profile: Option<String>,
pub started_at: chrono::DateTime<chrono::Utc>,
pub socket: String,
#[serde(default)]
pub exe: Option<std::path::PathBuf>,
#[serde(default = "one")]
pub generation: u64,
}
fn one() -> u64 {
1
}
#[cfg(unix)]
fn chmod_600(path: &Path) {
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
}
#[cfg(not(unix))]
fn chmod_600(_path: &Path) {}
pub fn write_daemon_json(paths: &DaemonPaths, info: &DaemonInfo) -> io::Result<()> {
let body = serde_json::to_vec_pretty(info).map_err(io::Error::other)?;
write_atomic(&paths.json, &body)?;
write_atomic(&paths.pid, format!("{}\n", info.pid).as_bytes())
}
fn write_atomic(path: &Path, body: &[u8]) -> io::Result<()> {
let tmp = path.with_extension("tmp");
std::fs::write(&tmp, body)?;
chmod_600(&tmp);
std::fs::rename(&tmp, path)
}
pub fn write_private_atomic(path: &Path, body: &[u8]) -> io::Result<()> {
write_atomic(path, body)
}
pub fn read_daemon_json(paths: &DaemonPaths) -> Option<DaemonInfo> {
let s = std::fs::read_to_string(&paths.json).ok()?;
serde_json::from_str(&s).ok()
}
pub struct DaemonLock {
file: File,
}
impl DaemonLock {
pub fn try_acquire(paths: &DaemonPaths) -> io::Result<Option<Self>> {
let file = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&paths.lock)?;
chmod_600(&paths.lock);
if FileExt::try_lock_exclusive(&file)? {
Ok(Some(Self { file }))
} else {
Ok(None)
}
}
}
impl DaemonLock {
#[cfg(unix)]
pub fn adopt(fd: i32) -> io::Result<Self> {
use std::os::unix::io::FromRawFd;
if fd < 0 {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "negative lock fd"));
}
let file = unsafe { File::from_raw_fd(fd) };
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFD);
if flags >= 0 {
libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC);
}
}
Ok(Self { file })
}
#[cfg(unix)]
pub fn raw_fd(&self) -> i32 {
use std::os::unix::io::AsRawFd;
self.file.as_raw_fd()
}
}
impl Drop for DaemonLock {
fn drop(&mut self) {
let _ = FileExt::unlock(&self.file);
}
}
impl DaemonPaths {
pub fn spawn_lock(&self) -> std::path::PathBuf {
self.lock.with_extension("spawn.lock")
}
}
#[derive(Debug)]
pub struct SpawnLock {
file: File,
}
impl SpawnLock {
pub fn acquire(paths: &DaemonPaths, wait: std::time::Duration) -> io::Result<Self> {
let path = paths.spawn_lock();
let file = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&path)?;
chmod_600(&path);
let deadline = std::time::Instant::now() + wait;
loop {
if FileExt::try_lock_exclusive(&file)? {
return Ok(Self { file });
}
if std::time::Instant::now() >= deadline {
return Err(io::Error::new(io::ErrorKind::TimedOut, "another client is still spawning the daemon"));
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
}
impl Drop for SpawnLock {
fn drop(&mut self) {
let _ = FileExt::unlock(&self.file);
}
}
pub fn is_alive(paths: &DaemonPaths) -> bool {
if !paths.lock.exists() {
return false;
}
match DaemonLock::try_acquire(paths) {
Ok(Some(_guard)) => false,
Ok(None) => true,
Err(_) => false,
}
}
pub fn reap_stale(paths: &DaemonPaths) -> bool {
if is_alive(paths) {
return false;
}
let mut reaped = false;
for p in [&paths.sock, &paths.json, &paths.pid] {
if p.exists() {
crate::events::socket::cleanup_socket(&p.to_string_lossy());
reaped |= !p.exists();
}
}
reaped
}
#[cfg(test)]
mod tests {
use super::*;
fn paths() -> (tempfile::TempDir, DaemonPaths) {
let d = tempfile::tempdir().unwrap();
let p = daemon_paths_in(d.path(), None);
(d, p)
}
#[test]
fn lock_is_the_liveness_oracle() {
let (_d, p) = paths();
assert!(!is_alive(&p));
let guard = DaemonLock::try_acquire(&p).unwrap().expect("first holder");
assert!(is_alive(&p));
assert!(DaemonLock::try_acquire(&p).unwrap().is_none(), "second holder refused");
drop(guard);
assert!(!is_alive(&p));
}
#[test]
fn spawn_lock_is_exclusive_and_waits() {
let (_d, p) = paths();
let a = SpawnLock::acquire(&p, std::time::Duration::from_millis(10)).unwrap();
let err = SpawnLock::acquire(&p, std::time::Duration::from_millis(120)).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::TimedOut);
drop(a);
SpawnLock::acquire(&p, std::time::Duration::from_millis(10)).unwrap();
assert!(!is_alive(&p), "the spawn lock is not the liveness oracle");
}
#[test]
fn stale_files_reaped_only_without_holder() {
let (_d, p) = paths();
std::fs::write(&p.sock, b"").unwrap();
let info = DaemonInfo {
pid: 4_000_000,
protocol_version: 1,
daemon_version: "t".into(),
profile: None,
started_at: chrono::Utc::now(),
socket: p.sock.to_string_lossy().into_owned(),
exe: None,
generation: 1,
};
write_daemon_json(&p, &info).unwrap();
assert_eq!(read_daemon_json(&p).unwrap(), info);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(std::fs::metadata(&p.json).unwrap().permissions().mode() & 0o777, 0o600);
}
let guard = DaemonLock::try_acquire(&p).unwrap().unwrap();
assert!(!reap_stale(&p), "live holder: nothing reaped");
assert!(p.sock.exists());
drop(guard);
assert!(reap_stale(&p));
assert!(!p.sock.exists() && !p.json.exists() && !p.pid.exists());
}
}