detached_shell/
session.rs

1use 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, // Sessions start detached
34            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, // Sessions start detached
52            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        // Verify the process is still alive
129        if !Self::is_process_alive(session.pid) {
130            // Clean up dead session
131            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                        // Check both process and socket health
152                        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                            // Clean up dead session completely
160                            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        // Sort by creation time
173        sessions.sort_by(|a, b| a.created_at.cmp(&b.created_at));
174        Ok(sessions)
175    }
176
177    /// Check if a socket is healthy by attempting to connect
178    fn is_socket_healthy(socket_path: &PathBuf) -> bool {
179        use std::time::Duration;
180
181        match UnixStream::connect(socket_path) {
182            Ok(socket) => {
183                // Set a very short timeout for the health check
184                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        // Check if process exists by sending signal 0
213        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        // Check if socket file exists first
230        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        // Try to connect with a timeout
238        match UnixStream::connect(&self.socket_path) {
239            Ok(socket) => {
240                // Set socket timeout to prevent hanging
241                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                // Check if it's a connection refused or broken pipe
255                match e.kind() {
256                    std::io::ErrorKind::ConnectionRefused
257                    | std::io::ErrorKind::BrokenPipe
258                    | std::io::ErrorKind::NotFound => {
259                        // Session might be dead, try to clean up
260                        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        // Read client count from a status file instead of connecting to the socket
276        // This avoids disrupting active sessions
277        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        // Fallback: assume 0 if detached, 1 if attached
290        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}