1use serde::{Deserialize, Serialize};
2
3use crate::session::scoping::session_dir;
4
5#[derive(Debug, Serialize, Deserialize)]
11pub struct LeaseFile {
12 pub pid: u32,
14 pub start_time: u64,
16 pub last_heartbeat: u64,
18}
19
20impl LeaseFile {
21 pub fn write(session_key: &str) -> std::io::Result<()> {
23 let path = lease_path(session_key);
24 if let Some(parent) = path.parent() {
25 std::fs::create_dir_all(parent)?;
26 }
27 let now = now_secs();
28 let lease = LeaseFile {
29 pid: std::process::id(),
30 start_time: now,
31 last_heartbeat: now,
32 };
33 let json = serde_json::to_string_pretty(&lease)
34 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
35 std::fs::write(&path, json)
36 }
37
38 pub fn read(session_key: &str) -> Option<LeaseFile> {
41 let path = lease_path(session_key);
42 let contents = std::fs::read_to_string(&path).ok()?;
43 serde_json::from_str(&contents).ok()
44 }
45
46 pub fn update_heartbeat(session_key: &str) -> std::io::Result<()> {
48 let path = lease_path(session_key);
49 let contents = std::fs::read_to_string(&path)?;
50 let mut lease: LeaseFile = serde_json::from_str(&contents)
51 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
52 lease.last_heartbeat = now_secs();
53 let json = serde_json::to_string_pretty(&lease)
54 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
55 std::fs::write(&path, json)
56 }
57
58 pub fn remove(session_key: &str) {
60 let _ = std::fs::remove_file(lease_path(session_key));
61 }
62
63 pub fn is_valid(&self, ttl_secs: u64) -> bool {
67 let now = now_secs();
68 if now.saturating_sub(self.last_heartbeat) > ttl_secs {
69 return false;
70 }
71 is_process_alive(self.pid)
72 }
73}
74
75fn lease_path(session_key: &str) -> std::path::PathBuf {
77 session_dir().join(format!("{session_key}.lease"))
78}
79
80fn now_secs() -> u64 {
82 std::time::SystemTime::now()
83 .duration_since(std::time::UNIX_EPOCH)
84 .unwrap_or_default()
85 .as_secs()
86}
87
88fn is_process_alive(pid: u32) -> bool {
90 let ret = unsafe { libc::kill(pid as libc::pid_t, 0) };
91 if ret == 0 {
92 return true;
93 }
94 std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn current_process_is_alive() {
103 assert!(is_process_alive(std::process::id()));
104 }
105
106 #[test]
107 fn expired_lease_is_invalid() {
108 let lease = LeaseFile {
109 pid: std::process::id(),
110 start_time: 1000,
111 last_heartbeat: 1000,
112 };
113 assert!(!lease.is_valid(60));
115 }
116
117 #[test]
118 fn fresh_lease_with_live_pid_is_valid() {
119 let now = now_secs();
120 let lease = LeaseFile {
121 pid: std::process::id(),
122 start_time: now,
123 last_heartbeat: now,
124 };
125 assert!(lease.is_valid(60));
126 }
127
128 #[test]
129 fn lease_with_dead_pid_is_invalid() {
130 let now = now_secs();
131 let lease = LeaseFile {
132 pid: 4_000_000, start_time: now,
134 last_heartbeat: now,
135 };
136 assert!(!lease.is_valid(60));
137 }
138}