Fixed exec bug
This commit is contained in:
@@ -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 "$@"
|
||||
|
||||
+26
-38
@@ -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<String> = 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 ────────────────────────────────────────────────────────────────
|
||||
|
||||
+6
-22
@@ -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<String, String> {
|
||||
@@ -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<String> = 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) ─────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user