use serde::Serialize;
use octl_core::{read_manifest_opt, read_node_opt, Kind, NodeId, RunLock, RunPaths, Status};
use crate::error::CliError;
use crate::output::{self, OutputFormat, OutputSpec};
use crate::run::dto::{RunSummary, SupervisorState, SupervisorView};
use crate::run::{from_core, runs_root};
pub struct Args<'a> {
pub status: Option<String>,
pub kind: Option<String>,
pub spec: &'a OutputSpec,
pub warnings: &'a [String],
}
#[derive(Serialize)]
struct ListPayload {
runs: Vec<RunSummary>,
}
const STILLBORN_LIST_GRACE_SECS: i64 = 900;
const STILLBORN_LIST_GRACE_ENV: &str = "OCTL_STILLBORN_LIST_GRACE_SECS";
fn stillborn_list_grace() -> chrono::Duration {
let secs = std::env::var(STILLBORN_LIST_GRACE_ENV)
.ok()
.and_then(|v| v.trim().parse::<i64>().ok())
.unwrap_or(STILLBORN_LIST_GRACE_SECS);
chrono::Duration::seconds(secs)
}
pub fn run(args: Args<'_>) -> Result<(), CliError> {
let root = crate::home::root_dir()?;
let runs_dir = runs_root(&root);
if let Some(s) = &args.status {
if s.trim().is_empty() {
return Err(CliError::user(
"invalid_value",
"--status must not be empty",
));
}
}
if let Some(k) = &args.kind {
if k.trim().is_empty() {
return Err(CliError::user("invalid_value", "--kind must not be empty"));
}
}
let now = chrono::Utc::now();
let stillborn_grace = stillborn_list_grace();
let mut out: Vec<RunSummary> = Vec::new();
let entries = match std::fs::read_dir(&runs_dir) {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return emit(out, args.spec, args.warnings);
}
Err(e) => {
return Err(CliError::system(
"io_error",
format!("read_dir {}: {}", runs_dir.display(), e),
));
}
};
for ent in entries {
let ent = ent.map_err(|e| CliError::system("io_error", e.to_string()))?;
if !ent.file_type().is_ok_and(|t| t.is_dir()) {
continue;
}
let Some(run_id) = ent.file_name().to_str().map(str::to_string) else {
continue;
};
let Ok(paths) = RunPaths::new(ent.path(), run_id) else {
continue;
};
let scanned = RunLock::with_shared_lock(&paths.lock(), || {
let Some(m) = read_manifest_opt(&paths)? else {
return Ok(None);
};
let supervisor = SupervisorView::probe(&paths);
let stillborn = crate::run::stalled::is_stillborn(
m.status,
supervisor.presumed_working(),
m.node_count,
m.created_at,
m.updated_at,
) && now.signed_duration_since(m.created_at) > stillborn_grace;
let stalled_orchestrate =
if !stillborn && m.status == Status::Pending && m.kind == Kind::Orchestrate {
let driver_id = NodeId::parse_str(crate::run::stalled::DRIVER_NODE_ID)
.expect("DRIVER_NODE_ID is a valid node id");
crate::run::stalled::is_stalled(
m.status,
m.kind,
read_node_opt(&paths, &driver_id)?.as_ref(),
now,
)
} else {
false
};
Ok(Some((m, supervisor, stalled_orchestrate, stillborn)))
})
.map_err(from_core)?;
let (m, supervisor, stalled_orchestrate, stillborn) = match scanned {
Some(v) => v,
None => continue, };
let stalled = stalled_orchestrate || stillborn;
let summary = RunSummary::from(&m)
.with_supervisor(supervisor)
.with_stalled(stalled)
.with_stillborn(stillborn);
if let Some(filter) = &args.status {
if &summary.status != filter {
continue;
}
}
if let Some(filter) = &args.kind {
if &summary.kind != filter {
continue;
}
}
out.push(summary);
}
out.sort_by_key(|r| std::cmp::Reverse(r.created_at));
emit(out, args.spec, args.warnings)
}
fn emit(runs: Vec<RunSummary>, spec: &OutputSpec, warnings: &[String]) -> Result<(), CliError> {
match spec.format {
OutputFormat::Json | OutputFormat::Jsonl => {
output::emit_envelope(&ListPayload { runs }, spec, warnings)?;
}
OutputFormat::Text => {
if runs.is_empty() {
println!("(no runs)");
}
for r in &runs {
let sup = match r.supervisor.state {
SupervisorState::Alive => match r.supervisor.pid {
Some(pid) => format!("sup:alive({pid})"),
None => "sup:alive".to_string(),
},
SupervisorState::Dead => match r.supervisor.pid {
Some(pid) => format!("sup:dead({pid})"),
None => "sup:dead".to_string(),
},
SupervisorState::NotRecorded => "sup:none".to_string(),
SupervisorState::Unreadable => "sup:unreadable".to_string(),
SupervisorState::Unknown => "sup:unknown".to_string(),
};
let status = if r.stillborn {
format!("{} (stillborn)", r.status)
} else if r.stalled {
format!("{} (stalled)", r.status)
} else {
r.status.clone()
};
println!(
"{}\t{}\t{}\t{}\t{}\t{}",
r.run_id,
r.kind,
status,
r.node_count,
sup,
output::escape_one_line(&r.title)
);
}
output::emit_text_warnings(warnings);
}
}
Ok(())
}