detached_shell/
session.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::os::unix::net::UnixStream;
5use std::path::PathBuf;
6
7use crate::error::{NdsError, Result};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Session {
11 pub id: String,
12 pub name: Option<String>,
13 pub pid: i32,
14 pub created_at: DateTime<Utc>,
15 pub attached: bool,
16 pub socket_path: PathBuf,
17 pub shell: String,
18 pub working_dir: String,
19}
20
21impl Session {
22 pub fn new(id: String, pid: i32, socket_path: PathBuf) -> Self {
23 let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
24 let working_dir = std::env::current_dir()
25 .map(|p| p.to_string_lossy().to_string())
26 .unwrap_or_else(|_| "/".to_string());
27
28 Session {
29 id,
30 name: None,
31 pid,
32 created_at: Utc::now(),
33 attached: false, socket_path,
35 shell,
36 working_dir,
37 }
38 }
39
40 pub fn with_name(id: String, name: Option<String>, pid: i32, socket_path: PathBuf) -> Self {
41 let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
42 let working_dir = std::env::current_dir()
43 .map(|p| p.to_string_lossy().to_string())
44 .unwrap_or_else(|_| "/".to_string());
45
46 Session {
47 id,
48 name,
49 pid,
50 created_at: Utc::now(),
51 attached: false, socket_path,
53 shell,
54 working_dir,
55 }
56 }
57
58 pub fn display_name(&self) -> String {
59 match &self.name {
60 Some(name) => format!("{} [{}]", name, self.id),
61 None => self.id.clone(),
62 }
63 }
64
65 pub fn session_dir() -> Result<PathBuf> {
66 let dir = if let Ok(nds_home) = std::env::var("NDS_HOME") {
67 PathBuf::from(nds_home).join("sessions")
68 } else {
69 directories::BaseDirs::new()
70 .ok_or_else(|| {
71 NdsError::DirectoryCreationError("Could not find home directory".to_string())
72 })?
73 .home_dir()
74 .join(".nds")
75 .join("sessions")
76 };
77
78 if !dir.exists() {
79 fs::create_dir_all(&dir)
80 .map_err(|e| NdsError::DirectoryCreationError(e.to_string()))?;
81 }
82
83 Ok(dir)
84 }
85
86 pub fn socket_dir() -> Result<PathBuf> {
87 let dir = if let Ok(nds_home) = std::env::var("NDS_HOME") {
88 PathBuf::from(nds_home).join("sockets")
89 } else {
90 directories::BaseDirs::new()
91 .ok_or_else(|| {
92 NdsError::DirectoryCreationError("Could not find home directory".to_string())
93 })?
94 .home_dir()
95 .join(".nds")
96 .join("sockets")
97 };
98
99 if !dir.exists() {
100 fs::create_dir_all(&dir)
101 .map_err(|e| NdsError::DirectoryCreationError(e.to_string()))?;
102 }
103
104 Ok(dir)
105 }
106
107 pub fn metadata_path(&self) -> Result<PathBuf> {
108 Ok(Self::session_dir()?.join(format!("{}.json", self.id)))
109 }
110
111 pub fn save(&self) -> Result<()> {
112 let path = self.metadata_path()?;
113 let json = serde_json::to_string_pretty(self)?;
114 fs::write(path, json)?;
115 Ok(())
116 }
117
118 pub fn load(id: &str) -> Result<Self> {
119 let path = Self::session_dir()?.join(format!("{}.json", id));
120
121 if !path.exists() {
122 return Err(NdsError::SessionNotFound(id.to_string()));
123 }
124
125 let content = fs::read_to_string(path)?;
126 let session: Session = serde_json::from_str(&content)?;
127
128 if !Self::is_process_alive(session.pid) {
130 Self::cleanup(&session.id)?;
132 return Err(NdsError::SessionNotFound(id.to_string()));
133 }
134
135 Ok(session)
136 }
137
138 pub fn list_all() -> Result<Vec<Session>> {
139 let dir = Self::session_dir()?;
140 let mut sessions = Vec::new();
141 let mut cleaned_count = 0;
142
143 if dir.exists() {
144 for entry in fs::read_dir(dir)? {
145 let entry = entry?;
146 let path = entry.path();
147
148 if path.extension().and_then(|s| s.to_str()) == Some("json") {
149 let content = fs::read_to_string(&path)?;
150 if let Ok(session) = serde_json::from_str::<Session>(&content) {
151 let process_alive = Self::is_process_alive(session.pid);
153 let socket_healthy = session.socket_path.exists()
154 && Self::is_socket_healthy(&session.socket_path);
155
156 if process_alive && socket_healthy {
157 sessions.push(session);
158 } else {
159 let _ = Self::cleanup(&session.id);
161 cleaned_count += 1;
162 }
163 }
164 }
165 }
166 }
167
168 if cleaned_count > 0 {
169 eprintln!("Auto-cleaned {} dead session(s)", cleaned_count);
170 }
171
172 sessions.sort_by(|a, b| a.created_at.cmp(&b.created_at));
174 Ok(sessions)
175 }
176
177 fn is_socket_healthy(socket_path: &PathBuf) -> bool {
179 use std::time::Duration;
180
181 match UnixStream::connect(socket_path) {
182 Ok(socket) => {
183 let _ = socket.set_read_timeout(Some(Duration::from_millis(50)));
185 let _ = socket.set_write_timeout(Some(Duration::from_millis(50)));
186 true
187 }
188 Err(_) => false,
189 }
190 }
191
192 pub fn cleanup(id: &str) -> Result<()> {
193 let metadata_path = Self::session_dir()?.join(format!("{}.json", id));
194 if metadata_path.exists() {
195 fs::remove_file(metadata_path)?;
196 }
197
198 let socket_path = Self::socket_dir()?.join(format!("{}.sock", id));
199 if socket_path.exists() {
200 fs::remove_file(socket_path)?;
201 }
202
203 let status_path = Self::session_dir()?.join(format!("{}.status", id));
204 if status_path.exists() {
205 fs::remove_file(status_path)?;
206 }
207
208 Ok(())
209 }
210
211 pub fn is_process_alive(pid: i32) -> bool {
212 unsafe { libc::kill(pid, 0) == 0 }
214 }
215
216 pub fn mark_attached(&mut self) -> Result<()> {
217 self.attached = true;
218 self.save()
219 }
220
221 pub fn mark_detached(&mut self) -> Result<()> {
222 self.attached = false;
223 self.save()
224 }
225
226 pub fn connect_socket(&self) -> Result<UnixStream> {
227 use std::time::Duration;
228
229 if !self.socket_path.exists() {
231 return Err(NdsError::SocketError(format!(
232 "Session socket does not exist: {}",
233 self.socket_path.display()
234 )));
235 }
236
237 match UnixStream::connect(&self.socket_path) {
239 Ok(socket) => {
240 socket
242 .set_read_timeout(Some(Duration::from_secs(5)))
243 .map_err(|e| {
244 NdsError::SocketError(format!("Failed to set socket timeout: {}", e))
245 })?;
246 socket
247 .set_write_timeout(Some(Duration::from_secs(5)))
248 .map_err(|e| {
249 NdsError::SocketError(format!("Failed to set socket timeout: {}", e))
250 })?;
251 Ok(socket)
252 }
253 Err(e) => {
254 match e.kind() {
256 std::io::ErrorKind::ConnectionRefused
257 | std::io::ErrorKind::BrokenPipe
258 | std::io::ErrorKind::NotFound => {
259 Err(NdsError::SessionNotFound(format!(
261 "Session {} appears to be dead or unreachable: {}",
262 self.id, e
263 )))
264 }
265 _ => Err(NdsError::SocketError(format!(
266 "Failed to connect to session socket: {}",
267 e
268 ))),
269 }
270 }
271 }
272 }
273
274 pub fn get_client_count(&self) -> usize {
275 let status_path = Self::session_dir()
278 .ok()
279 .and_then(|dir| Some(dir.join(format!("{}.status", self.id))));
280
281 if let Some(path) = status_path {
282 if let Ok(content) = fs::read_to_string(path) {
283 if let Ok(count) = content.trim().parse::<usize>() {
284 return count;
285 }
286 }
287 }
288
289 if self.attached {
291 1
292 } else {
293 0
294 }
295 }
296
297 pub fn update_client_count(session_id: &str, count: usize) -> Result<()> {
298 let status_path = Self::session_dir()?.join(format!("{}.status", session_id));
299 fs::write(status_path, count.to_string())?;
300 Ok(())
301 }
302}