shell_tunnel/session/
context.rs1use std::collections::HashMap;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Default)]
11pub struct SessionContext {
12 cwd: Option<PathBuf>,
14 env: HashMap<String, String>,
16 last_command: Option<String>,
18 last_exit_code: Option<i32>,
20 execution_count: u64,
22}
23
24impl SessionContext {
25 pub fn new() -> Self {
27 Self::default()
28 }
29
30 pub fn with_cwd(cwd: impl Into<PathBuf>) -> Self {
32 Self {
33 cwd: Some(cwd.into()),
34 ..Default::default()
35 }
36 }
37
38 pub fn cwd(&self) -> Option<&PathBuf> {
40 self.cwd.as_ref()
41 }
42
43 pub fn set_cwd(&mut self, cwd: impl Into<PathBuf>) {
45 self.cwd = Some(cwd.into());
46 }
47
48 pub fn clear_cwd(&mut self) {
50 self.cwd = None;
51 }
52
53 pub fn env(&self) -> &HashMap<String, String> {
55 &self.env
56 }
57
58 pub fn get_env(&self, key: &str) -> Option<&str> {
60 self.env.get(key).map(|s| s.as_str())
61 }
62
63 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 pub fn remove_env(&mut self, key: &str) -> Option<String> {
70 self.env.remove(key)
71 }
72
73 pub fn merge_env(&mut self, vars: HashMap<String, String>) {
75 self.env.extend(vars);
76 }
77
78 pub fn last_command(&self) -> Option<&str> {
80 self.last_command.as_deref()
81 }
82
83 pub fn last_exit_code(&self) -> Option<i32> {
85 self.last_exit_code
86 }
87
88 pub fn execution_count(&self) -> u64 {
90 self.execution_count
91 }
92
93 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 pub fn last_succeeded(&self) -> bool {
102 self.last_exit_code == Some(0)
103 }
104
105 pub fn last_failed(&self) -> bool {
107 matches!(self.last_exit_code, Some(code) if code != 0)
108 }
109}
110
111pub struct StateProbe;
116
117impl StateProbe {
118 #[cfg(unix)]
120 pub fn cwd_command() -> &'static str {
121 "pwd"
122 }
123
124 #[cfg(windows)]
126 pub fn cwd_command() -> &'static str {
127 "cd"
128 }
129
130 #[cfg(unix)]
132 pub fn env_command() -> &'static str {
133 "env"
134 }
135
136 #[cfg(windows)]
138 pub fn env_command() -> &'static str {
139 "set"
140 }
141
142 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 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 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 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 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}