pub async fn perform_upgrade(crate_name: &str) -> anyhow::Result<()> {
tracing::info!(crate_name, "running: cargo install {} --locked", crate_name);
let status = tokio::process::Command::new("cargo")
.args(["install", crate_name, "--locked"])
.status()
.await
.map_err(|e| anyhow::anyhow!("failed to spawn `cargo install`: {e}"))?;
if status.success() {
tracing::info!(crate_name, "cargo install succeeded");
Ok(())
} else {
Err(anyhow::anyhow!(
"`cargo install {crate_name} --locked` exited with status {status}"
))
}
}
pub async fn verify_installed_binary(binary_name: &str) -> anyhow::Result<()> {
let cargo_bin = dirs::home_dir().map(|h| h.join(".cargo").join("bin").join(binary_name));
let bin_path = match cargo_bin {
Some(p) if p.exists() => p.to_string_lossy().into_owned(),
_ => {
let which_out = tokio::process::Command::new("which")
.arg(binary_name)
.output()
.await
.map_err(|e| anyhow::anyhow!("failed to run `which {binary_name}`: {e}"))?;
if !which_out.status.success() {
return Err(anyhow::anyhow!(
"binary `{binary_name}` not found in ~/.cargo/bin or PATH"
));
}
String::from_utf8_lossy(&which_out.stdout).trim().to_owned()
}
};
tracing::debug!(
binary_name,
bin_path,
"probing installed binary with --version"
);
let result = tokio::time::timeout(
std::time::Duration::from_secs(10),
tokio::process::Command::new(&bin_path)
.arg("--version")
.output(),
)
.await;
match result {
Ok(Ok(output)) if output.status.success() => {
let version_line = String::from_utf8_lossy(&output.stdout);
tracing::info!(
binary_name,
version = version_line.trim(),
"health gate passed"
);
Ok(())
}
Ok(Ok(output)) => Err(anyhow::anyhow!(
"`{bin_path} --version` exited with status {} — new binary may be broken",
output.status
)),
Ok(Err(e)) => Err(anyhow::anyhow!(
"failed to spawn `{bin_path} --version`: {e}"
)),
Err(_) => Err(anyhow::anyhow!(
"`{bin_path} --version` timed out after 10 s — new binary may be hung"
)),
}
}
pub fn is_launchd_supervised() -> bool {
#[cfg(target_os = "macos")]
{
let has_xpc = std::env::var("XPC_SERVICE_NAME")
.map(|v| !v.is_empty())
.unwrap_or(false);
let in_terminal = std::env::var("TERM_PROGRAM")
.map(|v| !v.is_empty())
.unwrap_or(false);
if has_xpc && !in_terminal {
return true;
}
#[cfg(unix)]
{
let ppid = unsafe { libc::getppid() };
if ppid == 1 {
return true;
}
}
}
false
}
pub async fn upgrade_and_restart(
crate_name: &str,
binary_name: &str,
) -> anyhow::Result<Option<String>> {
perform_upgrade(crate_name).await?;
verify_installed_binary(binary_name).await.map_err(|e| {
anyhow::anyhow!(
"health gate failed after installing {crate_name}: {e}\n\
The old binary is still running — do not restart manually until resolved."
)
})?;
if is_launchd_supervised() {
tracing::info!(
crate_name,
binary_name,
"upgrade succeeded; restarting under launchd supervision"
);
eprintln!("[trusty-common] {binary_name} upgraded successfully — restarting under launchd");
std::process::exit(1);
}
let hint = format!(
"{binary_name} upgraded successfully. \
No supervisor detected — please restart the daemon to apply the new binary."
);
tracing::info!(hint, "no launchd supervision detected; restart required");
Ok(Some(hint))
}