huskies: merge 1144 story Gateway trampoline-restart: detached helper survives the gateway's own death

This commit is contained in:
dave
2026-05-19 18:13:26 +00:00
parent 20ec690e22
commit de638603cd
12 changed files with 656 additions and 1 deletions
+3
View File
@@ -4,6 +4,9 @@
//! Business logic lives in `service::gateway`, HTTP handlers in `http::gateway`.
//! This file contains only the `run` entrypoint and `build_gateway_route` wiring.
/// Gateway rebuild — builds the new binary and launches the detached trampoline.
pub mod rebuild;
use crate::http::gateway::*;
use crate::rebuild::ShutdownReason;
use crate::service::gateway::{self, GatewayState};
+115
View File
@@ -0,0 +1,115 @@
//! Gateway rebuild — builds the new huskies binary and hands off to the trampoline.
//!
//! The trampoline is spawned as a detached process (new Unix session) so that it
//! survives the gateway's own death. On success the gateway continues running
//! until the trampoline kills it; the new gateway then posts "gateway X.Y.Z ready".
use std::path::Path;
/// Build the huskies binary and launch the detached trampoline to swap the gateway.
///
/// Returns `Err(message)` (shown to the user in chat) if the build or trampoline
/// launch fails. On success returns `Ok(())` — the trampoline is now running
/// in a detached process and will kill this gateway and replace it with the new
/// binary within 10 s.
pub async fn rebuild_gateway(config_dir: &Path, gateway_port: u16) -> Result<(), String> {
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let workspace_root = manifest_dir
.parent()
.ok_or("cannot determine workspace root from CARGO_MANIFEST_DIR")?;
crate::slog!(
"[gateway-rebuild] Building from workspace root: {}",
workspace_root.display()
);
// Rebuild the frontend bundle so rust-embed picks up the latest assets.
let frontend_dir = workspace_root.join("frontend");
if frontend_dir.join("package.json").exists() {
crate::slog!("[gateway-rebuild] Building frontend");
let fe_output = tokio::task::spawn_blocking({
let dir = frontend_dir.clone();
move || {
std::process::Command::new("npm")
.args(["run", "build"])
.current_dir(&dir)
.output()
}
})
.await
.map_err(|e| format!("frontend build task panicked: {e}"))?
.map_err(|e| format!("failed to run npm run build: {e}"))?;
if !fe_output.status.success() {
let stderr = String::from_utf8_lossy(&fe_output.stderr);
return Err(format!("Frontend build failed:\n{stderr}"));
}
crate::slog!("[gateway-rebuild] Frontend build succeeded");
}
// Build the server binary matching the current profile.
let build_args: Vec<&str> = if cfg!(debug_assertions) {
vec!["build", "-p", "huskies"]
} else {
vec!["build", "--release", "-p", "huskies"]
};
crate::slog!("[gateway-rebuild] cargo {}", build_args.join(" "));
let output = tokio::task::spawn_blocking({
let root = workspace_root.to_path_buf();
move || {
std::process::Command::new("cargo")
.args(&build_args)
.current_dir(&root)
.output()
}
})
.await
.map_err(|e| format!("build task panicked: {e}"))?
.map_err(|e| format!("failed to run cargo build: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
crate::slog!("[gateway-rebuild] Build failed");
return Err(format!("Build failed:\n{stderr}"));
}
crate::slog!("[gateway-rebuild] Build succeeded — launching trampoline");
// Paths for the new and old binaries.
let new_binary = if cfg!(debug_assertions) {
workspace_root.join("target/debug/huskies")
} else {
workspace_root.join("target/release/huskies")
};
let old_binary =
std::env::current_exe().map_err(|e| format!("cannot locate current binary: {e}"))?;
let huskies_dir = config_dir.join(".huskies");
std::fs::create_dir_all(&huskies_dir)
.map_err(|e| format!("cannot create .huskies dir: {e}"))?;
let backup_binary = huskies_dir.join("huskies_backup");
// Current gateway args (skip argv[0]).
let gateway_args: Vec<String> = std::env::args().skip(1).collect();
let job = crate::trampoline::TrampolineJob {
gateway_pid: std::process::id(),
new_binary_path: new_binary,
old_binary_path: old_binary,
backup_binary_path: backup_binary,
gateway_args,
health_url: format!("http://127.0.0.1:{gateway_port}/api/gateway"),
};
let job_path = huskies_dir.join("trampoline.json");
crate::trampoline::write_job_atomic(&job, &job_path)?;
let exe = std::env::current_exe()
.map_err(|e| format!("cannot locate current binary for trampoline: {e}"))?;
crate::trampoline::spawn_detached_trampoline(&exe, &job_path)?;
crate::slog!("[gateway-rebuild] Trampoline launched — gateway will be replaced shortly");
Ok(())
}