huskies: merge 1144 story Gateway trampoline-restart: detached helper survives the gateway's own death
This commit is contained in:
@@ -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};
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user