start_command/
session_probe.rs1use crate::docker_cleanup::docker_command;
9use crate::execution_control::CommandRunner;
10use crate::execution_store::ExecutionRecord;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum SessionState {
15 Running,
17 Stopped,
19 Missing,
21 Unknown,
23}
24
25impl SessionState {
26 pub fn as_str(self) -> &'static str {
27 match self {
28 SessionState::Running => "running",
29 SessionState::Stopped => "stopped",
30 SessionState::Missing => "missing",
31 SessionState::Unknown => "unknown",
32 }
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct SessionProbe {
39 pub backend: Option<String>,
40 pub session_name: Option<String>,
41 pub state: SessionState,
42 pub alive: bool,
43 pub container_status: Option<String>,
44}
45
46impl Default for SessionProbe {
47 fn default() -> Self {
48 SessionProbe {
49 backend: None,
50 session_name: None,
51 state: SessionState::Unknown,
52 alive: false,
53 container_status: None,
54 }
55 }
56}
57
58pub fn map_docker_status_to_state(status: Option<&str>) -> SessionState {
60 let normalized = status.unwrap_or("").trim().to_lowercase();
61 if normalized.is_empty() {
62 return SessionState::Unknown;
63 }
64 if normalized == "running" || normalized == "restarting" {
65 return SessionState::Running;
66 }
67 SessionState::Stopped
68}
69
70pub fn parse_screen_session_state(output: &str, session_name: &str) -> SessionState {
75 for line in output.lines() {
76 let first_column = line.split_whitespace().next().unwrap_or("");
77 let Some((pid, name)) = first_column.split_once('.') else {
78 continue;
79 };
80 if !pid.is_empty() && pid.chars().all(|c| c.is_ascii_digit()) && name == session_name {
81 return SessionState::Running;
82 }
83 }
84 SessionState::Missing
85}
86
87fn record_option<'a>(record: &'a ExecutionRecord, key: &str) -> Option<&'a str> {
88 record.options.get(key).and_then(|value| value.as_str())
89}
90
91pub fn probe_session<R: CommandRunner>(record: &ExecutionRecord, runner: &R) -> SessionProbe {
93 let backend = record_option(record, "isolated").map(str::to_string);
94 let session_name = record_option(record, "sessionName").map(str::to_string);
95
96 let mut probe = SessionProbe {
97 backend: backend.clone(),
98 session_name: session_name.clone(),
99 ..SessionProbe::default()
100 };
101
102 let (Some(backend), Some(session_name)) = (backend, session_name) else {
103 return probe;
104 };
105
106 match backend.as_str() {
107 "docker" => {
108 let docker = docker_command().to_string_lossy().to_string();
109 let result = runner.run(
110 &docker,
111 &[
112 "inspect".to_string(),
113 "-f".to_string(),
114 "{{.State.Status}}".to_string(),
115 session_name,
116 ],
117 );
118 if !result.success {
119 probe.state = SessionState::Missing;
120 return probe;
121 }
122 let status = result.stdout.trim().to_string();
123 probe.container_status = (!status.is_empty()).then_some(status);
124 probe.state = map_docker_status_to_state(probe.container_status.as_deref());
125 }
126 "screen" => {
127 let result = runner.run("screen", &["-ls".to_string()]);
128 let combined = format!("{}{}", result.stdout, result.stderr);
131 probe.state = parse_screen_session_state(&combined, &session_name);
132 }
133 "tmux" => {
134 let result = runner.run(
135 "tmux",
136 &["has-session".to_string(), "-t".to_string(), session_name],
137 );
138 probe.state = if result.success {
139 SessionState::Running
140 } else {
141 SessionState::Missing
142 };
143 }
144 _ => {}
145 }
146
147 probe.alive = probe.state == SessionState::Running;
148 probe
149}
150
151#[cfg(test)]
152#[path = "session_probe_cases.rs"]
153mod tests;