Remove all alternate update paths — fleet redeploy is release + upgrade all
Killed: - rebuild_and_restart (in-container cargo self-compile): the MCP tool, the `rebuild` chat command in all four transports, the web-ui bot command, and the underlying function. This was the path that caused the exec() deadlocks. - upgrade_sled gateway MCP tool: second entry point to sled upgrades, defaulted to serving the gateway's own macOS binary to Linux sleds. - GET /api/huskies-binary (both sled and gateway route trees): served current_exe(), wrong platform when the gateway is macOS. Superseded by /api/artifacts/ which now also serves on the gateway route tree. - `huskies upgrade` CLI subcommand and --source flag: third way of doing the same download-and-replace. Escape hatch for a bricked sled is `docker cp` + restart. Kept, distinct jobs: `project-rebuild` (container/image updates), `rebuild gateway` + script/local-release (gateway self-update). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019fHdm92yjvguPi2LiXfLB9
This commit is contained in:
+3
-138
@@ -1,6 +1,5 @@
|
||||
//! Server rebuild and restart logic shared between the MCP tool and Matrix bot command.
|
||||
//! Shutdown notification and drain-and-exit sequence for server restarts.
|
||||
|
||||
use crate::agents::AgentPool;
|
||||
use crate::chat::ChatTransport;
|
||||
use crate::slog;
|
||||
use std::path::Path;
|
||||
@@ -77,8 +76,8 @@ impl BotShutdownNotifier {
|
||||
// ── Shared shutdown sequence ─────────────────────────────────────────────
|
||||
|
||||
/// Flush CRDT persistence, remove port files, and exit so Docker restarts
|
||||
/// the container with the new binary. Used by both `rebuild_and_restart`
|
||||
/// and `upgrade_and_reexec`.
|
||||
/// the container with the new binary. Called by `upgrade_and_reexec` after
|
||||
/// the binary has been replaced on disk.
|
||||
pub async fn drain_and_exit(project_root: &Path, label: &str) -> ! {
|
||||
crate::crdt_state::flush_persistence(std::time::Duration::from_secs(5)).await;
|
||||
|
||||
@@ -95,140 +94,6 @@ pub async fn drain_and_exit(project_root: &Path, label: &str) -> ! {
|
||||
std::process::exit(0)
|
||||
}
|
||||
|
||||
// ── Rebuild ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Rebuild the server binary and exit for Docker restart.
|
||||
///
|
||||
/// 1. Gracefully stops all running agents (kills PTY children).
|
||||
/// 2. Runs `cargo build [-p huskies]` from the workspace root, matching
|
||||
/// the current build profile (debug or release).
|
||||
/// 3. If the build fails, returns the build error (server stays up).
|
||||
/// 4. If the build succeeds, sends a best-effort shutdown notification (if a
|
||||
/// [`BotShutdownNotifier`] is provided), then calls [`drain_and_exit`] to
|
||||
/// flush persistence and exit. Docker's restart policy brings the
|
||||
/// container back up with the new binary.
|
||||
pub async fn rebuild_and_restart(
|
||||
agents: &AgentPool,
|
||||
project_root: &Path,
|
||||
notifier: Option<&BotShutdownNotifier>,
|
||||
) -> Result<String, String> {
|
||||
slog!("[rebuild] Rebuild and restart requested");
|
||||
|
||||
// 1. Gracefully stop all running agents.
|
||||
let running_count = agents
|
||||
.list_agents()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter(|a| a.status == crate::agents::AgentStatus::Running)
|
||||
.count();
|
||||
if running_count > 0 {
|
||||
slog!("[rebuild] Stopping {running_count} running agent(s) before rebuild");
|
||||
}
|
||||
agents.kill_all_children().await;
|
||||
|
||||
// 2. Find the workspace root (parent of the server binary's source).
|
||||
// CARGO_MANIFEST_DIR at compile time points to the `server/` crate;
|
||||
// the workspace root is its parent. When running inside Docker the
|
||||
// compile-time path (/app) no longer exists — the source is bind-mounted
|
||||
// at the project_root instead, so fall back to that.
|
||||
let compile_time_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.filter(|p| p.join("Cargo.toml").exists());
|
||||
let workspace_root = match compile_time_root {
|
||||
Some(p) => p.to_path_buf(),
|
||||
None => {
|
||||
if project_root.join("Cargo.toml").exists() {
|
||||
project_root.to_path_buf()
|
||||
} else {
|
||||
return Err(
|
||||
"Cannot determine workspace root: neither CARGO_MANIFEST_DIR nor project_root contain Cargo.toml".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
let workspace_root = workspace_root.as_path();
|
||||
|
||||
slog!(
|
||||
"[rebuild] Building server from workspace root: {}",
|
||||
workspace_root.display()
|
||||
);
|
||||
|
||||
// 3. 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() {
|
||||
slog!("[rebuild] Building frontend");
|
||||
let fe_output = tokio::task::spawn_blocking({
|
||||
let frontend_dir = frontend_dir.clone();
|
||||
move || {
|
||||
std::process::Command::new("npm")
|
||||
.args(["run", "build"])
|
||||
.current_dir(&frontend_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);
|
||||
slog!("[rebuild] Frontend build failed:\n{stderr}");
|
||||
return Err(format!("Frontend build failed:\n{stderr}"));
|
||||
}
|
||||
slog!("[rebuild] Frontend build succeeded");
|
||||
}
|
||||
|
||||
// 4. Build the server binary, matching the current build profile so the
|
||||
// re-exec via current_exe() picks up the new binary.
|
||||
let build_args: Vec<&str> = if cfg!(debug_assertions) {
|
||||
vec!["build", "-p", "huskies"]
|
||||
} else {
|
||||
vec!["build", "--release", "-p", "huskies"]
|
||||
};
|
||||
slog!("[rebuild] cargo {}", build_args.join(" "));
|
||||
let output = tokio::task::spawn_blocking({
|
||||
let workspace_root = workspace_root.to_path_buf();
|
||||
move || {
|
||||
std::process::Command::new("cargo")
|
||||
.args(&build_args)
|
||||
.current_dir(&workspace_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);
|
||||
slog!("[rebuild] Build failed:\n{stderr}");
|
||||
return Err(format!("Build failed:\n{stderr}"));
|
||||
}
|
||||
|
||||
// Write the current git HEAD to a file so we can verify which commit is running.
|
||||
if let Ok(head) = std::process::Command::new("git")
|
||||
.args(["rev-parse", "--short", "HEAD"])
|
||||
.current_dir(workspace_root)
|
||||
.output()
|
||||
{
|
||||
let hash = String::from_utf8_lossy(&head.stdout).trim().to_string();
|
||||
let _ = std::fs::write(workspace_root.join(".huskies/build_hash"), &hash);
|
||||
slog!("[rebuild] Build succeeded (commit {hash}), re-execing with new binary");
|
||||
} else {
|
||||
slog!("[rebuild] Build succeeded, re-execing with new binary");
|
||||
}
|
||||
|
||||
// 5. Send shutdown notification before replacing the process so that chat
|
||||
// participants know the bot is going offline. Best-effort only — we
|
||||
// do not abort the rebuild if the send fails.
|
||||
if let Some(n) = notifier {
|
||||
n.notify(ShutdownReason::Rebuild).await;
|
||||
}
|
||||
|
||||
drain_and_exit(project_root, "rebuild").await
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user