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/// Configuration for creating a new session.
12#[derive(Debug, Clone, Default)]
13pub struct SessionConfig {
14    /// Initial shell command (e.g., "bash", "powershell.exe").
15    pub shell: Option<String>,
16    /// Initial working directory.
17    pub working_dir: Option<String>,
18    /// Environment variables to set.
19    pub env: HashMap<String, String>,
20}
21
22/// A shell session.
23#[derive(Debug)]
24pub struct Session {
25    /// Unique identifier.
26    pub id: SessionId,
27    /// Current state.
28    pub state: SessionState,
29    /// Configuration used to create this session.
30    pub config: SessionConfig,
31    /// Execution context (CWD, env, etc.).
32    pub context: SessionContext,
33    /// Time when session was created.
34    pub created_at: Instant,
35    /// Time of last activity.
36    pub last_activity: Instant,
37}
38
39impl Session {
40    /// Create a new session with the given ID and configuration.
41    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    /// Update the last activity timestamp.
60    pub fn touch(&mut self) {
61        self.last_activity = Instant::now();
62    }
63
64    /// Get the idle duration since last activity.
65    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
83/// Thread-safe storage for sessions.
84pub struct SessionStore {
85    sessions: RwLock<HashMap<SessionId, Session>>,
86}
87
88impl SessionStore {
89    /// Create a new empty session store.
90    pub fn new() -> Self {
91        Self {
92            sessions: RwLock::new(HashMap::new()),
93        }
94    }
95
96    /// Create a new session with the given configuration.
97    ///
98    /// Returns the newly assigned session ID.
99    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    /// Get a clone of the session with the given ID.
113    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    /// Check if a session exists.
122    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    /// Update a session using a closure.
131    ///
132    /// The closure receives a mutable reference to the session and can modify it.
133    /// Returns an error if the session doesn't exist.
134    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    /// Remove a session from the store.
152    ///
153    /// Returns the removed session, or None if it didn't exist.
154    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    /// Get the number of sessions in the store.
163    pub fn count(&self) -> usize {
164        self.sessions.read().map(|s| s.len()).unwrap_or(0)
165    }
166
167    /// List all session IDs.
168    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    /// Remove all sessions matching a predicate.
177    ///
178    /// Returns the number of sessions removed.
179    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        // Mark one as terminated
290        let ids = store.list_ids().unwrap();
291        store
292            .update(&ids[0], |s| s.state = SessionState::Terminated)
293            .unwrap();
294
295        // Remove terminated sessions
296        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        // Spawn 100 threads that each create a session
313        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        // All IDs should be unique
323        let unique: std::collections::HashSet<_> = ids.iter().collect();
324        assert_eq!(unique.len(), 100);
325
326        // Store should have 100 sessions
327        assert_eq!(store.count(), 100);
328    }
329}