skiff_cli/session/
paths.rs1use std::fs;
4use std::path::{Path, PathBuf};
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::{Error, Result};
9use crate::paths::cache_dir;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SessionMeta {
13 pub pid: u32,
14 pub source: String,
15 pub transport: String,
16 pub created_at: f64,
17 #[serde(default)]
18 pub idle_secs: u64,
19 #[serde(default)]
21 pub last_activity_at: f64,
22}
23
24pub fn sessions_dir() -> PathBuf {
25 cache_dir().join("sessions")
26}
27
28pub fn session_meta_path(name: &str) -> PathBuf {
29 sessions_dir().join(format!("{name}.json"))
30}
31
32pub fn session_sock_path(name: &str) -> PathBuf {
33 sessions_dir().join(format!("{name}.sock"))
34}
35
36pub fn session_log_path(name: &str) -> PathBuf {
37 sessions_dir().join(format!("{name}.log"))
38}
39
40pub fn session_config_path(name: &str) -> PathBuf {
41 sessions_dir().join(format!("{name}.config.json"))
42}
43
44pub fn session_lock_path(name: &str) -> PathBuf {
46 sessions_dir().join(format!("{name}.lock"))
47}
48
49pub fn ensure_sessions_dir() -> Result<()> {
50 crate::fsutil::ensure_dir_0700(&sessions_dir())
51}
52
53pub fn is_process_alive(pid: u32) -> bool {
54 if pid == 0 {
55 return false;
56 }
57 let rc = unsafe { libc::kill(pid as i32, 0) };
59 rc == 0
60}
61
62pub fn session_is_alive(meta: &SessionMeta) -> bool {
63 is_process_alive(meta.pid)
64}
65
66pub fn load_meta(name: &str) -> Result<Option<SessionMeta>> {
67 let path = session_meta_path(name);
68 if !path.exists() {
69 return Ok(None);
70 }
71 let text = fs::read_to_string(&path)?;
72 match serde_json::from_str(&text) {
73 Ok(m) => Ok(Some(m)),
74 Err(_) => Ok(None),
75 }
76}
77
78pub fn write_meta(name: &str, meta: &SessionMeta) -> Result<()> {
79 ensure_sessions_dir()?;
80 let path = session_meta_path(name);
81 let json = serde_json::to_vec_pretty(meta)?;
82 crate::fsutil::atomic_write_0600(&path, &json)?;
83 Ok(())
84}
85
86pub fn unlink_session_files(name: &str) {
88 for p in [
89 session_meta_path(name),
90 session_sock_path(name),
91 session_log_path(name),
92 session_config_path(name),
93 ] {
94 let _ = fs::remove_file(p);
95 }
96}
97
98pub fn try_acquire_session_lock(name: &str) -> Result<bool> {
103 use std::io::Write;
104 let lock_path = session_lock_path(name);
105 for attempt in 0..2 {
106 let mut opts = fs::OpenOptions::new();
107 opts.write(true).create_new(true);
108 #[cfg(unix)]
109 {
110 use std::os::unix::fs::OpenOptionsExt;
111 opts.mode(0o600);
112 }
113 match opts.open(&lock_path) {
114 Ok(mut f) => {
115 let _ = write!(f, "{}", std::process::id());
116 let _ = f.sync_all();
117 return Ok(true);
118 }
119 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
120 if attempt == 0 {
121 let stale = match fs::read_to_string(&lock_path) {
124 Ok(s) => match s.trim().parse::<u32>() {
125 Ok(pid) => !is_process_alive(pid),
126 Err(_) => true,
127 },
128 Err(_) => true,
129 };
130 if stale {
131 let _ = fs::remove_file(&lock_path);
132 continue;
133 }
134 return Ok(false);
135 }
136 return Ok(false);
137 }
138 Err(e) => return Err(e.into()),
139 }
140 }
141 Ok(false)
142}
143
144pub fn release_session_lock(name: &str) {
146 let _ = fs::remove_file(session_lock_path(name));
147}
148
149pub fn clear_stale_session(name: &str) -> Result<bool> {
151 if let Some(meta) = load_meta(name)? {
152 if !session_is_alive(&meta) {
153 unlink_session_files(name);
154 return Ok(true);
155 }
156 return Ok(false);
157 }
158 let sock = session_sock_path(name);
160 if sock.exists() {
161 unlink_session_files(name);
162 return Ok(true);
163 }
164 Ok(false)
165}
166
167pub fn validate_session_name(name: &str) -> Result<()> {
168 if name.is_empty()
169 || name.contains('/')
170 || name.contains('\\')
171 || name.contains("..")
172 || name.contains('\0')
173 {
174 return Err(Error::usage(format!(
175 "invalid session name {name:?}; use a simple identifier"
176 )));
177 }
178 Ok(())
179}
180
181pub fn chmod_0600(path: &Path) -> Result<()> {
182 #[cfg(unix)]
183 {
184 use std::os::unix::fs::PermissionsExt;
185 fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
186 }
187 let _ = path;
188 Ok(())
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::paths::{set_cache_dir_override, TEST_PATHS_LOCK};
195 use tempfile::tempdir;
196
197 #[test]
198 fn paths_and_stale() {
199 let _g = TEST_PATHS_LOCK.lock().unwrap_or_else(|e| e.into_inner());
200 let dir = tempdir().unwrap();
201 set_cache_dir_override(Some(dir.path().to_path_buf()));
202 ensure_sessions_dir().unwrap();
203 assert!(sessions_dir().ends_with("sessions"));
204
205 write_meta(
206 "demo",
207 &SessionMeta {
208 pid: 999_999_999,
209 source: "echo".into(),
210 transport: "stdio".into(),
211 created_at: 1.0,
212 idle_secs: 1800,
213 last_activity_at: 1.0,
214 },
215 )
216 .unwrap();
217 assert!(clear_stale_session("demo").unwrap());
218 assert!(!session_meta_path("demo").exists());
219 set_cache_dir_override(None);
220 }
221
222 #[test]
223 fn name_validation() {
224 assert!(validate_session_name("myfs").is_ok());
225 assert!(validate_session_name("../x").is_err());
226 assert!(validate_session_name("a/b").is_err());
227 }
228}