Architecture review (all P0+P1 issues now fixed): - Add 10s timeout to 6 bare Nostr client.connect() calls - Pin all 12 crypto deps to exact versions from Cargo.lock - Pin all 15 floating container image tags to exact patch versions - Add CI pipeline (cargo fmt + clippy + tests, frontend type-check + build) Self-update system (git.tx1138.com): - scripts/self-update.sh: pull, build, install, restart with rollback - systemd timer checks daily at 3 AM - update.check RPC does git-based checks when repo is present - update.git-apply RPC triggers self-update from UI - Default update URL changed from GitHub to git.tx1138.com - Git added to ISO package list for fresh installs Documentation: - CHANGELOG v1.3.1 with all changes - README updated (version, update system section) - BETA-PROGRESS session #6 logged - architecture-review.html: 4 issues marked FIXED, 8/12 refactoring done Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
209 lines
8.0 KiB
Rust
209 lines
8.0 KiB
Rust
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<serde_json::Value> {
|
|
// 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<serde_json::Value> {
|
|
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<serde_json::Value> {
|
|
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<serde_json::Value> {
|
|
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<serde_json::Value> {
|
|
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<serde_json::Value> {
|
|
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<serde_json::Value> {
|
|
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<serde_json::Value> {
|
|
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<serde_json::Value> {
|
|
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<serde_json::Value> {
|
|
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 }))
|
|
}
|
|
}
|