use serde::Serialize;
use serde_json::json;
use octl_core::{append_and_apply_event, read_manifest_opt};
use crate::error::CliError;
use crate::output::{self, OutputFormat, OutputSpec};
use crate::run::supervisor_spawn;
use crate::run::{from_core, run_paths};
use crate::supervise::pid_file;
#[derive(Serialize)]
struct ReattachPayload<'a> {
run_id: &'a str,
action: &'static str,
supervisor_pid: u32,
}
pub fn run(
run_id: &str,
once: bool,
max_iter: Option<u32>,
spec: &OutputSpec,
warnings: &[String],
) -> Result<(), CliError> {
let root = crate::home::root_dir()?;
let paths = run_paths(&root, run_id)?;
if read_manifest_opt(&paths).map_err(from_core)?.is_none() {
return Err(
CliError::user("run_not_found", format!("no run with id {run_id}"))
.with_invalid_value(run_id),
);
}
let recorded_pid = spawn_supervisor(&paths, run_id, once, max_iter)?;
let payload = ReattachPayload {
run_id,
action: "reattached",
supervisor_pid: recorded_pid,
};
match spec.format {
OutputFormat::Json | OutputFormat::Jsonl => {
output::emit_envelope(&payload, spec, warnings)?;
}
OutputFormat::Text => {
println!("reattached run {run_id} (supervisor pid {recorded_pid})");
output::emit_text_warnings(warnings);
}
}
Ok(())
}
pub fn spawn_supervisor(
paths: &octl_core::RunPaths,
run_id: &str,
once: bool,
max_iter: Option<u32>,
) -> Result<u32, CliError> {
let pid_path = paths.supervisor_pid();
if let Some((existing, start_time)) = pid_file::read_pid_record(&pid_path) {
if pid_file::pid_live_with_identity(existing, start_time) {
return Err(CliError::system(
"supervisor_already_running",
format!("supervisor pid {existing} for run {run_id} is alive (no reattach needed)"),
));
}
let exited_key = match start_time {
Some(st) => format!("stale-on-reattach:{existing}:{st}"),
None => format!("stale-on-reattach:{existing}"),
};
let _ = append_and_apply_event(
paths,
"supervisor.exited",
None,
Some(&exited_key),
json!({"pid": existing, "reason": "stale-on-reattach"}),
);
}
append_and_apply_event(
paths,
"supervisor.reattach-requested",
None,
None,
json!({}),
)
.map_err(from_core)?;
let log_path = paths.root.join("supervisor.stderr.log");
let mut cmd = supervisor_spawn::detached_supervise_command(run_id, &log_path, None)?;
if once {
cmd.arg("--once");
}
if let Some(n) = max_iter {
cmd.arg("--max-iter").arg(n.to_string());
}
supervisor_spawn::spawn_and_reap(&mut cmd, run_id)?;
let recorded_pid = supervisor_spawn::await_recorded_pid(paths).unwrap_or(0);
let _ = append_and_apply_event(
paths,
"supervisor.reattached",
None,
None,
json!({"pid": recorded_pid}),
);
Ok(recorded_pid)
}