use super::RpcHandler; use crate::update; use anyhow::{Context, Result}; impl RpcHandler { /// Check for available system updates. /// Tries git-based check first (if repo exists), falls back to manifest-based. pub(super) async fn handle_update_check(&self) -> Result { // Try git-based check first (preferred for beta nodes) let repo_dir = std::path::PathBuf::from( std::env::var("HOME").unwrap_or_else(|_| "/home/archipelago".to_string()), ) .join("archy"); if repo_dir.join(".git").exists() { if let Ok(git_status) = self.git_check_update(&repo_dir).await { return Ok(git_status); } } // Fall back to manifest-based check let state = update::check_for_updates(&self.config.data_dir).await?; let update_info = state.available_update.as_ref().map(|u| { serde_json::json!({ "version": u.version, "release_date": u.release_date, "changelog": u.changelog, "components": u.components.len(), }) }); Ok(serde_json::json!({ "current_version": state.current_version, "last_check": state.last_check, "update_available": update_info.is_some(), "update": update_info, })) } /// Git-based update check: runs `git fetch` and compares HEAD to origin/main. async fn git_check_update(&self, repo_dir: &std::path::Path) -> Result { let repo_str = repo_dir.to_string_lossy().to_string(); // git fetch origin main let fetch = tokio::process::Command::new("git") .args(["fetch", "origin", "main", "--quiet"]) .current_dir(&repo_str) .output() .await .context("git fetch failed")?; if !fetch.status.success() { anyhow::bail!("git fetch failed: {}", String::from_utf8_lossy(&fetch.stderr)); } // Get local and remote HEADs let local = tokio::process::Command::new("git") .args(["rev-parse", "--short", "HEAD"]) .current_dir(&repo_str) .output() .await?; let local_hash = String::from_utf8_lossy(&local.stdout).trim().to_string(); let remote = tokio::process::Command::new("git") .args(["rev-parse", "--short", "origin/main"]) .current_dir(&repo_str) .output() .await?; let remote_hash = String::from_utf8_lossy(&remote.stdout).trim().to_string(); let update_available = local_hash != remote_hash; // Get commit count and changelog if update available let mut changelog = Vec::new(); let mut commits_behind = 0u64; if update_available { let count = tokio::process::Command::new("git") .args(["rev-list", "HEAD..origin/main", "--count"]) .current_dir(&repo_str) .output() .await?; commits_behind = String::from_utf8_lossy(&count.stdout) .trim() .parse() .unwrap_or(0); let log = tokio::process::Command::new("git") .args(["log", "HEAD..origin/main", "--oneline", "--no-merges", "-20"]) .current_dir(&repo_str) .output() .await?; changelog = String::from_utf8_lossy(&log.stdout) .lines() .map(|l| l.to_string()) .collect(); } let now = chrono::Utc::now().to_rfc3339(); Ok(serde_json::json!({ "current_version": local_hash, "last_check": now, "update_available": update_available, "update_method": "git", "update": if update_available { Some(serde_json::json!({ "version": remote_hash, "commits_behind": commits_behind, "changelog": changelog, })) } else { None }, })) } /// Apply git-based update: runs self-update.sh which pulls, builds, and restarts. pub(super) async fn handle_update_git_apply(&self) -> Result { let script = std::path::PathBuf::from( std::env::var("HOME").unwrap_or_else(|_| "/home/archipelago".to_string()), ) .join("archy/scripts/self-update.sh"); if !script.exists() { anyhow::bail!("self-update.sh not found at {}", script.display()); } // Spawn the update script in the background (it will restart the service) let child = tokio::process::Command::new("bash") .arg(&script) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .spawn() .context("Failed to spawn self-update.sh")?; tracing::info!(pid = child.id(), "Self-update script spawned"); Ok(serde_json::json!({ "started": true, "message": "Update started. The service will restart when complete.", })) } /// Get update status without checking remote. pub(super) async fn handle_update_status(&self) -> Result { let state = update::get_status(&self.config.data_dir).await?; Ok(serde_json::json!({ "current_version": state.current_version, "last_check": state.last_check, "update_available": state.available_update.is_some(), "update_in_progress": state.update_in_progress, "rollback_available": state.rollback_available, })) } /// Dismiss the update notification. pub(super) async fn handle_update_dismiss(&self) -> Result { update::dismiss_update(&self.config.data_dir).await?; Ok(serde_json::json!({ "ok": true })) } /// Download the available update to staging. pub(super) async fn handle_update_download(&self) -> Result { let progress = update::download_update(&self.config.data_dir).await?; Ok(serde_json::json!({ "total_bytes": progress.total_bytes, "downloaded_bytes": progress.downloaded_bytes, "components_downloaded": progress.components_downloaded, })) } /// Apply the staged update. pub(super) async fn handle_update_apply(&self) -> Result { update::apply_update(&self.config.data_dir).await?; Ok(serde_json::json!({ "applied": true, "restart_required": true })) } /// Rollback to the previous version. pub(super) async fn handle_update_rollback(&self) -> Result { update::rollback_update(&self.config.data_dir).await?; Ok(serde_json::json!({ "rolled_back": true, "restart_required": true })) } /// Get the current update schedule. pub(super) async fn handle_update_get_schedule(&self) -> Result { let schedule = update::get_schedule(&self.config.data_dir).await?; Ok(serde_json::json!({ "schedule": schedule })) } /// Set the update schedule. Params: { schedule: "manual" | "daily_check" | "auto_apply" } pub(super) async fn handle_update_set_schedule( &self, params: &serde_json::Value, ) -> Result { let schedule_str = params["schedule"] .as_str() .ok_or_else(|| anyhow::anyhow!("Missing 'schedule' parameter"))?; let schedule = match schedule_str { "manual" => update::UpdateSchedule::Manual, "daily_check" => update::UpdateSchedule::DailyCheck, "auto_apply" => update::UpdateSchedule::AutoApply, _ => anyhow::bail!("Invalid schedule: '{}'. Use manual, daily_check, or auto_apply", schedule_str), }; update::set_schedule(&self.config.data_dir, schedule).await?; Ok(serde_json::json!({ "schedule": schedule })) } }