use super::bounded_child::{Outcome, first_version_field, run_bounded};
use super::restart_status::RestartStatus;
use super::systemd::{build_systemd_restart_command, select_unit_bus, sweep_stale_evolve_units};
use crate::brain::agent::{ProgressCallback, ProgressEvent};
use crate::brain::tools::error::{Result, ToolError};
use crate::brain::tools::r#trait::ToolResult;
use std::time::Duration;
const FORMULA: &str = "opencrabs";
const BREW_TIMEOUT: Duration = Duration::from_secs(600);
pub(crate) fn spawn_failure_message(err: &str) -> String {
format!(
"This build is managed by Homebrew, but `brew` could not be run: {err}. \
Upgrade with `brew upgrade {FORMULA}`."
)
}
pub(crate) fn upgrade_failure_message(stderr: &str) -> String {
let excerpt: String = stderr.chars().take(500).collect();
format!("brew upgrade failed: {excerpt}")
}
pub(crate) fn timeout_message(stage: &str, budget: Duration) -> String {
format!(
"`brew {stage}` was still running after {}s and has been killed. Whether the \
upgrade landed is unknown: check with `brew list --versions {FORMULA}`, and if \
it did not land, retry with `brew upgrade {FORMULA}`.",
budget.as_secs()
)
}
pub(crate) async fn read_installed_version(program: &str, budget: Duration) -> Option<String> {
match run_bounded(program, &["list", "--versions", FORMULA], budget)
.await
.ok()?
{
Outcome::Exited(output) if output.status.success() => {
first_version_field(&String::from_utf8_lossy(&output.stdout))
}
_ => None,
}
}
pub(crate) fn success_line(
current: &str,
read_back: Option<&str>,
restart: &RestartStatus,
) -> String {
let versions = match read_back {
Some(v) => format!("Evolved via Homebrew: v{current} -> v{v}."),
None => format!(
"Ran `brew upgrade {FORMULA}` from v{current}. brew reported no installed \
version afterwards, so none is claimed here; confirm with `brew list \
--versions {FORMULA}`."
),
};
let clause = restart.restart_clause();
if clause.is_empty() {
versions
} else {
format!("{versions} {clause}")
}
}
fn arm_systemd_backstop(sid: uuid::Uuid) -> RestartStatus {
if !std::path::Path::new("/run/systemd/system").exists() {
return RestartStatus::NotSystemd;
}
let Some(use_user_units) = select_unit_bus(sid) else {
return RestartStatus::NoUnitsMatched;
};
sweep_stale_evolve_units(use_user_units, sid);
let pid = std::process::id();
match build_systemd_restart_command(pid, use_user_units).spawn() {
Ok(child) => {
tracing::info!(
target: "evolve",
unit = %format!("opencrabs-evolve-{pid}"),
systemd_run_pid = ?child.id(),
session_id = %sid,
"evolve: systemd-run spawned; daemon will restart in 3s"
);
RestartStatus::Scheduled
}
Err(e) => {
tracing::warn!(
target: "evolve",
unit = %format!("opencrabs-evolve-{pid}"),
error = %e,
session_id = %sid,
"evolve: failed to spawn systemd-run, the daemon will NOT auto-restart \
and a manual restart is required to load the new binary"
);
RestartStatus::SpawnFailed(e.to_string())
}
}
}
pub(crate) fn restart_ready_status(current: &str, read_back: Option<&str>) -> String {
let installed = match read_back {
Some(v) => format!("v{v}"),
None => "an unconfirmed version (brew reported nothing)".to_string(),
};
format!("Evolved via Homebrew: v{current} -> {installed}. Restarting now.")
}
pub(crate) async fn evolve_with(
sid: uuid::Uuid,
current_version: &str,
latest_version: &str,
progress: Option<&ProgressCallback>,
program: &str,
budget: Duration,
) -> Result<ToolResult> {
tracing::info!(
target: "evolve",
current_version,
latest_version,
session_id = %sid,
"evolve: running `brew upgrade {FORMULA}` (latest_version is the pre-run \
GitHub release name, not a claim about what will be installed)"
);
let update_problem = match run_bounded(program, &["update"], budget).await {
Err(e) => Some(format!("could not spawn ({e})")),
Ok(Outcome::TimedOut) => Some(format!(
"outlived its {}s budget and was killed",
budget.as_secs()
)),
Ok(Outcome::Exited(output)) if !output.status.success() => {
Some(format!("exited {}", output.status))
}
Ok(Outcome::Exited(_)) => None,
};
if let Some(problem) = update_problem {
tracing::warn!(
target: "evolve",
problem = %problem,
session_id = %sid,
"evolve: `brew update` did not succeed, continuing on the existing formula index"
);
}
let output = match run_bounded(program, &["upgrade", FORMULA], budget).await {
Err(e) => {
tracing::warn!(
target: "evolve",
error = %e,
session_id = %sid,
"evolve: failed to spawn `brew` (is Homebrew on PATH?)"
);
return Err(ToolError::Execution(spawn_failure_message(&e.to_string())));
}
Ok(Outcome::TimedOut) => {
tracing::warn!(
target: "evolve",
budget_secs = budget.as_secs(),
session_id = %sid,
"evolve: `brew upgrade` outlived its budget and was killed"
);
return Ok(ToolResult::error(timeout_message("upgrade", budget)));
}
Ok(Outcome::Exited(output)) => output,
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
tracing::warn!(
target: "evolve",
exit_status = %output.status,
session_id = %sid,
"evolve: `brew upgrade opencrabs` failed"
);
return Ok(ToolResult::error(upgrade_failure_message(&stderr)));
}
let read_back = read_installed_version(program, budget).await;
if read_back.is_none() {
tracing::warn!(
target: "evolve",
session_id = %sid,
"evolve: brew reported no installed version after the upgrade, so the success \
line claims none instead of borrowing the pre-run release name"
);
}
let restart = arm_systemd_backstop(sid);
if let Some(cb) = progress {
cb(
sid,
ProgressEvent::RestartReady {
status: restart_ready_status(current_version, read_back.as_deref()),
binary_path: None,
},
);
}
Ok(ToolResult::success(success_line(
current_version,
read_back.as_deref(),
&restart,
)))
}
pub(crate) async fn evolve(
sid: uuid::Uuid,
current_version: &str,
latest_version: &str,
progress: Option<&ProgressCallback>,
) -> Result<ToolResult> {
evolve_with(
sid,
current_version,
latest_version,
progress,
"brew",
BREW_TIMEOUT,
)
.await
}