shell_tunnel/session/
store.rs1use std::collections::HashMap;
4use std::sync::RwLock;
5use std::time::Instant;
6
7use super::{SessionContext, SessionId, SessionState};
8use crate::error::ShellTunnelError;
9use crate::Result;
10
11#[derive(Debug, Clone, Default)]
13pub struct SessionConfig {
14 pub shell: Option<String>,
16 pub working_dir: Option<String>,
18 pub env: HashMap<String, String>,
20}
21
22#[derive(Debug)]
24pub struct Session {
25 pub id: SessionId,
27 pub state: SessionState,
29 pub config: SessionConfig,
31 pub context: SessionContext,
33 pub created_at: Instant,
35 pub last_activity: Instant,
37}
38
39impl Session {
40 pub fn new(id: SessionId, config: SessionConfig) -> Self {
42 let now = Instant::now();
43 let context = config
44 .working_dir
45 .as_ref()
46 .map(SessionContext::with_cwd)
47 .unwrap_or_default();
48
49 Self {
50 id,
51 state: SessionState::Created,
52 config,
53 context,
54 created_at: now,
55 last_activity: now,
56 }
57 }
58
59 pub fn touch(&mut self) {
61 self.last_activity = Instant::now();
62 }
63
64 pub fn idle_duration(&self) -> std::time::Duration {
66 self.last_activity.elapsed()
67 }
68}
69
70impl Clone for Session {
71 fn clone(&self) -> Self {
72 Self {
73 id: self.id,
74 state: self.state,
75 config: self.config.clone(),
76 context: self.context.clone(),
77 created_at: self.created_at,
78 last_activity: self.last_activity,
79 }
80 }
81}
82
83pub struct SessionStore {
85 sessions: RwLock<HashMap<SessionId, Session>>,
86}
87
88impl SessionStore {
89 pub fn new() -> Self {
91 Self {
92 sessions: RwLock::new(HashMap::new()),
93 }
94 }
95
96 pub fn create(&self, config: SessionConfig) -> Result<SessionId> {
100 let id = SessionId::new();
101 let session = Session::new(id, config);
102
103 let mut sessions = self
104 .sessions
105 .write()
106 .map_err(|_| ShellTunnelError::LockPoisoned)?;
107
108 sessions.insert(id, session);
109 Ok(id)
110 }
111
112 pub fn get(&self, id: &SessionId) -> Result<Option<Session>> {
114 let sessions = self
115 .sessions
116 .read()
117 .map_err(|_| ShellTunnelError::LockPoisoned)?;
118 Ok(sessions.get(id).cloned())
119 }
120
121 pub fn contains(&self, id: &SessionId) -> Result<bool> {
123 let sessions = self
124 .sessions
125 .read()
126 .map_err(|_| ShellTunnelError::LockPoisoned)?;
127 Ok(sessions.contains_key(id))
128 }
129
130 pub fn update<F>(&self, id: &SessionId, f: F) -> Result<()>
135 where
136 F: FnOnce(&mut Session),
137 {
138 let mut sessions = self
139 .sessions
140 .write()
141 .map_err(|_| ShellTunnelError::LockPoisoned)?;
142
143 let session = sessions
144 .get_mut(id)
145 .ok_or_else(|| ShellTunnelError::SessionNotFound(id.to_string()))?;
146
147 f(session);
148 Ok(())
149 }
150
151 pub fn remove(&self, id: &SessionId) -> Result<Option<Session>> {
155 let mut sessions = self
156 .sessions
157 .write()
158 .map_err(|_| ShellTunnelError::LockPoisoned)?;
159 Ok(sessions.remove(id))
160 }
161
162 pub fn count(&self) -> usize {
164 self.sessions.read().map(|s| s.len()).unwrap_or(0)
165 }
166
167 pub fn list_ids(&self) -> Result<Vec<SessionId>> {
169 let sessions = self
170 .sessions
171 .read()
172 .map_err(|_| ShellTunnelError::LockPoisoned)?;
173 Ok(sessions.keys().copied().collect())
174 }
175
176 pub fn remove_matching<F>(&self, predicate: F) -> Result<usize>
180 where
181 F: Fn(&Session) -> bool,
182 {
183 let mut sessions = self
184 .sessions
185 .write()
186 .map_err(|_| ShellTunnelError::LockPoisoned)?;
187
188 let before = sessions.len();
189 sessions.retain(|_, session| !predicate(session));
190 Ok(before - sessions.len())
191 }
192}
193
194impl Default for SessionStore {
195 fn default() -> Self {
196 Self::new()
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn test_create_session() {
206 let store = SessionStore::new();
207 let id = store.create(SessionConfig::default()).unwrap();
208
209 assert!(store.contains(&id).unwrap());
210 assert_eq!(store.count(), 1);
211 }
212
213 #[test]
214 fn test_get_session() {
215 let store = SessionStore::new();
216 let id = store.create(SessionConfig::default()).unwrap();
217
218 let session = store.get(&id).unwrap().unwrap();
219 assert_eq!(session.id, id);
220 assert_eq!(session.state, SessionState::Created);
221 }
222
223 #[test]
224 fn test_get_nonexistent() {
225 let store = SessionStore::new();
226 let fake_id = SessionId::from_raw(999999);
227
228 let result = store.get(&fake_id).unwrap();
229 assert!(result.is_none());
230 }
231
232 #[test]
233 fn test_update_session() {
234 let store = SessionStore::new();
235 let id = store.create(SessionConfig::default()).unwrap();
236
237 store
238 .update(&id, |s| {
239 s.state = SessionState::Active;
240 })
241 .unwrap();
242
243 let session = store.get(&id).unwrap().unwrap();
244 assert_eq!(session.state, SessionState::Active);
245 }
246
247 #[test]
248 fn test_update_nonexistent() {
249 let store = SessionStore::new();
250 let fake_id = SessionId::from_raw(999999);
251
252 let result = store.update(&fake_id, |_| {});
253 assert!(result.is_err());
254 }
255
256 #[test]
257 fn test_remove_session() {
258 let store = SessionStore::new();
259 let id = store.create(SessionConfig::default()).unwrap();
260
261 let removed = store.remove(&id).unwrap();
262 assert!(removed.is_some());
263 assert_eq!(removed.unwrap().id, id);
264
265 assert!(!store.contains(&id).unwrap());
266 assert_eq!(store.count(), 0);
267 }
268
269 #[test]
270 fn test_list_ids() {
271 let store = SessionStore::new();
272 let id1 = store.create(SessionConfig::default()).unwrap();
273 let id2 = store.create(SessionConfig::default()).unwrap();
274 let id3 = store.create(SessionConfig::default()).unwrap();
275
276 let ids = store.list_ids().unwrap();
277 assert_eq!(ids.len(), 3);
278 assert!(ids.contains(&id1));
279 assert!(ids.contains(&id2));
280 assert!(ids.contains(&id3));
281 }
282
283 #[test]
284 fn test_remove_matching() {
285 let store = SessionStore::new();
286 store.create(SessionConfig::default()).unwrap();
287 store.create(SessionConfig::default()).unwrap();
288
289 let ids = store.list_ids().unwrap();
291 store
292 .update(&ids[0], |s| s.state = SessionState::Terminated)
293 .unwrap();
294
295 let removed = store
297 .remove_matching(|s| s.state == SessionState::Terminated)
298 .unwrap();
299
300 assert_eq!(removed, 1);
301 assert_eq!(store.count(), 1);
302 }
303
304 #[test]
305 fn test_concurrent_access() {
306 use std::sync::Arc;
307 use std::thread;
308
309 let store = Arc::new(SessionStore::new());
310 let mut handles = vec![];
311
312 for _ in 0..100 {
314 let store = Arc::clone(&store);
315 handles.push(thread::spawn(move || {
316 store.create(SessionConfig::default()).unwrap()
317 }));
318 }
319
320 let ids: Vec<SessionId> = handles.into_iter().map(|h| h.join().unwrap()).collect();
321
322 let unique: std::collections::HashSet<_> = ids.iter().collect();
324 assert_eq!(unique.len(), 100);
325
326 assert_eq!(store.count(), 100);
328 }
329}