use serde::{Deserialize, Serialize};
use crate::session::scoping::session_dir;
#[derive(Debug, Serialize, Deserialize)]
pub struct LeaseFile {
pub pid: u32,
pub start_time: u64,
pub last_heartbeat: u64,
}
impl LeaseFile {
pub fn write(session_key: &str) -> std::io::Result<()> {
let path = lease_path(session_key);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let now = now_secs();
let lease = LeaseFile {
pid: std::process::id(),
start_time: now,
last_heartbeat: now,
};
let json = serde_json::to_string_pretty(&lease)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
std::fs::write(&path, json)
}
pub fn read(session_key: &str) -> Option<LeaseFile> {
let path = lease_path(session_key);
let contents = std::fs::read_to_string(&path).ok()?;
serde_json::from_str(&contents).ok()
}
pub fn update_heartbeat(session_key: &str) -> std::io::Result<()> {
let path = lease_path(session_key);
let contents = std::fs::read_to_string(&path)?;
let mut lease: LeaseFile = serde_json::from_str(&contents)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
lease.last_heartbeat = now_secs();
let json = serde_json::to_string_pretty(&lease)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
std::fs::write(&path, json)
}
pub fn remove(session_key: &str) {
let _ = std::fs::remove_file(lease_path(session_key));
}
pub fn is_valid(&self, ttl_secs: u64) -> bool {
let now = now_secs();
if now.saturating_sub(self.last_heartbeat) > ttl_secs {
return false;
}
is_process_alive(self.pid)
}
}
fn lease_path(session_key: &str) -> std::path::PathBuf {
session_dir().join(format!("{session_key}.lease"))
}
fn now_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
fn is_process_alive(pid: u32) -> bool {
let ret = unsafe { libc::kill(pid as libc::pid_t, 0) };
if ret == 0 {
return true;
}
std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn current_process_is_alive() {
assert!(is_process_alive(std::process::id()));
}
#[test]
fn expired_lease_is_invalid() {
let lease = LeaseFile {
pid: std::process::id(),
start_time: 1000,
last_heartbeat: 1000,
};
assert!(!lease.is_valid(60));
}
#[test]
fn fresh_lease_with_live_pid_is_valid() {
let now = now_secs();
let lease = LeaseFile {
pid: std::process::id(),
start_time: now,
last_heartbeat: now,
};
assert!(lease.is_valid(60));
}
#[test]
fn lease_with_dead_pid_is_invalid() {
let now = now_secs();
let lease = LeaseFile {
pid: 4_000_000, start_time: now,
last_heartbeat: now,
};
assert!(!lease.is_valid(60));
}
}