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
142        if dir.exists() {
143            for entry in fs::read_dir(dir)? {
144                let entry = entry?;
145                let path = entry.path();
146
147                if path.extension().and_then(|s| s.to_str()) == Some("json") {
148                    let content = fs::read_to_string(&path)?;
149                    if let Ok(session) = serde_json::from_str::<Session>(&content) {
150                        // Only include sessions with live processes
151                        if Self::is_process_alive(session.pid) {
152                            sessions.push(session);
153                        } else {
154                            // Clean up dead session
155                            let _ = fs::remove_file(&path);
156                        }
157                    }
158                }
159            }
160        }
161
162        // Sort by creation time
163        sessions.sort_by(|a, b| a.created_at.cmp(&b.created_at));
164        Ok(sessions)
165    }
166
167    pub fn cleanup(id: &str) -> Result<()> {
168        let metadata_path = Self::session_dir()?.join(format!("{}.json", id));
169        if metadata_path.exists() {
170            fs::remove_file(metadata_path)?;
171        }
172
173        let socket_path = Self::socket_dir()?.join(format!("{}.sock", id));
174        if socket_path.exists() {
175            fs::remove_file(socket_path)?;
176        }
177
178        let status_path = Self::session_dir()?.join(format!("{}.status", id));
179        if status_path.exists() {
180            fs::remove_file(status_path)?;
181        }
182
183        Ok(())
184    }
185
186    pub fn is_process_alive(pid: i32) -> bool {
187        // Check if process exists by sending signal 0
188        unsafe { libc::kill(pid, 0) == 0 }
189    }
190
191    pub fn mark_attached(&mut self) -> Result<()> {
192        self.attached = true;
193        self.save()
194    }
195
196    pub fn mark_detached(&mut self) -> Result<()> {
197        self.attached = false;
198        self.save()
199    }
200
201    pub fn connect_socket(&self) -> Result<UnixStream> {
202        UnixStream::connect(&self.socket_path).map_err(|e| {
203            NdsError::SocketError(format!("Failed to connect to session socket: {}", e))
204        })
205    }
206
207    pub fn get_client_count(&self) -> usize {
208        // Read client count from a status file instead of connecting to the socket
209        // This avoids disrupting active sessions
210        let status_path = Self::session_dir()
211            .ok()
212            .and_then(|dir| Some(dir.join(format!("{}.status", self.id))));
213
214        if let Some(path) = status_path {
215            if let Ok(content) = fs::read_to_string(path) {
216                if let Ok(count) = content.trim().parse::<usize>() {
217                    return count;
218                }
219            }
220        }
221
222        // Fallback: assume 0 if detached, 1 if attached
223        if self.attached {
224            1
225        } else {
226            0
227        }
228    }
229
230    pub fn update_client_count(session_id: &str, count: usize) -> Result<()> {
231        let status_path = Self::session_dir()?.join(format!("{}.status", session_id));
232        fs::write(status_path, count.to_string())?;
233        Ok(())
234    }
235}