From 2a7db16a77b09a824531ff3cb754bfac808af2c0 Mon Sep 17 00:00:00 2001 From: Timmy Date: Wed, 15 Jul 2026 14:55:27 +0100 Subject: [PATCH] Fixed exec bug --- docker/entrypoint.sh | 8 ++++++ server/src/rebuild.rs | 64 ++++++++++++++++++------------------------- server/src/upgrade.rs | 28 ++++--------------- 3 files changed, 40 insertions(+), 60 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index c16d87a1..90e3e593 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -75,4 +75,12 @@ if [ -d /workspace/frontend ] && [ -f /workspace/frontend/package.json ]; then cd /workspace fi +# If a rebuilt binary exists (from rebuild_and_restart or upgrade), prefer it +# over the image-baked /usr/local/bin/huskies. +REBUILT="/app/target/release/huskies" +if [ -x "$REBUILT" ] && [ "$1" = "huskies" ]; then + shift + exec "$REBUILT" "$@" +fi + exec "$@" diff --git a/server/src/rebuild.rs b/server/src/rebuild.rs index f4805cea..7730cf92 100644 --- a/server/src/rebuild.rs +++ b/server/src/rebuild.rs @@ -74,17 +74,39 @@ 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`. +pub async fn drain_and_exit(project_root: &Path, label: &str) -> ! { + crate::crdt_state::flush_persistence(std::time::Duration::from_secs(5)).await; + + let port_file = project_root.join(".huskies_port"); + if port_file.exists() { + let _ = std::fs::remove_file(&port_file); + } + let cwd_port_file = std::path::Path::new(".huskies_port"); + if cwd_port_file.exists() { + let _ = std::fs::remove_file(cwd_port_file); + } + + slog!("[{label}] Exiting for Docker restart with new binary"); + std::process::exit(0) +} + // ── Rebuild ────────────────────────────────────────────────────────────── -/// Rebuild the server binary and re-exec. +/// 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 re-execs the process with -/// the new binary via `std::os::unix::process::CommandExt::exec()`. +/// [`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, @@ -204,41 +226,7 @@ pub async fn rebuild_and_restart( n.notify(ShutdownReason::Rebuild).await; } - // 5b. Drain the persistence channel so no queued ops are lost when exec() - // replaces this process. Times out after 5 s with a logged warning - // naming the queue depth so any regression is visible in logs. - crate::crdt_state::flush_persistence(std::time::Duration::from_secs(5)).await; - - // 6. Re-exec with the new binary. - // Use the cargo output path rather than current_exe() so that rebuilds - // inside Docker work correctly — the running binary may be installed at - // /usr/local/bin/huskies (read-only) while cargo writes the new binary - // to /app/target/release/huskies (a writable volume). - let new_exe = if cfg!(debug_assertions) { - workspace_root.join("target/debug/huskies") - } else { - workspace_root.join("target/release/huskies") - }; - let args: Vec = std::env::args().collect(); - - // Remove the port file before re-exec so the new process can write its own. - let port_file = project_root.join(".huskies_port"); - if port_file.exists() { - let _ = std::fs::remove_file(&port_file); - } - // Also check cwd for port file. - let cwd_port_file = std::path::Path::new(".huskies_port"); - if cwd_port_file.exists() { - let _ = std::fs::remove_file(cwd_port_file); - } - - // Use exec() to replace the current process. - // This never returns on success. - use std::os::unix::process::CommandExt; - let err = std::process::Command::new(&new_exe).args(&args[1..]).exec(); - - // If we get here, exec() failed. - Err(format!("Failed to exec new binary: {err}")) + drain_and_exit(project_root, "rebuild").await } // ── Tests ──────────────────────────────────────────────────────────────── diff --git a/server/src/upgrade.rs b/server/src/upgrade.rs index 2c014578..a78aacca 100644 --- a/server/src/upgrade.rs +++ b/server/src/upgrade.rs @@ -60,9 +60,12 @@ pub async fn fetch_and_replace_binary(source_url: &str, target_path: &Path) -> R // ── Full server upgrade (called from the running process) ───────────────── /// Fetch a new binary, atomically replace the current executable, drain CRDT -/// persistence, and re-exec the running server process with its original args. +/// persistence, and exit so Docker restarts the container with the new binary. +/// +/// The entrypoint script checks for a rebuilt binary at +/// /app/target/release/huskies and prefers it over the image-baked +/// /usr/local/bin/huskies. /// -/// This function never returns on success — `exec()` replaces the process. /// On failure it returns `Err(message)` so the caller can report the error /// while keeping the original server running. pub async fn upgrade_and_reexec(source_url: &str, project_root: &Path) -> Result { @@ -70,26 +73,7 @@ pub async fn upgrade_and_reexec(source_url: &str, project_root: &Path) -> Result fetch_and_replace_binary(source_url, &target).await?; - // Drain queued CRDT ops so nothing is lost when exec() replaces the process. - crate::crdt_state::flush_persistence(std::time::Duration::from_secs(5)).await; - - // Clean up the port file so the new process can write a fresh one. - let port_file = project_root.join(".huskies_port"); - if port_file.exists() { - let _ = std::fs::remove_file(&port_file); - } - - let args: Vec = std::env::args().collect(); - slog!("[upgrade] Re-execing with new binary: {}", target.display()); - - use std::os::unix::process::CommandExt; - let err = std::process::Command::new(&target).args(&args[1..]).exec(); - - // exec() only returns on failure. - Err(format!( - "Failed to exec new binary at {}: {err}", - target.display() - )) + crate::rebuild::drain_and_exit(project_root, "upgrade").await } // ── CLI upgrade (no re-exec) ─────────────────────────────────────────────