1use anyhow::{Context, Result};
2use rand::seq::SliceRandom;
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::PathBuf;
6use std::time::SystemTime;
7
8const ADJECTIVES: &[&str] = &[
9 "fuzzy", "quick", "lazy", "happy", "sleepy", "brave", "calm", "eager", "gentle", "kind",
10 "lively", "merry", "nice", "proud", "silly", "witty", "bold", "cool", "dapper", "fancy",
11 "jolly", "keen", "lucky", "noble",
12];
13
14const NOUNS: &[&str] = &[
15 "penguin", "dolphin", "falcon", "tiger", "panda", "koala", "otter", "fox", "owl", "bear",
16 "wolf", "eagle", "shark", "whale", "raven", "lynx", "badger", "gecko", "lemur", "moose",
17 "orca", "quail", "sloth", "zebra",
18];
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct SessionInfo {
23 pub name: String,
24 pub command: Vec<String>,
25 pub pid: u32,
26 pub created_at: u64,
27 pub socket_path: String,
28}
29
30pub fn sessions_dir() -> Result<PathBuf> {
32 if let Ok(env_path) = std::env::var("KEEP_RUNNING_SESSION_DIR") {
34 let path = PathBuf::from(env_path);
35 fs::create_dir_all(&path)?;
36 return Ok(path);
37 }
38
39 let config_dir = dirs::config_dir()
40 .context("Could not determine config directory")?
41 .join("keep-running")
42 .join("sessions");
43 fs::create_dir_all(&config_dir)?;
44 Ok(config_dir)
45}
46
47pub fn sockets_dir() -> Result<PathBuf> {
49 if let Ok(env_path) = std::env::var("KEEP_RUNNING_SOCKET_DIR") {
51 let path = PathBuf::from(env_path);
52 fs::create_dir_all(&path)?;
53 return Ok(path);
54 }
55
56 let runtime_dir = std::env::var("XDG_RUNTIME_DIR")
57 .map(PathBuf::from)
58 .unwrap_or_else(|_| {
59 let uid = unsafe { libc::getuid() };
60 PathBuf::from(format!("/tmp/keep-running-{}", uid))
61 });
62 let socket_dir = runtime_dir.join("keep-running");
63 fs::create_dir_all(&socket_dir)?;
64 Ok(socket_dir)
65}
66
67pub fn generate_name() -> String {
69 let mut rng = rand::thread_rng();
70 let adj = ADJECTIVES.choose(&mut rng).unwrap();
71 let noun = NOUNS.choose(&mut rng).unwrap();
72 format!("{}-{}", adj, noun)
73}
74
75pub fn generate_unique_name() -> Result<String> {
77 let existing = list_sessions()?;
78 let existing_names: std::collections::HashSet<_> =
79 existing.iter().map(|s| s.name.as_str()).collect();
80
81 for _ in 0..100 {
82 let name = generate_name();
83 if !existing_names.contains(name.as_str()) {
84 return Ok(name);
85 }
86 }
87
88 let name = format!("{}-{}", generate_name(), rand::random::<u16>());
90 Ok(name)
91}
92
93pub fn save_session(info: &SessionInfo) -> Result<()> {
95 let path = sessions_dir()?.join(format!("{}.json", info.name));
96 let json = serde_json::to_string_pretty(info)?;
97 fs::write(&path, json)?;
98 Ok(())
99}
100
101pub fn load_session(name: &str) -> Result<Option<SessionInfo>> {
103 let path = sessions_dir()?.join(format!("{}.json", name));
104 if !path.exists() {
105 return Ok(None);
106 }
107 let json = fs::read_to_string(&path)?;
108 let info: SessionInfo = serde_json::from_str(&json)?;
109 Ok(Some(info))
110}
111
112pub fn remove_session(name: &str) -> Result<()> {
114 let path = sessions_dir()?.join(format!("{}.json", name));
115 if path.exists() {
116 fs::remove_file(&path)?;
117 }
118
119 if let Ok(sockets) = sockets_dir() {
121 let socket_path = sockets.join(format!("{}.sock", name));
122 let _ = fs::remove_file(&socket_path);
123 }
124
125 Ok(())
126}
127
128pub fn list_sessions() -> Result<Vec<SessionInfo>> {
130 let dir = sessions_dir()?;
131 let mut sessions = Vec::new();
132 let mut dead_sessions = Vec::new();
133
134 for entry in fs::read_dir(&dir)? {
135 let entry = entry?;
136 let path = entry.path();
137
138 if path.extension().map(|e| e == "json").unwrap_or(false) {
139 if let Ok(json) = fs::read_to_string(&path) {
140 if let Ok(info) = serde_json::from_str::<SessionInfo>(&json) {
141 if is_process_alive(info.pid) {
143 sessions.push(info);
144 } else {
145 dead_sessions.push(info.name.clone());
146 }
147 }
148 }
149 }
150 }
151
152 for name in dead_sessions {
154 let _ = remove_session(&name);
155 }
156
157 sessions.sort_by_key(|s| s.created_at);
158 Ok(sessions)
159}
160
161fn is_process_alive(pid: u32) -> bool {
163 unsafe { libc::kill(pid as i32, 0) == 0 }
164}
165
166pub fn find_session(query: &str) -> Result<Option<SessionInfo>> {
168 let sessions = list_sessions()?;
169
170 if let Some(session) = sessions.iter().find(|s| s.name == query) {
172 return Ok(Some(session.clone()));
173 }
174
175 let matches: Vec<_> = sessions
177 .iter()
178 .filter(|s| s.name.starts_with(query))
179 .collect();
180
181 match matches.len() {
182 0 => Ok(None),
183 1 => Ok(Some(matches[0].clone())),
184 _ => {
185 let names: Vec<_> = matches.iter().map(|s| s.name.as_str()).collect();
186 anyhow::bail!(
187 "Ambiguous session name '{}', matches: {}",
188 query,
189 names.join(", ")
190 );
191 }
192 }
193}
194
195pub fn socket_path(name: &str) -> Result<PathBuf> {
197 Ok(sockets_dir()?.join(format!("{}.sock", name)))
198}
199
200pub fn timestamp() -> u64 {
202 SystemTime::now()
203 .duration_since(SystemTime::UNIX_EPOCH)
204 .map(|d| d.as_secs())
205 .unwrap_or(0)
206}