Skip to main content

shell_tunnel/session/
store.rs

1//! Session storage and management.
2
3use 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/// A shell session.
12///
13/// A session carries no execution configuration. It is an identifier the audit
14/// trail records against, a place for streaming to attach, and the bookkeeping
15/// in [`SessionContext`]; where a command runs and what it runs with are
16/// decided per execute. It once held a `shell`, a `working_dir` and an `env`
17/// that no execute ever read.
18#[derive(Debug, Clone)]
19pub struct Session {
20    /// Unique identifier.
21    pub id: SessionId,
22    /// Current state.
23    pub state: SessionState,
24    /// Execution bookkeeping (last command, exit code, count).
25    pub context: SessionContext,
26    /// Time when session was created.
27    pub created_at: Instant,
28    /// Time of last activity.
29    pub last_activity: Instant,
30}
31
32impl Session {
33    /// Create a new session with the given ID.
34    pub fn new(id: SessionId) -> Self {
35        let now = Instant::now();
36
37        Self {
38            id,
39            state: SessionState::Created,
40            context: SessionContext::new(),
41            created_at: now,
42            last_activity: now,
43        }
44    }
45
46    /// Update the last activity timestamp.
47    pub fn touch(&mut self) {
48        self.last_activity = Instant::now();
49    }
50
51    /// Get the idle duration since last activity.
52    pub fn idle_duration(&self) -> std::time::Duration {
53        self.last_activity.elapsed()
54    }
55}
56
57/// Thread-safe storage for sessions.
58pub struct SessionStore {
59    sessions: RwLock<HashMap<SessionId, Session>>,
60}
61
62impl SessionStore {
63    /// Create a new empty session store.
64    pub fn new() -> Self {
65        Self {
66            sessions: RwLock::new(HashMap::new()),
67        }
68    }
69
70    /// Create a new session.
71    ///
72    /// Returns the newly assigned session ID.
73    pub fn create(&self) -> Result<SessionId> {
74        let id = SessionId::new();
75        let session = Session::new(id);
76
77        let mut sessions = self
78            .sessions
79            .write()
80            .map_err(|_| ShellTunnelError::LockPoisoned)?;
81
82        sessions.insert(id, session);
83        Ok(id)
84    }
85
86    /// Get a clone of the session with the given ID.
87    pub fn get(&self, id: &SessionId) -> Result<Option<Session>> {
88        let sessions = self
89            .sessions
90            .read()
91            .map_err(|_| ShellTunnelError::LockPoisoned)?;
92        Ok(sessions.get(id).cloned())
93    }
94
95    /// Check if a session exists.
96    pub fn contains(&self, id: &SessionId) -> Result<bool> {
97        let sessions = self
98            .sessions
99            .read()
100            .map_err(|_| ShellTunnelError::LockPoisoned)?;
101        Ok(sessions.contains_key(id))
102    }
103
104    /// Update a session using a closure.
105    ///
106    /// The closure receives a mutable reference to the session and can modify it.
107    /// Returns an error if the session doesn't exist.
108    pub fn update<F>(&self, id: &SessionId, f: F) -> Result<()>
109    where
110        F: FnOnce(&mut Session),
111    {
112        let mut sessions = self
113            .sessions
114            .write()
115            .map_err(|_| ShellTunnelError::LockPoisoned)?;
116
117        let session = sessions
118            .get_mut(id)
119            .ok_or_else(|| ShellTunnelError::SessionNotFound(id.to_string()))?;
120
121        f(session);
122        Ok(())
123    }
124
125    /// Remove a session from the store.
126    ///
127    /// Returns the removed session, or None if it didn't exist.
128    pub fn remove(&self, id: &SessionId) -> Result<Option<Session>> {
129        let mut sessions = self
130            .sessions
131            .write()
132            .map_err(|_| ShellTunnelError::LockPoisoned)?;
133        Ok(sessions.remove(id))
134    }
135
136    /// Get the number of sessions in the store.
137    pub fn count(&self) -> usize {
138        self.sessions.read().map(|s| s.len()).unwrap_or(0)
139    }
140
141    /// List all session IDs.
142    pub fn list_ids(&self) -> Result<Vec<SessionId>> {
143        let sessions = self
144            .sessions
145            .read()
146            .map_err(|_| ShellTunnelError::LockPoisoned)?;
147        Ok(sessions.keys().copied().collect())
148    }
149
150    /// Remove all sessions matching a predicate.
151    ///
152    /// Returns the number of sessions removed.
153    pub fn remove_matching<F>(&self, predicate: F) -> Result<usize>
154    where
155        F: Fn(&Session) -> bool,
156    {
157        let mut sessions = self
158            .sessions
159            .write()
160            .map_err(|_| ShellTunnelError::LockPoisoned)?;
161
162        let before = sessions.len();
163        sessions.retain(|_, session| !predicate(session));
164        Ok(before - sessions.len())
165    }
166}
167
168impl Default for SessionStore {
169    fn default() -> Self {
170        Self::new()
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_create_session() {
180        let store = SessionStore::new();
181        let id = store.create().unwrap();
182
183        assert!(store.contains(&id).unwrap());
184        assert_eq!(store.count(), 1);
185    }
186
187    #[test]
188    fn test_get_session() {
189        let store = SessionStore::new();
190        let id = store.create().unwrap();
191
192        let session = store.get(&id).unwrap().unwrap();
193        assert_eq!(session.id, id);
194        assert_eq!(session.state, SessionState::Created);
195    }
196
197    #[test]
198    fn test_get_nonexistent() {
199        let store = SessionStore::new();
200        let fake_id = SessionId::from_raw(999999);
201
202        let result = store.get(&fake_id).unwrap();
203        assert!(result.is_none());
204    }
205
206    #[test]
207    fn test_update_session() {
208        let store = SessionStore::new();
209        let id = store.create().unwrap();
210
211        store
212            .update(&id, |s| {
213                s.state = SessionState::Active;
214            })
215            .unwrap();
216
217        let session = store.get(&id).unwrap().unwrap();
218        assert_eq!(session.state, SessionState::Active);
219    }
220
221    #[test]
222    fn test_update_nonexistent() {
223        let store = SessionStore::new();
224        let fake_id = SessionId::from_raw(999999);
225
226        let result = store.update(&fake_id, |_| {});
227        assert!(result.is_err());
228    }
229
230    #[test]
231    fn test_remove_session() {
232        let store = SessionStore::new();
233        let id = store.create().unwrap();
234
235        let removed = store.remove(&id).unwrap();
236        assert!(removed.is_some());
237        assert_eq!(removed.unwrap().id, id);
238
239        assert!(!store.contains(&id).unwrap());
240        assert_eq!(store.count(), 0);
241    }
242
243    #[test]
244    fn test_list_ids() {
245        let store = SessionStore::new();
246        let id1 = store.create().unwrap();
247        let id2 = store.create().unwrap();
248        let id3 = store.create().unwrap();
249
250        let ids = store.list_ids().unwrap();
251        assert_eq!(ids.len(), 3);
252        assert!(ids.contains(&id1));
253        assert!(ids.contains(&id2));
254        assert!(ids.contains(&id3));
255    }
256
257    #[test]
258    fn test_remove_matching() {
259        let store = SessionStore::new();
260        store.create().unwrap();
261        store.create().unwrap();
262
263        // Mark one as terminated
264        let ids = store.list_ids().unwrap();
265        store
266            .update(&ids[0], |s| s.state = SessionState::Terminated)
267            .unwrap();
268
269        // Remove terminated sessions
270        let removed = store
271            .remove_matching(|s| s.state == SessionState::Terminated)
272            .unwrap();
273
274        assert_eq!(removed, 1);
275        assert_eq!(store.count(), 1);
276    }
277
278    #[test]
279    fn test_concurrent_access() {
280        use std::sync::Arc;
281        use std::thread;
282
283        let store = Arc::new(SessionStore::new());
284        let mut handles = vec![];
285
286        // Spawn 100 threads that each create a session
287        for _ in 0..100 {
288            let store = Arc::clone(&store);
289            handles.push(thread::spawn(move || store.create().unwrap()));
290        }
291
292        let ids: Vec<SessionId> = handles.into_iter().map(|h| h.join().unwrap()).collect();
293
294        // All IDs should be unique
295        let unique: std::collections::HashSet<_> = ids.iter().collect();
296        assert_eq!(unique.len(), 100);
297
298        // Store should have 100 sessions
299        assert_eq!(store.count(), 100);
300    }
301}