1use crate::detection::{Element, ElementDetector};
8use crate::pty::{key_to_escape_sequence, PtyError, PtyHandle};
9use crate::sync_utils::{mutex_lock_or_recover, rwlock_read_or_recover, rwlock_write_or_recover};
10use crate::terminal::{CursorPosition, ScreenBuffer, VirtualTerminal};
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, VecDeque};
14use std::fs;
15use std::io::{BufReader, BufWriter};
16use std::path::PathBuf;
17use std::sync::{Arc, Mutex, RwLock};
18use std::time::Instant;
19use thiserror::Error;
20use uuid::Uuid;
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
31#[serde(transparent)]
32pub struct SessionId(String);
33
34impl SessionId {
35 pub fn new(id: impl Into<String>) -> Self {
37 Self(id.into())
38 }
39
40 pub fn generate() -> Self {
42 Self(Uuid::new_v4().to_string()[..8].to_string())
43 }
44
45 pub fn as_str(&self) -> &str {
47 &self.0
48 }
49}
50
51impl std::fmt::Display for SessionId {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 write!(f, "{}", self.0)
54 }
55}
56
57impl AsRef<str> for SessionId {
58 fn as_ref(&self) -> &str {
59 &self.0
60 }
61}
62
63impl From<String> for SessionId {
64 fn from(s: String) -> Self {
65 Self(s)
66 }
67}
68
69impl From<&str> for SessionId {
70 fn from(s: &str) -> Self {
71 Self(s.to_string())
72 }
73}
74
75#[derive(Clone, Debug)]
81pub struct RecordingFrame {
82 pub timestamp_ms: u64,
83 pub screen: String,
84}
85
86struct RecordingState {
88 is_recording: bool,
89 start_time: Instant,
90 frames: Vec<RecordingFrame>,
91}
92
93impl RecordingState {
94 fn new() -> Self {
95 Self {
96 is_recording: false,
97 start_time: Instant::now(),
98 frames: Vec::new(),
99 }
100 }
101}
102
103#[derive(Clone, Debug)]
105pub struct TraceEntry {
106 pub timestamp_ms: u64,
107 pub action: String,
108 pub details: Option<String>,
109}
110
111struct TraceState {
113 is_tracing: bool,
114 start_time: Instant,
115 entries: VecDeque<TraceEntry>,
116 max_entries: usize,
117}
118
119impl TraceState {
120 fn new() -> Self {
121 Self {
122 is_tracing: false,
123 start_time: Instant::now(),
124 entries: VecDeque::new(),
125 max_entries: 1000,
126 }
127 }
128}
129
130pub struct RecordingStatus {
132 pub is_recording: bool,
133 pub frame_count: usize,
134 pub duration_ms: u64,
135}
136
137#[derive(Clone, Debug)]
139pub struct ErrorEntry {
140 pub timestamp: String,
141 pub message: String,
142 pub source: String,
143}
144
145struct ErrorState {
147 entries: VecDeque<ErrorEntry>,
148 max_entries: usize,
149}
150
151impl ErrorState {
152 fn new() -> Self {
153 Self {
154 entries: VecDeque::new(),
155 max_entries: 500,
156 }
157 }
158}
159
160#[derive(Error, Debug)]
161pub enum SessionError {
162 #[error("Session not found: {0}")]
163 NotFound(String),
164 #[error("No active session")]
165 NoActiveSession,
166 #[error("PTY error: {0}")]
167 Pty(#[from] PtyError),
168 #[error("Element not found: {0}")]
169 ElementNotFound(String),
170 #[error("Invalid key: {0}")]
171 InvalidKey(String),
172 #[error("Timeout waiting for condition")]
173 Timeout,
174}
175
176#[derive(Default)]
178struct ModifierState {
179 ctrl: bool,
180 alt: bool,
181 shift: bool,
182 meta: bool,
183}
184
185pub struct Session {
187 pub id: SessionId,
188 pub command: String,
189 pub created_at: DateTime<Utc>,
190 pty: PtyHandle,
191 terminal: VirtualTerminal,
192 detector: ElementDetector,
193 cached_elements: Vec<Element>,
195 recording: RecordingState,
197 trace: TraceState,
199 held_modifiers: ModifierState,
201 errors: ErrorState,
203}
204
205impl Session {
206 fn new(id: SessionId, command: String, pty: PtyHandle, cols: u16, rows: u16) -> Self {
207 Self {
208 id,
209 command,
210 created_at: Utc::now(),
211 pty,
212 terminal: VirtualTerminal::new(cols, rows),
213 detector: ElementDetector::new(),
214 cached_elements: Vec::new(),
215 recording: RecordingState::new(),
216 trace: TraceState::new(),
217 held_modifiers: ModifierState::default(),
218 errors: ErrorState::new(),
219 }
220 }
221
222 pub fn pid(&self) -> Option<u32> {
224 self.pty.pid()
225 }
226
227 pub fn is_running(&mut self) -> bool {
229 self.pty.is_running()
230 }
231
232 pub fn size(&self) -> (u16, u16) {
234 self.terminal.size()
235 }
236
237 pub fn update(&mut self) -> Result<(), SessionError> {
239 let mut buf = [0u8; 4096];
240
241 loop {
243 match self.pty.try_read(&mut buf, 10) {
244 Ok(0) => break, Ok(n) => {
246 self.terminal.process(&buf[..n]);
247 }
248 Err(_) => break,
249 }
250 }
251
252 Ok(())
253 }
254
255 pub fn screen_text(&self) -> String {
257 self.terminal.screen_text()
258 }
259
260 pub fn screen_buffer(&self) -> ScreenBuffer {
262 self.terminal.screen_buffer()
263 }
264
265 pub fn cursor(&self) -> CursorPosition {
267 self.terminal.cursor()
268 }
269
270 pub fn detect_elements(&mut self) -> &[Element] {
272 let screen_text = self.terminal.screen_text();
273 let screen_buffer = self.terminal.screen_buffer();
274 self.cached_elements = self.detector.detect(&screen_text, Some(&screen_buffer));
275 &self.cached_elements
276 }
277
278 pub fn elements(&self) -> &[Element] {
280 &self.cached_elements
281 }
282
283 pub fn find_element(&self, element_ref: &str) -> Option<&Element> {
285 self.detector
286 .find_by_ref(&self.cached_elements, element_ref)
287 }
288
289 pub fn keystroke(&self, key: &str) -> Result<(), SessionError> {
291 let seq =
292 key_to_escape_sequence(key).ok_or_else(|| SessionError::InvalidKey(key.to_string()))?;
293 self.pty.write(&seq)?;
294 Ok(())
295 }
296
297 pub fn keydown(&mut self, key: &str) -> Result<(), SessionError> {
304 match key.to_lowercase().as_str() {
305 "ctrl" | "control" => self.held_modifiers.ctrl = true,
306 "alt" => self.held_modifiers.alt = true,
307 "shift" => self.held_modifiers.shift = true,
308 "meta" | "cmd" | "command" | "win" | "super" => self.held_modifiers.meta = true,
309 _ => {
310 return Err(SessionError::InvalidKey(format!(
311 "{}. Only modifier keys (Ctrl, Alt, Shift, Meta) can be held",
312 key
313 )))
314 }
315 }
316 Ok(())
317 }
318
319 pub fn keyup(&mut self, key: &str) -> Result<(), SessionError> {
321 match key.to_lowercase().as_str() {
322 "ctrl" | "control" => self.held_modifiers.ctrl = false,
323 "alt" => self.held_modifiers.alt = false,
324 "shift" => self.held_modifiers.shift = false,
325 "meta" | "cmd" | "command" | "win" | "super" => self.held_modifiers.meta = false,
326 _ => {
327 return Err(SessionError::InvalidKey(format!(
328 "{}. Only modifier keys (Ctrl, Alt, Shift, Meta) can be released",
329 key
330 )))
331 }
332 }
333 Ok(())
334 }
335
336 pub fn type_text(&self, text: &str) -> Result<(), SessionError> {
338 self.pty.write_str(text)?;
339 Ok(())
340 }
341
342 pub fn click(&mut self, element_ref: &str) -> Result<(), SessionError> {
344 self.update()?;
345 self.detect_elements();
346
347 let element = self
348 .find_element(element_ref)
349 .ok_or_else(|| SessionError::ElementNotFound(element_ref.to_string()))?;
350
351 match element.element_type.as_str() {
352 "checkbox" | "radio" => {
353 self.pty.write(b" ")?; }
355 _ => {
356 self.pty.write(b"\r")?; }
358 }
359
360 Ok(())
361 }
362
363 pub fn fill(&mut self, element_ref: &str, value: &str) -> Result<(), SessionError> {
365 self.update()?;
366 self.detect_elements();
367
368 let _element = self
369 .find_element(element_ref)
370 .ok_or_else(|| SessionError::ElementNotFound(element_ref.to_string()))?;
371
372 self.pty.write_str(value)?;
373
374 Ok(())
375 }
376
377 pub fn resize(&mut self, cols: u16, rows: u16) -> Result<(), SessionError> {
379 self.pty.resize(cols, rows)?;
380 self.terminal.resize(cols, rows);
381 Ok(())
382 }
383
384 pub fn kill(&mut self) -> Result<(), SessionError> {
386 self.pty.kill()?;
387 Ok(())
388 }
389
390 pub fn pty_write(&self, data: &[u8]) -> Result<(), SessionError> {
392 self.pty.write(data)?;
393 Ok(())
394 }
395
396 pub fn pty_try_read(&self, buf: &mut [u8], timeout_ms: i32) -> Result<usize, SessionError> {
398 self.pty
399 .try_read(buf, timeout_ms)
400 .map_err(SessionError::Pty)
401 }
402
403 pub fn pty_reader_fd(&self) -> std::os::fd::RawFd {
405 self.pty.reader_fd()
406 }
407
408 pub fn start_recording(&mut self) {
410 self.recording.is_recording = true;
411 self.recording.start_time = Instant::now();
412 self.recording.frames.clear();
413
414 let screen = self.terminal.screen_text();
415 self.recording.frames.push(RecordingFrame {
416 timestamp_ms: 0,
417 screen,
418 });
419 }
420
421 pub fn stop_recording(&mut self) -> Vec<RecordingFrame> {
423 self.recording.is_recording = false;
424 std::mem::take(&mut self.recording.frames)
425 }
426
427 pub fn recording_status(&self) -> RecordingStatus {
429 RecordingStatus {
430 is_recording: self.recording.is_recording,
431 frame_count: self.recording.frames.len(),
432 duration_ms: if self.recording.is_recording {
433 self.recording.start_time.elapsed().as_millis() as u64
434 } else {
435 0
436 },
437 }
438 }
439
440 pub fn capture_frame(&mut self) {
442 if self.recording.is_recording {
443 let screen = self.terminal.screen_text();
444 let timestamp_ms = self.recording.start_time.elapsed().as_millis() as u64;
445 self.recording.frames.push(RecordingFrame {
446 timestamp_ms,
447 screen,
448 });
449 }
450 }
451
452 pub fn start_trace(&mut self) {
454 self.trace.is_tracing = true;
455 self.trace.start_time = Instant::now();
456 self.trace.entries.clear();
457 }
458
459 pub fn stop_trace(&mut self) {
461 self.trace.is_tracing = false;
462 }
463
464 pub fn is_tracing(&self) -> bool {
466 self.trace.is_tracing
467 }
468
469 pub fn add_trace_entry(&mut self, action: &str, details: Option<&str>) {
471 if self.trace.is_tracing {
472 let timestamp_ms = self.trace.start_time.elapsed().as_millis() as u64;
473 self.trace.entries.push_back(TraceEntry {
474 timestamp_ms,
475 action: action.to_string(),
476 details: details.map(String::from),
477 });
478
479 while self.trace.entries.len() > self.trace.max_entries {
480 self.trace.entries.pop_front();
481 }
482 }
483 }
484
485 pub fn get_trace_entries(&self, count: usize) -> Vec<TraceEntry> {
487 let len = self.trace.entries.len();
488 let start = len.saturating_sub(count);
489 self.trace.entries.iter().skip(start).cloned().collect()
490 }
491
492 pub fn add_error(&mut self, message: &str, source: &str) {
494 let timestamp = Utc::now().to_rfc3339();
495 self.errors.entries.push_back(ErrorEntry {
496 timestamp,
497 message: message.to_string(),
498 source: source.to_string(),
499 });
500
501 while self.errors.entries.len() > self.errors.max_entries {
502 self.errors.entries.pop_front();
503 }
504 }
505
506 pub fn get_errors(&self, count: usize) -> Vec<ErrorEntry> {
508 let len = self.errors.entries.len();
509 let start = len.saturating_sub(count);
510 self.errors.entries.iter().skip(start).cloned().collect()
511 }
512
513 pub fn error_count(&self) -> usize {
515 self.errors.entries.len()
516 }
517
518 pub fn clear_errors(&mut self) {
520 self.errors.entries.clear();
521 }
522
523 pub fn clear_console(&mut self) {
525 self.terminal.clear();
526 }
527}
528
529pub struct SessionManager {
531 sessions: RwLock<HashMap<SessionId, Arc<Mutex<Session>>>>,
532 active_session: RwLock<Option<SessionId>>,
533 start_time: Instant,
534 persistence: SessionPersistence,
535}
536
537impl Default for SessionManager {
538 fn default() -> Self {
539 Self::new()
540 }
541}
542
543impl SessionManager {
544 pub fn new() -> Self {
545 let persistence = SessionPersistence::new();
546 let _ = persistence.cleanup_stale_sessions();
547
548 Self {
549 sessions: RwLock::new(HashMap::new()),
550 active_session: RwLock::new(None),
551 start_time: Instant::now(),
552 persistence,
553 }
554 }
555
556 #[allow(clippy::too_many_arguments)]
558 pub fn spawn(
559 &self,
560 command: &str,
561 args: &[String],
562 cwd: Option<&str>,
563 env: Option<&HashMap<String, String>>,
564 session_id: Option<String>,
565 cols: u16,
566 rows: u16,
567 ) -> Result<(SessionId, u32), SessionError> {
568 let id = session_id
569 .map(SessionId::new)
570 .unwrap_or_else(SessionId::generate);
571
572 let pty = PtyHandle::spawn(command, args, cwd, env, cols, rows)?;
573 let pid = pty.pid().unwrap_or(0);
574
575 let session = Session::new(id.clone(), command.to_string(), pty, cols, rows);
576 let session = Arc::new(Mutex::new(session));
577
578 let created_at = Utc::now().to_rfc3339();
582 let persisted = PersistedSession {
583 id: id.clone(),
584 command: command.to_string(),
585 pid,
586 created_at,
587 cols,
588 rows,
589 };
590
591 {
592 let mut sessions = rwlock_write_or_recover(&self.sessions);
593 sessions.insert(id.clone(), session);
594 }
595
596 {
597 let mut active = rwlock_write_or_recover(&self.active_session);
598 *active = Some(id.clone());
599 }
600
601 if let Err(e) = self.persistence.add_session(persisted) {
603 eprintln!("Warning: Failed to persist session metadata: {}", e);
604 }
605
606 Ok((id, pid))
607 }
608
609 pub fn get(&self, session_id: &str) -> Result<Arc<Mutex<Session>>, SessionError> {
611 let sessions = rwlock_read_or_recover(&self.sessions);
612 let id = SessionId::new(session_id);
613 sessions
614 .get(&id)
615 .cloned()
616 .ok_or_else(|| SessionError::NotFound(session_id.to_string()))
617 }
618
619 pub fn active(&self) -> Result<Arc<Mutex<Session>>, SessionError> {
621 let active_id = {
622 let active = rwlock_read_or_recover(&self.active_session);
623 active.clone()
624 };
625
626 match active_id {
627 Some(id) => self.get(id.as_str()),
628 None => Err(SessionError::NoActiveSession),
629 }
630 }
631
632 pub fn resolve(&self, session_id: Option<&str>) -> Result<Arc<Mutex<Session>>, SessionError> {
634 match session_id {
635 Some(id) => self.get(id),
636 None => self.active(),
637 }
638 }
639
640 pub fn set_active(&self, session_id: &str) -> Result<(), SessionError> {
642 let _ = self.get(session_id)?;
643
644 let mut active = rwlock_write_or_recover(&self.active_session);
645 *active = Some(SessionId::new(session_id));
646 Ok(())
647 }
648
649 pub fn list(&self) -> Vec<SessionInfo> {
653 let session_refs: Vec<(SessionId, Arc<Mutex<Session>>)> = {
656 let sessions = rwlock_read_or_recover(&self.sessions);
657 sessions
658 .iter()
659 .map(|(id, session)| (id.clone(), Arc::clone(session)))
660 .collect()
661 };
662
663 let active_id = rwlock_read_or_recover(&self.active_session).clone();
664
665 session_refs
666 .into_iter()
667 .map(|(id, session)| {
668 match session.try_lock() {
670 Ok(mut sess) => SessionInfo {
671 id: id.clone(),
672 command: sess.command.clone(),
673 pid: sess.pid().unwrap_or(0),
674 running: sess.is_running(),
675 created_at: sess.created_at.to_rfc3339(),
676 size: sess.size(),
677 is_active: active_id.as_ref() == Some(&id),
678 },
679 Err(_) => {
680 SessionInfo {
683 id: id.clone(),
684 command: "(busy)".to_string(),
685 pid: 0,
686 running: true,
687 created_at: "".to_string(),
688 size: (80, 24),
689 is_active: active_id.as_ref() == Some(&id),
690 }
691 }
692 }
693 })
694 .collect()
695 }
696
697 pub fn kill(&self, session_id: &str) -> Result<(), SessionError> {
702 let id = SessionId::new(session_id);
703
704 let session = {
707 let mut sessions = rwlock_write_or_recover(&self.sessions);
708 let mut active = rwlock_write_or_recover(&self.active_session);
709
710 let session = sessions
711 .remove(&id)
712 .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
713
714 if active.as_ref() == Some(&id) {
715 *active = None;
716 }
717
718 session
719 };
721
722 {
725 let mut sess = mutex_lock_or_recover(&session);
726 sess.kill()?;
727 }
728
729 if let Err(e) = self.persistence.remove_session(session_id) {
731 eprintln!("Warning: Failed to remove session from persistence: {}", e);
732 }
733
734 Ok(())
735 }
736
737 pub fn get_persisted_sessions(&self) -> Vec<PersistedSession> {
739 self.persistence.load()
740 }
741
742 pub fn cleanup_persisted(&self) -> std::io::Result<usize> {
744 self.persistence.cleanup_stale_sessions()
745 }
746
747 pub fn uptime_ms(&self) -> u64 {
749 self.start_time.elapsed().as_millis() as u64
750 }
751
752 pub fn session_count(&self) -> usize {
754 rwlock_read_or_recover(&self.sessions).len()
755 }
756
757 pub fn active_session_id(&self) -> Option<SessionId> {
759 rwlock_read_or_recover(&self.active_session).clone()
760 }
761}
762
763#[derive(Debug, Clone)]
765pub struct SessionInfo {
766 pub id: SessionId,
767 pub command: String,
768 pub pid: u32,
769 pub running: bool,
770 pub created_at: String,
771 pub size: (u16, u16),
772 pub is_active: bool,
773}
774
775lazy_static::lazy_static! {
776 pub static ref SESSION_MANAGER: SessionManager = SessionManager::new();
777}
778
779#[derive(Debug, Clone, Serialize, Deserialize)]
781pub struct PersistedSession {
782 pub id: SessionId,
783 pub command: String,
784 pub pid: u32,
785 pub created_at: String,
786 pub cols: u16,
787 pub rows: u16,
788}
789
790pub struct SessionPersistence {
792 path: PathBuf,
793}
794
795impl SessionPersistence {
796 pub fn new() -> Self {
798 let path = Self::sessions_file_path();
799 Self { path }
800 }
801
802 fn sessions_file_path() -> PathBuf {
804 let home = std::env::var("HOME")
805 .map(PathBuf::from)
806 .unwrap_or_else(|_| PathBuf::from("/tmp"));
807 let dir = home.join(".agent-tui");
808 dir.join("sessions.json")
809 }
810
811 fn ensure_dir(&self) -> std::io::Result<()> {
813 if let Some(parent) = self.path.parent() {
814 fs::create_dir_all(parent).map_err(|e| {
815 std::io::Error::new(
816 e.kind(),
817 format!("Failed to create directory '{}': {}", parent.display(), e),
818 )
819 })?;
820 }
821 Ok(())
822 }
823
824 pub fn load(&self) -> Vec<PersistedSession> {
826 if !self.path.exists() {
827 return Vec::new();
828 }
829
830 match fs::File::open(&self.path) {
831 Ok(file) => {
832 let reader = BufReader::new(file);
833 serde_json::from_reader(reader).unwrap_or_default()
834 }
835 Err(e) => {
836 eprintln!(
837 "Warning: Failed to open sessions file '{}': {}",
838 self.path.display(),
839 e
840 );
841 Vec::new()
842 }
843 }
844 }
845
846 pub fn save(&self, sessions: &[PersistedSession]) -> std::io::Result<()> {
848 self.ensure_dir()?;
849
850 let file = fs::File::create(&self.path).map_err(|e| {
851 std::io::Error::new(
852 e.kind(),
853 format!("Failed to create file '{}': {}", self.path.display(), e),
854 )
855 })?;
856 let writer = BufWriter::new(file);
857 serde_json::to_writer_pretty(writer, sessions).map_err(|e| {
858 std::io::Error::other(format!(
859 "Failed to write sessions to '{}': {}",
860 self.path.display(),
861 e
862 ))
863 })?;
864 Ok(())
865 }
866
867 pub fn add_session(&self, session: PersistedSession) -> std::io::Result<()> {
869 let mut sessions = self.load();
870
871 sessions.retain(|s| s.id != session.id);
872 sessions.push(session);
873
874 self.save(&sessions)
875 }
876
877 pub fn remove_session(&self, session_id: &str) -> std::io::Result<()> {
879 let mut sessions = self.load();
880 sessions.retain(|s| s.id.as_str() != session_id);
881 self.save(&sessions)
882 }
883
884 pub fn update_session(&self, session: PersistedSession) -> std::io::Result<()> {
886 let mut sessions = self.load();
887
888 if let Some(existing) = sessions.iter_mut().find(|s| s.id == session.id) {
889 *existing = session;
890 } else {
891 sessions.push(session);
892 }
893
894 self.save(&sessions)
895 }
896
897 pub fn get_session(&self, session_id: &str) -> Option<PersistedSession> {
899 self.load()
900 .into_iter()
901 .find(|s| s.id.as_str() == session_id)
902 }
903
904 pub fn cleanup_stale_sessions(&self) -> std::io::Result<usize> {
906 let sessions = self.load();
907 let mut cleaned = 0;
908
909 let active_sessions: Vec<PersistedSession> = sessions
910 .into_iter()
911 .filter(|s| {
912 let running = is_process_running(s.pid);
913 if !running {
914 cleaned += 1;
915 }
916 running
917 })
918 .collect();
919
920 self.save(&active_sessions)?;
921 Ok(cleaned)
922 }
923
924 pub fn clear(&self) -> std::io::Result<()> {
926 if self.path.exists() {
927 fs::remove_file(&self.path).map_err(|e| {
928 std::io::Error::new(
929 e.kind(),
930 format!("Failed to remove file '{}': {}", self.path.display(), e),
931 )
932 })?;
933 }
934 Ok(())
935 }
936}
937
938impl Default for SessionPersistence {
939 fn default() -> Self {
940 Self::new()
941 }
942}
943
944fn is_process_running(pid: u32) -> bool {
946 unsafe { libc::kill(pid as i32, 0) == 0 }
948}
949
950impl From<&SessionInfo> for PersistedSession {
952 fn from(info: &SessionInfo) -> Self {
953 PersistedSession {
954 id: info.id.clone(),
955 command: info.command.clone(),
956 pid: info.pid,
957 created_at: info.created_at.clone(),
958 cols: info.size.0,
959 rows: info.size.1,
960 }
961 }
962}
963
964#[cfg(test)]
965mod persistence_tests {
966 use super::*;
967
968 #[test]
969 fn test_persisted_session_serialization() {
970 let session = PersistedSession {
971 id: SessionId::new("test123"),
972 command: "bash".to_string(),
973 pid: 12345,
974 created_at: "2024-01-01T00:00:00Z".to_string(),
975 cols: 80,
976 rows: 24,
977 };
978
979 let json = serde_json::to_string(&session).unwrap();
980 let parsed: PersistedSession = serde_json::from_str(&json).unwrap();
981
982 assert_eq!(parsed.id, session.id);
983 assert_eq!(parsed.command, session.command);
984 assert_eq!(parsed.pid, session.pid);
985 }
986
987 #[test]
988 fn test_is_process_running() {
989 let current_pid = std::process::id();
991 assert!(is_process_running(current_pid));
992
993 assert!(!is_process_running(999999999));
995 }
996}