//! Server rebuild and restart logic shared between the MCP tool and Matrix bot command. use crate::agents::AgentPool; use crate::slog; use std::path::Path; /// Rebuild the server binary and re-exec. /// /// 1. Gracefully stops all running agents (kills PTY children). /// 2. Runs `cargo build [-p storkit]` 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, re-execs the process with the new binary via /// `std::os::unix::process::CommandExt::exec()`. pub async fn rebuild_and_restart(agents: &AgentPool, project_root: &Path) -> Result { slog!("[rebuild] Rebuild and restart requested"); // 1. Gracefully stop all running agents. let running_count = agents .list_agents() .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(); // 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. let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); let workspace_root = manifest_dir .parent() .ok_or_else(|| "Cannot determine workspace root from CARGO_MANIFEST_DIR".to_string())?; slog!( "[rebuild] Building server from workspace root: {}", workspace_root.display() ); // 3. 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", "storkit"] } else { vec!["build", "--release", "-p", "storkit"] }; 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}")); } slog!("[rebuild] Build succeeded, re-execing with new binary"); // 4. Re-exec with the new binary. // Collect current argv so we preserve any CLI arguments (e.g. project path). let current_exe = std::env::current_exe().map_err(|e| format!("Cannot determine current executable: {e}"))?; 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(".storkit_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(".storkit_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(¤t_exe) .args(&args[1..]) .exec(); // If we get here, exec() failed. Err(format!("Failed to exec new binary: {err}")) }