//! `hammer-recover` — auto-recuperación de upgrades al **arranque** (E4 / refinamiento #4b). //! //! El producto NO lleva el CLI `takana` completo; este mini-binario (estático musl) es lo único que el //! sistema instalado necesita para auto-sanar un upgrade interrumpido. El wrapper `/sbin/init` lo corre //! **tras montar `/store` y `/var/lib/hammer`** y **antes** de incarnar arje-zero: //! //! - Sin intento pendiente ⇒ no-op (el caso normal). //! - Con un `pending.json` (un `takana upgrade apply` cortado por un apagón/reinicio): intenta //! **completar** (roll-forward, re-ejecuta el plan idempotente + commitea); si no puede (p.ej. el árbol //! ya no está en `/store`), **deshace** (roll-back) para dejar el FHS consistente. En cualquier caso el //! sistema arranca en un estado coherente, no a medias. //! //! Encaja con el modelo de generaciones **in-place** (el FHS es la generación viva): no hay menú de //! generaciones tipo NixOS que seleccionar, sino una reparación determinista del estado a medias. //! //! Paths del producto (overridables por entorno para tests): //! `TAKANA_RECOVER_ROOT=/` `TAKANA_RECOVER_STORE=/store` //! `TAKANA_RECOVER_STATE=/var/lib/hammer/upgrades` `TAKANA_RECOVER_JOURNAL=/var/lib/hammer/journal` //! Flag: `--rollback` fuerza deshacer en vez de completar. //! //! NUNCA sale con código ≠0 por un fallo de recuperación: es un hook de arranque, no debe abortar el //! boot. Reporta por consola y deja que arje-zero siga (con el estado más consistente que logró). use std::path::PathBuf; /// Lee la variable aceptando el nombre nuevo y, si falta, el viejo (ADR 0016). /// /// La caída va inline y no por `takana_core::env`: este es un mini-binario que se copia a /// `/usr/sbin` de los sistemas instalados, y no vale la pena arrastrarle una dep entera por /// dos líneas. Se retira junto con el resto de las caídas en la etapa 6. fn env_path(key: &str, default: &str) -> PathBuf { let viejo = key.strip_prefix("TAKANA_").map(|s| format!("HAMMER_{s}")); std::env::var(key) .ok() .or_else(|| viejo.and_then(|v| std::env::var(v).ok())) .map(PathBuf::from) .unwrap_or_else(|| PathBuf::from(default)) } fn main() { let force_rollback = std::env::args().skip(1).any(|a| a == "--rollback"); let root = env_path("TAKANA_RECOVER_ROOT", "/"); let store = env_path("TAKANA_RECOVER_STORE", "/store"); let state = env_path("TAKANA_RECOVER_STATE", "/var/lib/hammer/upgrades"); let journal_dir = env_path("TAKANA_RECOVER_JOURNAL", "/var/lib/hammer/journal"); match takana_upgrade::pending(&state) { Ok(None) => { println!("hammer-recover: sin upgrade interrumpido"); return; } Ok(Some(p)) => { println!("hammer-recover: upgrade interrumpido (generación {}) — recuperando", p.id); } Err(e) => { eprintln!("hammer-recover: no pude leer el estado de upgrades: {e}"); return; } } let journal = takana_journal::Journal::open(&journal_dir).ok(); let j = journal.as_ref(); if force_rollback { match takana_upgrade::recover(&store, &root, &state, j, true) { Ok(r) => println!("hammer-recover: DESHECHO (generación {} descartada)", r.generation), Err(e) => eprintln!("hammer-recover: rollback falló: {e}"), } return; } // Optimista: completar el upgrade. Si el roll-forward falla, deshacer ⇒ FHS consistente igual. match takana_upgrade::recover(&store, &root, &state, j, false) { Ok(r) => println!("hammer-recover: COMPLETADO (generación {})", r.generation), Err(e) => { eprintln!("hammer-recover: roll-forward falló ({e}); deshaciendo para dejar el FHS consistente"); match takana_upgrade::recover(&store, &root, &state, j, true) { Ok(r) => println!("hammer-recover: DESHECHO tras fallo (generación {})", r.generation), Err(e2) => eprintln!("hammer-recover: rollback de respaldo también falló: {e2}"), } } } }