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
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 if Self::is_process_alive(session.pid) {
152 sessions.push(session);
153 } else {
154 let _ = fs::remove_file(&path);
156 }
157 }
158 }
159 }
160 }
161
162 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 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 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 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}