use crate::docker_cleanup::docker_command;
use crate::execution_control::CommandRunner;
use crate::execution_store::ExecutionRecord;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionState {
Running,
Stopped,
Missing,
Unknown,
}
impl SessionState {
pub fn as_str(self) -> &'static str {
match self {
SessionState::Running => "running",
SessionState::Stopped => "stopped",
SessionState::Missing => "missing",
SessionState::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionProbe {
pub backend: Option<String>,
pub session_name: Option<String>,
pub state: SessionState,
pub alive: bool,
pub container_status: Option<String>,
}
impl Default for SessionProbe {
fn default() -> Self {
SessionProbe {
backend: None,
session_name: None,
state: SessionState::Unknown,
alive: false,
container_status: None,
}
}
}
pub fn map_docker_status_to_state(status: Option<&str>) -> SessionState {
let normalized = status.unwrap_or("").trim().to_lowercase();
if normalized.is_empty() {
return SessionState::Unknown;
}
if normalized == "running" || normalized == "restarting" {
return SessionState::Running;
}
SessionState::Stopped
}
pub fn parse_screen_session_state(output: &str, session_name: &str) -> SessionState {
for line in output.lines() {
let first_column = line.split_whitespace().next().unwrap_or("");
let Some((pid, name)) = first_column.split_once('.') else {
continue;
};
if !pid.is_empty() && pid.chars().all(|c| c.is_ascii_digit()) && name == session_name {
return SessionState::Running;
}
}
SessionState::Missing
}
fn record_option<'a>(record: &'a ExecutionRecord, key: &str) -> Option<&'a str> {
record.options.get(key).and_then(|value| value.as_str())
}
pub fn probe_session<R: CommandRunner>(record: &ExecutionRecord, runner: &R) -> SessionProbe {
let backend = record_option(record, "isolated").map(str::to_string);
let session_name = record_option(record, "sessionName").map(str::to_string);
let mut probe = SessionProbe {
backend: backend.clone(),
session_name: session_name.clone(),
..SessionProbe::default()
};
let (Some(backend), Some(session_name)) = (backend, session_name) else {
return probe;
};
match backend.as_str() {
"docker" => {
let docker = docker_command().to_string_lossy().to_string();
let result = runner.run(
&docker,
&[
"inspect".to_string(),
"-f".to_string(),
"{{.State.Status}}".to_string(),
session_name,
],
);
if !result.success {
probe.state = SessionState::Missing;
return probe;
}
let status = result.stdout.trim().to_string();
probe.container_status = (!status.is_empty()).then_some(status);
probe.state = map_docker_status_to_state(probe.container_status.as_deref());
}
"screen" => {
let result = runner.run("screen", &["-ls".to_string()]);
let combined = format!("{}{}", result.stdout, result.stderr);
probe.state = parse_screen_session_state(&combined, &session_name);
}
"tmux" => {
let result = runner.run(
"tmux",
&["has-session".to_string(), "-t".to_string(), session_name],
);
probe.state = if result.success {
SessionState::Running
} else {
SessionState::Missing
};
}
_ => {}
}
probe.alive = probe.state == SessionState::Running;
probe
}
#[cfg(test)]
#[path = "session_probe_cases.rs"]
mod tests;