Skip to main content

shell_tunnel/session/
context.rs

1//! Session execution context and state tracking.
2
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6/// Execution context for a shell session.
7///
8/// This tracks the current working directory, environment variables,
9/// and other runtime state information for a session.
10#[derive(Debug, Clone, Default)]
11pub struct SessionContext {
12    /// Current working directory.
13    cwd: Option<PathBuf>,
14    /// Environment variables snapshot.
15    env: HashMap<String, String>,
16    /// Last command executed.
17    last_command: Option<String>,
18    /// Exit code of last command.
19    last_exit_code: Option<i32>,
20    /// Command execution count.
21    execution_count: u64,
22}
23
24impl SessionContext {
25    /// Create a new empty session context.
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    /// Create a session context with initial working directory.
31    pub fn with_cwd(cwd: impl Into<PathBuf>) -> Self {
32        Self {
33            cwd: Some(cwd.into()),
34            ..Default::default()
35        }
36    }
37
38    /// Get the current working directory.
39    pub fn cwd(&self) -> Option<&PathBuf> {
40        self.cwd.as_ref()
41    }
42
43    /// Set the current working directory.
44    pub fn set_cwd(&mut self, cwd: impl Into<PathBuf>) {
45        self.cwd = Some(cwd.into());
46    }
47
48    /// Clear the current working directory.
49    pub fn clear_cwd(&mut self) {
50        self.cwd = None;
51    }
52
53    /// Get the environment variables.
54    pub fn env(&self) -> &HashMap<String, String> {
55        &self.env
56    }
57
58    /// Get a specific environment variable.
59    pub fn get_env(&self, key: &str) -> Option<&str> {
60        self.env.get(key).map(|s| s.as_str())
61    }
62
63    /// Set an environment variable.
64    pub fn set_env(&mut self, key: impl Into<String>, value: impl Into<String>) {
65        self.env.insert(key.into(), value.into());
66    }
67
68    /// Remove an environment variable.
69    pub fn remove_env(&mut self, key: &str) -> Option<String> {
70        self.env.remove(key)
71    }
72
73    /// Merge environment variables from another map.
74    pub fn merge_env(&mut self, vars: HashMap<String, String>) {
75        self.env.extend(vars);
76    }
77
78    /// Get the last command executed.
79    pub fn last_command(&self) -> Option<&str> {
80        self.last_command.as_deref()
81    }
82
83    /// Get the exit code of the last command.
84    pub fn last_exit_code(&self) -> Option<i32> {
85        self.last_exit_code
86    }
87
88    /// Get the number of commands executed.
89    pub fn execution_count(&self) -> u64 {
90        self.execution_count
91    }
92
93    /// Record a command execution result.
94    pub fn record_execution(&mut self, command: impl Into<String>, exit_code: Option<i32>) {
95        self.last_command = Some(command.into());
96        self.last_exit_code = exit_code;
97        self.execution_count += 1;
98    }
99
100    /// Check if the last command succeeded.
101    pub fn last_succeeded(&self) -> bool {
102        self.last_exit_code == Some(0)
103    }
104
105    /// Check if the last command failed.
106    pub fn last_failed(&self) -> bool {
107        matches!(self.last_exit_code, Some(code) if code != 0)
108    }
109}
110
111/// State probe for querying shell state.
112///
113/// This provides utility methods for probing shell state like
114/// current directory and environment variables.
115pub struct StateProbe;
116
117impl StateProbe {
118    /// Get the command to probe current working directory.
119    #[cfg(unix)]
120    pub fn cwd_command() -> &'static str {
121        "pwd"
122    }
123
124    /// Get the command to probe current working directory.
125    #[cfg(windows)]
126    pub fn cwd_command() -> &'static str {
127        "cd"
128    }
129
130    /// Get the command to list environment variables.
131    #[cfg(unix)]
132    pub fn env_command() -> &'static str {
133        "env"
134    }
135
136    /// Get the command to list environment variables.
137    #[cfg(windows)]
138    pub fn env_command() -> &'static str {
139        "set"
140    }
141
142    /// Parse CWD from command output.
143    pub fn parse_cwd(output: &str) -> Option<PathBuf> {
144        let trimmed = output.trim();
145        if trimmed.is_empty() {
146            None
147        } else {
148            Some(PathBuf::from(trimmed.lines().next().unwrap_or("")))
149        }
150    }
151
152    /// Parse environment variables from command output.
153    pub fn parse_env(output: &str) -> HashMap<String, String> {
154        let mut env = HashMap::new();
155
156        for line in output.lines() {
157            if let Some((key, value)) = line.split_once('=') {
158                let key = key.trim();
159                if !key.is_empty() {
160                    env.insert(key.to_string(), value.to_string());
161                }
162            }
163        }
164
165        env
166    }
167
168    /// Generate a unique marker for output detection.
169    pub fn marker(prefix: &str) -> String {
170        use std::time::{SystemTime, UNIX_EPOCH};
171        let timestamp = SystemTime::now()
172            .duration_since(UNIX_EPOCH)
173            .map(|d| d.as_nanos())
174            .unwrap_or(0);
175        format!("__{}_{}_MARKER__", prefix, timestamp)
176    }
177
178    /// Generate an echo command with marker.
179    pub fn echo_marker(marker: &str) -> String {
180        format!("echo {}", marker)
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_context_new() {
190        let ctx = SessionContext::new();
191        assert!(ctx.cwd().is_none());
192        assert!(ctx.env().is_empty());
193        assert!(ctx.last_command().is_none());
194        assert!(ctx.last_exit_code().is_none());
195        assert_eq!(ctx.execution_count(), 0);
196    }
197
198    #[test]
199    fn test_context_with_cwd() {
200        let ctx = SessionContext::with_cwd("/home/user");
201        assert_eq!(ctx.cwd(), Some(&PathBuf::from("/home/user")));
202    }
203
204    #[test]
205    fn test_context_set_cwd() {
206        let mut ctx = SessionContext::new();
207        ctx.set_cwd("/tmp");
208        assert_eq!(ctx.cwd(), Some(&PathBuf::from("/tmp")));
209
210        ctx.clear_cwd();
211        assert!(ctx.cwd().is_none());
212    }
213
214    #[test]
215    fn test_context_env() {
216        let mut ctx = SessionContext::new();
217        ctx.set_env("PATH", "/usr/bin");
218        ctx.set_env("HOME", "/home/user");
219
220        assert_eq!(ctx.get_env("PATH"), Some("/usr/bin"));
221        assert_eq!(ctx.get_env("HOME"), Some("/home/user"));
222        assert_eq!(ctx.get_env("NONEXISTENT"), None);
223
224        assert_eq!(ctx.remove_env("PATH"), Some("/usr/bin".to_string()));
225        assert_eq!(ctx.get_env("PATH"), None);
226    }
227
228    #[test]
229    fn test_context_merge_env() {
230        let mut ctx = SessionContext::new();
231        ctx.set_env("EXISTING", "value");
232
233        let mut new_vars = HashMap::new();
234        new_vars.insert("NEW1".to_string(), "val1".to_string());
235        new_vars.insert("NEW2".to_string(), "val2".to_string());
236
237        ctx.merge_env(new_vars);
238
239        assert_eq!(ctx.get_env("EXISTING"), Some("value"));
240        assert_eq!(ctx.get_env("NEW1"), Some("val1"));
241        assert_eq!(ctx.get_env("NEW2"), Some("val2"));
242    }
243
244    #[test]
245    fn test_context_record_execution() {
246        let mut ctx = SessionContext::new();
247
248        ctx.record_execution("ls -la", Some(0));
249        assert_eq!(ctx.last_command(), Some("ls -la"));
250        assert_eq!(ctx.last_exit_code(), Some(0));
251        assert_eq!(ctx.execution_count(), 1);
252        assert!(ctx.last_succeeded());
253        assert!(!ctx.last_failed());
254
255        ctx.record_execution("false", Some(1));
256        assert_eq!(ctx.last_command(), Some("false"));
257        assert_eq!(ctx.last_exit_code(), Some(1));
258        assert_eq!(ctx.execution_count(), 2);
259        assert!(!ctx.last_succeeded());
260        assert!(ctx.last_failed());
261    }
262
263    #[test]
264    fn test_state_probe_cwd_command() {
265        let cmd = StateProbe::cwd_command();
266        assert!(!cmd.is_empty());
267    }
268
269    #[test]
270    fn test_state_probe_env_command() {
271        let cmd = StateProbe::env_command();
272        assert!(!cmd.is_empty());
273    }
274
275    #[test]
276    fn test_state_probe_parse_cwd() {
277        let output = "/home/user\n";
278        let cwd = StateProbe::parse_cwd(output);
279        assert_eq!(cwd, Some(PathBuf::from("/home/user")));
280
281        let empty = "";
282        assert!(StateProbe::parse_cwd(empty).is_none());
283    }
284
285    #[test]
286    fn test_state_probe_parse_env() {
287        let output = "PATH=/usr/bin\nHOME=/home/user\nEMPTY=\n";
288        let env = StateProbe::parse_env(output);
289
290        assert_eq!(env.get("PATH"), Some(&"/usr/bin".to_string()));
291        assert_eq!(env.get("HOME"), Some(&"/home/user".to_string()));
292        assert_eq!(env.get("EMPTY"), Some(&"".to_string()));
293    }
294
295    #[test]
296    fn test_state_probe_marker() {
297        let marker1 = StateProbe::marker("TEST");
298        let marker2 = StateProbe::marker("TEST");
299
300        assert!(marker1.contains("TEST"));
301        assert!(marker1.contains("MARKER"));
302        // Markers should be unique (due to timestamp)
303        // Note: This might fail if called in the same nanosecond, but very unlikely
304        assert!(marker1 != marker2 || marker1.contains("TEST"));
305    }
306
307    #[test]
308    fn test_state_probe_echo_marker() {
309        let marker = "__TEST_MARKER__";
310        let cmd = StateProbe::echo_marker(marker);
311        assert_eq!(cmd, "echo __TEST_MARKER__");
312    }
313}