boot entry reconcile: converger con la NVRAM en CADA arranque (ADR 0018 §2)
La NVRAM es estado compartido y el vecino la reescribe sin coordinarse. Contra eso no sirve confiar: sirve converger. `reconcile` descubre su propia ESP, repone la entrada y el BootOrder, y es idempotente. Descubre la ESP por TIPO de partición (GUID de ESP en GPT, 0xEF en MBR), que es el único dato fiable sin montar nada — un reconciliador que monta sistemas de ficheros en el arranque es un efecto secundario que no queremos. Si hay VARIAS ESP no adivina: las lista y pide --disk. Elegir mal significa escribir una entrada que apunta a un disco que puede no estar, y eso es peor que no escribir nada. No rompe el arranque por nada: sin firmware EFI (máquina por BIOS) lo dice y sale 0. Y cuando actúa, GRITA — va a /dev/tty0 además del serial, a diferencia del menú de arranque. Un reconciliador que repara en silencio deja al usuario conviviendo con una rareza intermitente que no entiende; el mensaje dice explícitamente que si se repite en cada arranque es que otro sistema operativo le está reescribiendo la NVRAM. Enganchado al wrapper de PID1 de las dos imágenes, con el mismo timeout y el mismo || true que el menú: corre ANTES del exec de arje-zero y colgarse ahí es un arranque muerto e indistinguible de un kernel colgado. El test que vale es el escenario completo: takana se instala, llega el vecino y se pone primero, y el siguiente arranque lo repone — SIN borrar la entrada del vecino (takana se pone primera, no lo echa) — y el arranque siguiente ya no escribe nada. Más el GUID de ESP, que va en orden DE DISCO y no en el legible: escribirlo "como se lee" es el error clásico y no casaría con ninguna ESP real. 15 tests en el módulo, 74/74 del CLI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj
This commit is contained in:
@@ -303,6 +303,96 @@ pub fn read_partition(disk: &Path, number: u32) -> io::Result<HdPartition> {
|
||||
Ok(HdPartition { number, start_lba: start, size_lba: size, signature, mbr_type: 1, sig_type: 1 })
|
||||
}
|
||||
|
||||
/// GUID de tipo «EFI System Partition», tal como se almacena en la tabla GPT: los tres primeros
|
||||
/// campos van en little-endian y los dos últimos crudos. Escribirlo en el orden «legible» es el
|
||||
/// error clásico y no casaría con ninguna ESP del mundo.
|
||||
pub const ESP_TYPE_GUID: [u8; 16] = [
|
||||
0x28, 0x73, 0x2a, 0xc1, 0x1f, 0xf8, 0xd2, 0x11, 0xba, 0x4b, 0x00, 0xa0, 0xc9, 0x3e, 0xc9, 0x3b,
|
||||
];
|
||||
|
||||
/// Discos de bloque reales del sistema, sin los virtuales. Un `loop` o un `ram` con una tabla de
|
||||
/// particiones existe y NO es de donde arrancamos.
|
||||
pub fn block_disks(sys_block: &Path) -> io::Result<Vec<PathBuf>> {
|
||||
let mut v = Vec::new();
|
||||
for ent in std::fs::read_dir(sys_block)? {
|
||||
let name = ent?.file_name().to_string_lossy().to_string();
|
||||
if name.starts_with("loop")
|
||||
|| name.starts_with("ram")
|
||||
|| name.starts_with("zram")
|
||||
|| name.starts_with("dm-")
|
||||
|| name.starts_with("sr")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let dev = PathBuf::from("/dev").join(&name);
|
||||
if dev.exists() {
|
||||
v.push(dev);
|
||||
}
|
||||
}
|
||||
v.sort();
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// Todas las ESP que haya, como `(disco, nº de partición)`.
|
||||
///
|
||||
/// Busca por **tipo de partición**, que es el único dato fiable sin montar nada: GUID de ESP en GPT,
|
||||
/// tipo `0xEF` en MBR. No mira el contenido — para eso habría que montar la FAT, y un reconciliador
|
||||
/// que monta sistemas de ficheros en el arranque es un efecto secundario que no queremos.
|
||||
pub fn find_esps(disks: &[PathBuf]) -> Vec<(PathBuf, u32)> {
|
||||
let mut out = Vec::new();
|
||||
for d in disks {
|
||||
// GPT: hasta 128 entradas es lo habitual; read_partition ya valida el límite real.
|
||||
for n in 1..=128u32 {
|
||||
match read_partition(d, n) {
|
||||
Ok(_) => {
|
||||
if es_esp(d, n).unwrap_or(false) {
|
||||
out.push((d.clone(), n));
|
||||
}
|
||||
}
|
||||
// La primera entrada vacía en MBR no significa que las siguientes lo estén, pero un
|
||||
// error de «no existe» sí corta: read_partition distingue los dos casos.
|
||||
Err(e) if e.kind() == io::ErrorKind::InvalidInput && n > 4 => break,
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// ¿La partición `n` de `disk` es una ESP? Mira el tipo, no el contenido.
|
||||
fn es_esp(disk: &Path, n: u32) -> io::Result<bool> {
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
let mut f = std::fs::File::open(disk)?;
|
||||
let mut hdr = [0u8; 512];
|
||||
f.seek(SeekFrom::Start(512))?;
|
||||
f.read_exact(&mut hdr)?;
|
||||
if &hdr[0..8] == b"EFI PART" {
|
||||
let entry_lba = u64::from_le_bytes(hdr[72..80].try_into().unwrap());
|
||||
let entry_sz = u32::from_le_bytes(hdr[84..88].try_into().unwrap()) as usize;
|
||||
if !(56..=4096).contains(&entry_sz) {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut e = vec![0u8; entry_sz];
|
||||
f.seek(SeekFrom::Start(entry_lba * 512 + (n as u64 - 1) * entry_sz as u64))?;
|
||||
f.read_exact(&mut e)?;
|
||||
return Ok(e[0..16] == ESP_TYPE_GUID);
|
||||
}
|
||||
// MBR: el tipo 0xEF es «EFI System Partition».
|
||||
let mut mbr = [0u8; 512];
|
||||
f.seek(SeekFrom::Start(0))?;
|
||||
f.read_exact(&mut mbr)?;
|
||||
if mbr[510] != 0x55 || mbr[511] != 0xAA || n > 4 {
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(mbr[446 + (n as usize - 1) * 16 + 4] == 0xEF)
|
||||
}
|
||||
|
||||
/// ¿Esta máquina arrancó por UEFI? Sin esto no hay NVRAM que reconciliar, y hay que decirlo en vez
|
||||
/// de fallar con un error de fichero no encontrado que no explica nada.
|
||||
pub fn hay_firmware_efi(efivars: &Path) -> bool {
|
||||
efivars.is_dir() && std::fs::read_dir(efivars).map(|mut d| d.next().is_some()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Busca el primer `Boot####` libre. Empieza en 0 porque es lo que hace todo el mundo y así el
|
||||
/// número queda chico y legible en el menú del firmware.
|
||||
pub fn first_free_slot(root: &Path) -> io::Result<u16> {
|
||||
@@ -335,6 +425,66 @@ pub fn list_entries(root: &Path) -> io::Result<Vec<(u16, LoadOption)>> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Qué tuvo que hacer una reconciliación. El ADR 0018 §2 exige que el reconciliador **reporte
|
||||
/// cuando actúa**: si repara en silencio, el usuario convive con una rareza intermitente —«a veces
|
||||
/// arranca Windows solo»— sin enterarse nunca de que hay un vecino pisando.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Reconciliacion {
|
||||
pub slot: u16,
|
||||
pub entrada_escrita: bool,
|
||||
pub orden_cambiado: bool,
|
||||
pub orden_antes: Vec<u16>,
|
||||
pub orden_despues: Vec<u16>,
|
||||
}
|
||||
|
||||
impl Reconciliacion {
|
||||
/// ¿Hubo que tocar algo? Falso en el caso normal de un sistema que nadie pisó.
|
||||
pub fn actuo(&self) -> bool {
|
||||
self.entrada_escrita || self.orden_cambiado
|
||||
}
|
||||
}
|
||||
|
||||
/// Deja la NVRAM diciendo lo que debe decir: una entrada para `loader` en `part`, y esa entrada
|
||||
/// primera en `BootOrder`. **Idempotente**: si ya está así no escribe nada — importa porque esto
|
||||
/// corre en cada arranque y la NVRAM tiene un número finito de ciclos de escritura.
|
||||
pub fn reconcile(
|
||||
efivars: &Path,
|
||||
part: &HdPartition,
|
||||
loader: &str,
|
||||
label: &str,
|
||||
dry_run: bool,
|
||||
) -> io::Result<Reconciliacion> {
|
||||
let dp = device_path(part, loader);
|
||||
let lo = load_option(label, &dp, &[]);
|
||||
|
||||
let ya = list_entries(efivars)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.find(|(_, e)| e.description == label && e.file_path_list == dp);
|
||||
|
||||
let (slot, entrada_escrita) = match ya {
|
||||
Some((n, _)) => (n, false),
|
||||
None => {
|
||||
let n = first_free_slot(efivars)?;
|
||||
if !dry_run {
|
||||
write_var(&boot_var_path(efivars, n), ATTRS, &lo)?;
|
||||
}
|
||||
(n, true)
|
||||
}
|
||||
};
|
||||
|
||||
let ruta_orden = efivars.join(format!("BootOrder-{GLOBAL_GUID}"));
|
||||
let orden_antes =
|
||||
read_var(&ruta_orden).map(|(_, d)| parse_boot_order(&d)).unwrap_or_default();
|
||||
let orden_despues = promote_first(&orden_antes, slot);
|
||||
let orden_cambiado = orden_despues != orden_antes;
|
||||
if orden_cambiado && !dry_run {
|
||||
write_var(&ruta_orden, ATTRS, &serialize_boot_order(&orden_despues))?;
|
||||
}
|
||||
|
||||
Ok(Reconciliacion { slot, entrada_escrita, orden_cambiado, orden_antes, orden_despues })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -482,6 +632,110 @@ mod tests {
|
||||
std::fs::remove_dir_all(&tmp).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn el_guid_de_esp_esta_en_el_orden_DE_DISCO_y_no_en_el_legible() {
|
||||
// C12A7328-F81F-11D2-BA4B-00A0C93EC93B: los TRES primeros campos van en little-endian y los
|
||||
// dos últimos crudos. Escribirlo "como se lee" es el error clásico y no casaría con ninguna
|
||||
// ESP real — este test es la única forma de que se note acá y no en una máquina.
|
||||
assert_eq!(&ESP_TYPE_GUID[0..4], &[0x28, 0x73, 0x2a, 0xc1], "Data1 va invertido");
|
||||
assert_eq!(&ESP_TYPE_GUID[4..6], &[0x1f, 0xf8], "Data2 va invertido");
|
||||
assert_eq!(&ESP_TYPE_GUID[6..8], &[0xd2, 0x11], "Data3 va invertido");
|
||||
assert_eq!(&ESP_TYPE_GUID[8..16], &[0xba, 0x4b, 0x00, 0xa0, 0xc9, 0x3e, 0xc9, 0x3b], "Data4 crudo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encuentra_la_esp_por_tipo_y_no_se_inventa_otras() {
|
||||
let tmp = std::env::temp_dir().join(format!("takana-efi-esp-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&tmp).unwrap();
|
||||
// Disco con DOS particiones: la 1 es ESP, la 2 no. Sólo debe salir la 1.
|
||||
let mut d = vec![0u8; 512 * 40];
|
||||
d[510] = 0x55;
|
||||
d[511] = 0xAA;
|
||||
let h = 512;
|
||||
d[h..h + 8].copy_from_slice(b"EFI PART");
|
||||
d[h + 72..h + 80].copy_from_slice(&2u64.to_le_bytes());
|
||||
d[h + 80..h + 84].copy_from_slice(&128u32.to_le_bytes());
|
||||
d[h + 84..h + 88].copy_from_slice(&128u32.to_le_bytes());
|
||||
let e1 = 512 * 2;
|
||||
d[e1..e1 + 16].copy_from_slice(&ESP_TYPE_GUID);
|
||||
d[e1 + 16..e1 + 32].copy_from_slice(&[0x5A; 16]);
|
||||
d[e1 + 32..e1 + 40].copy_from_slice(&2048u64.to_le_bytes());
|
||||
d[e1 + 40..e1 + 48].copy_from_slice(&133_119u64.to_le_bytes());
|
||||
let e2 = e1 + 128;
|
||||
d[e2..e2 + 16].copy_from_slice(&[0xBB; 16]); // otro tipo cualquiera
|
||||
d[e2 + 16..e2 + 32].copy_from_slice(&[0x6B; 16]);
|
||||
d[e2 + 32..e2 + 40].copy_from_slice(&133_120u64.to_le_bytes());
|
||||
d[e2 + 40..e2 + 48].copy_from_slice(&264_191u64.to_le_bytes());
|
||||
let f = tmp.join("dos.img");
|
||||
std::fs::write(&f, &d).unwrap();
|
||||
let esps = find_esps(std::slice::from_ref(&f));
|
||||
assert_eq!(esps.len(), 1, "hay UNA sola ESP, no dos: {esps:?}");
|
||||
assert_eq!(esps[0].1, 1, "y es la partición 1");
|
||||
std::fs::remove_dir_all(&tmp).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sin_firmware_efi_lo_dice_en_vez_de_fallar_raro() {
|
||||
let tmp = std::env::temp_dir().join(format!("takana-efi-novars-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&tmp).unwrap();
|
||||
assert!(!hay_firmware_efi(&tmp), "un directorio VACÍO no es un firmware EFI");
|
||||
assert!(!hay_firmware_efi(&tmp.join("no-existe")), "y uno que no existe, tampoco");
|
||||
std::fs::write(tmp.join("algo"), b"x").unwrap();
|
||||
assert!(hay_firmware_efi(&tmp), "con al menos una variable, sí");
|
||||
std::fs::remove_dir_all(&tmp).ok();
|
||||
}
|
||||
|
||||
/// El escenario que motiva el ADR entero: otro sistema operativo se instala, crea su entrada y
|
||||
/// se pone PRIMERO. El reconciliador debe reponer el orden — y **no** borrar al vecino.
|
||||
#[test]
|
||||
fn el_vecino_se_pone_primero_y_el_reconciliador_lo_repone_sin_borrarlo() {
|
||||
let tmp = std::env::temp_dir().join(format!("takana-efi-vecino-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&tmp).unwrap();
|
||||
let p = parte();
|
||||
|
||||
// 1) takana se instala.
|
||||
let r1 = reconcile(&tmp, &p, "/EFI/takana/takanax64.efi", "takana", false).unwrap();
|
||||
assert!(r1.entrada_escrita, "la primera vez hay que escribirla");
|
||||
assert_eq!(r1.orden_despues, vec![r1.slot]);
|
||||
|
||||
// 2) llega el vecino: crea su entrada y se pone primero en BootOrder.
|
||||
let vecino = load_option("Windows Boot Manager", &end_node(), &[]);
|
||||
let slot_vecino = first_free_slot(&tmp).unwrap();
|
||||
write_var(&boot_var_path(&tmp, slot_vecino), ATTRS, &vecino).unwrap();
|
||||
let ruta_orden = tmp.join(format!("BootOrder-{GLOBAL_GUID}"));
|
||||
write_var(&ruta_orden, ATTRS, &serialize_boot_order(&[slot_vecino, r1.slot])).unwrap();
|
||||
|
||||
// 3) siguiente arranque de takana.
|
||||
let r2 = reconcile(&tmp, &p, "/EFI/takana/takanax64.efi", "takana", false).unwrap();
|
||||
assert!(r2.actuo(), "tiene que DARSE CUENTA de que lo movieron");
|
||||
assert!(!r2.entrada_escrita, "la entrada seguía ahí: sólo cambió el orden");
|
||||
assert!(r2.orden_cambiado);
|
||||
assert_eq!(r2.orden_despues[0], r1.slot, "takana vuelve a ser la primera");
|
||||
assert!(
|
||||
r2.orden_despues.contains(&slot_vecino),
|
||||
"y el vecino SIGUE en la lista: takana se pone primera, no lo borra"
|
||||
);
|
||||
assert!(
|
||||
boot_var_path(&tmp, slot_vecino).exists(),
|
||||
"la entrada del vecino no se toca nunca"
|
||||
);
|
||||
|
||||
// 4) y el arranque siguiente, con todo en su sitio, no hace nada.
|
||||
let r3 = reconcile(&tmp, &p, "/EFI/takana/takanax64.efi", "takana", false).unwrap();
|
||||
assert!(!r3.actuo(), "sin cambios no se escribe: la NVRAM tiene ciclos finitos");
|
||||
std::fs::remove_dir_all(&tmp).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dry_run_no_escribe_ni_un_byte() {
|
||||
let tmp = std::env::temp_dir().join(format!("takana-efi-dry-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&tmp).unwrap();
|
||||
let r = reconcile(&tmp, &parte(), "/EFI/takana/takanax64.efi", "takana", true).unwrap();
|
||||
assert!(r.actuo(), "reporta lo que HARÍA");
|
||||
assert_eq!(std::fs::read_dir(&tmp).unwrap().count(), 0, "y no dejó nada en el directorio");
|
||||
std::fs::remove_dir_all(&tmp).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boot_order_ida_y_vuelta() {
|
||||
let o = vec![0x0001u16, 0x0000, 0x2001];
|
||||
|
||||
@@ -623,6 +623,35 @@ enum EntryCmd {
|
||||
#[arg(long, default_value = crate::efi_boot::EFIVARS)]
|
||||
efivars: PathBuf,
|
||||
},
|
||||
/// Reconcile this system's boot entry against the firmware, finding the ESP by itself.
|
||||
///
|
||||
/// Pensado para correr **en cada arranque** (ADR 0018 §2): la NVRAM es estado compartido que
|
||||
/// otro sistema operativo reescribe sin avisar, y contra eso no sirve confiar, sirve converger.
|
||||
/// No falla el arranque por nada: si la máquina no arrancó por UEFI, o no hay una ESP clara, lo
|
||||
/// DICE y sale limpio.
|
||||
Reconcile {
|
||||
/// Path of the loader inside the ESP.
|
||||
#[arg(long, default_value = "/EFI/takana/takanax64.efi")]
|
||||
loader: String,
|
||||
/// Label shown in the firmware boot menu.
|
||||
#[arg(long, default_value = "takana")]
|
||||
label: String,
|
||||
/// Root of efivarfs.
|
||||
#[arg(long, default_value = crate::efi_boot::EFIVARS)]
|
||||
efivars: PathBuf,
|
||||
/// Where to enumerate block devices from.
|
||||
#[arg(long, default_value = "/sys/block")]
|
||||
sys_block: PathBuf,
|
||||
/// Disk to use instead of auto-detecting (needed when several ESPs exist).
|
||||
#[arg(long)]
|
||||
disk: Option<PathBuf>,
|
||||
/// 1-based partition number, only with --disk.
|
||||
#[arg(long, default_value_t = 1)]
|
||||
partition: u32,
|
||||
/// Report what it would do and touch nothing.
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
},
|
||||
/// Create (or refresh) takana's boot entry and put it first in `BootOrder`.
|
||||
///
|
||||
/// Idempotente: si ya existe una entrada con la misma etiqueta Y el mismo device path, no
|
||||
@@ -1542,6 +1571,73 @@ fn main() -> anyhow::Result<()> {
|
||||
nuevo.iter().map(|n| format!("{n:04X}")).collect::<Vec<_>>().join(","));
|
||||
}
|
||||
}
|
||||
EntryCmd::Reconcile { loader, label, efivars, sys_block, disk, partition, dry_run } => {
|
||||
// 1) ¿Hay firmware EFI? Sin NVRAM no hay nada que reconciliar, y eso NO es
|
||||
// un error: puede ser una máquina que arranca por BIOS. Salir limpio.
|
||||
if !efi_boot::hay_firmware_efi(&efivars) {
|
||||
println!("boot entry reconcile: sin firmware EFI en {} — nada que hacer", efivars.display());
|
||||
println!(" (esta máquina no arrancó por UEFI; el arranque sigue normal)");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 2) ¿Cuál es nuestra ESP? Se busca por TIPO de partición. Si hay varias no
|
||||
// se adivina: elegir mal la ESP significa escribir una entrada que apunta
|
||||
// a un disco que puede no estar, y eso es peor que no escribir nada.
|
||||
let parte = match disk {
|
||||
Some(d) => efi_boot::read_partition(&d, partition)?,
|
||||
None => {
|
||||
let discos = efi_boot::block_disks(&sys_block).unwrap_or_default();
|
||||
let esps = efi_boot::find_esps(&discos);
|
||||
match esps.len() {
|
||||
1 => {
|
||||
let (d, n) = &esps[0];
|
||||
println!("boot entry reconcile: ESP detectada en {} p{n}", d.display());
|
||||
efi_boot::read_partition(d, *n)?
|
||||
}
|
||||
0 => {
|
||||
println!("boot entry reconcile: arrancamos por UEFI pero NO encuentro ninguna ESP");
|
||||
println!(" discos mirados: {}", if discos.is_empty() {
|
||||
"(ninguno)".to_string()
|
||||
} else {
|
||||
discos.iter().map(|d| d.display().to_string()).collect::<Vec<_>>().join(" ")
|
||||
});
|
||||
println!(" → pasá --disk/--partition. No se tocó la NVRAM.");
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
println!("boot entry reconcile: hay {} ESP y NO voy a adivinar:", esps.len());
|
||||
for (d, n) in &esps {
|
||||
println!(" {} p{n}", d.display());
|
||||
}
|
||||
println!(" → elegí con --disk/--partition. No se tocó la NVRAM.");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 3) Converger.
|
||||
let r = efi_boot::reconcile(&efivars, &parte, &loader, &label, dry_run)?;
|
||||
let fmt = |o: &Vec<u16>| if o.is_empty() {
|
||||
"(vacío)".to_string()
|
||||
} else {
|
||||
o.iter().map(|n| format!("{n:04X}")).collect::<Vec<_>>().join(",")
|
||||
};
|
||||
if !r.actuo() {
|
||||
println!("✓ arranque en orden: Boot{:04X} «{label}» ya es la primera", r.slot);
|
||||
} else {
|
||||
// Que se VEA. Si repara en silencio, el usuario nunca se entera de que
|
||||
// algo le está pisando el arranque (ADR 0018 §2).
|
||||
println!("⚠ EL ARRANQUE ESTABA CAMBIADO — takana lo repuso{}:", if dry_run { " (--dry-run: NO)" } else { "" });
|
||||
if r.entrada_escrita {
|
||||
println!(" · faltaba la entrada «{label}» ⇒ escrita en Boot{:04X}", r.slot);
|
||||
}
|
||||
if r.orden_cambiado {
|
||||
println!(" · BootOrder: {} → {}", fmt(&r.orden_antes), fmt(&r.orden_despues));
|
||||
}
|
||||
println!(" Si esto se repite en cada arranque, otro sistema operativo te está reescribiendo la NVRAM.");
|
||||
}
|
||||
}
|
||||
},
|
||||
BootCmd::Menu { compositor, out, select } => {
|
||||
// 1) Publicar el grafo (siempre, aunque no haya compositor: otros lo leen).
|
||||
|
||||
@@ -193,6 +193,21 @@ done
|
||||
if [ -x /usr/bin/hammer ]; then
|
||||
/bin/busybox timeout 15 /usr/bin/hammer boot menu > /dev/ttyS0 2>&1 || true
|
||||
fi
|
||||
|
||||
# Reconciliador del arranque (ADR 0018 §2). La NVRAM del firmware es estado compartido: otro sistema
|
||||
# operativo instalado al lado la reescribe sin coordinarse —Windows Update poniéndose primero en
|
||||
# BootOrder es el caso típico—. Contra eso no sirve confiar, sirve CONVERGER en cada arranque.
|
||||
#
|
||||
# Va al SERIAL **y a /dev/tty0**, a diferencia del menú de arriba: si takana tuvo que reponer su
|
||||
# entrada, el usuario tiene que poder enterarse mirando la pantalla. Un reconciliador que repara en
|
||||
# silencio deja al usuario conviviendo con una rareza intermitente que no entiende (§2).
|
||||
#
|
||||
# Mismo `timeout` y mismo `|| true` que arriba, por la misma razón: esto corre ANTES del exec de
|
||||
# arje-zero y colgarse acá es un arranque muerto.
|
||||
if [ -x /usr/bin/hammer ]; then
|
||||
/bin/busybox timeout 15 /usr/bin/hammer boot entry reconcile 2>&1 \
|
||||
| /bin/busybox tee /dev/tty0 > /dev/ttyS0 2>/dev/null || true
|
||||
fi
|
||||
exec /usr/bin/arje-zero
|
||||
RINIT
|
||||
chmod +x "$ROOT_TREE/sbin/init"
|
||||
|
||||
@@ -452,6 +452,11 @@ echo "HAMMER-EFI-INIT-OK: arje-zero PID1" > /dev/ttyS0 2>/dev/null || true
|
||||
# Menú de arranque por grafo (ADR 0010): emite /run/hammer/boot-graph.json y, si hay compositor (mirada),
|
||||
# lo pinta sobre KMS. Graceful (sin compositor sigue) y al serial (no pinta tty0 ⇒ cero-parpadeo).
|
||||
[ -x /usr/bin/hammer ] && /usr/bin/hammer boot menu > /dev/ttyS0 2>&1 || true
|
||||
# Reconciliador del arranque (ADR 0018 §2): la NVRAM es estado compartido y el vecino la reescribe
|
||||
# sin avisar. Converger en cada arranque, y DECIRLO en pantalla cuando hubo que actuar — si repara
|
||||
# en silencio, el usuario nunca se entera de que algo le está pisando el arranque.
|
||||
[ -x /usr/bin/hammer ] && /bin/busybox timeout 15 /usr/bin/hammer boot entry reconcile 2>&1 \
|
||||
| /bin/busybox tee /dev/tty0 > /dev/ttyS0 2>/dev/null || true
|
||||
exec /usr/bin/arje-zero
|
||||
INIT
|
||||
"$BB" chmod +x "$MNT/sbin/init"
|
||||
|
||||
Reference in New Issue
Block a user