#!/bin/sh
# Arrancar en una instantánea, para cuando la máquina ya no arranca.
#
# `filmos-volver-atras` deshace una actualización que rompió algo, pero necesita
# que el sistema encienda. Si lo que se rompió impide arrancar, hasta ahora la
# única salida era el pendrive. Esto pone las instantáneas en el menú.
#
# Aquí sale más fácil que en otras máquinas por un detalle del reparto: `/boot`
# es una partición APARTE, así que el kernel y su initrd siempre están, pase lo
# que pase dentro de la raíz. No hay que ir a buscarlos dentro de la instantánea.
#
# Lo que se arranca es de SOLO LECTURA -una instantánea de snapper lo es-, así
# que el sistema sube a medias y a propósito: es un sitio desde el que reparar,
# no un sitio donde trabajar. Desde ahí, `sudo filmos-volver-atras <n>` deja la
# raíz buena en su sitio y un reinicio termina el trabajo.
#
# Regla de oro de un guion de /etc/grub.d: ante cualquier duda, no imprimir nada
# y salir bien. Un fallo aquí no rompe una función: rompe `update-grub`, y con
# él el menú entero.
set -u

CUANTAS="${FILMOS_INSTANTANEAS_EN_EL_MENU:-8}"
SNAPSHOTS="${FILMOS_SNAPSHOTS_DIR:-/.snapshots}"
BOOT="${FILMOS_BOOT_DIR:-/boot}"
# De dónde se heredan los parámetros. Es una variable y no una ruta fija para
# poder probar la herencia con la línea REAL de la estación: lo que decide con
# qué arranca el rescate no puede quedarse sin prueba, porque un error ahí no se
# ve hasta el día en que ya no queda otra cosa que probar.
CMDLINE="${FILMOS_CMDLINE:-/proc/cmdline}"

[ -d "$SNAPSHOTS" ] || exit 0
command -v findmnt >/dev/null 2>&1 || exit 0

# La raíz tiene que ser btrfs y estar montada por nombre de subvolumen: si no,
# esto no aplica y no se inventa nada.
RAIZ_INFO="$(findmnt -no FSTYPE,UUID,OPTIONS / 2>/dev/null)" || exit 0
FSTYPE="$(echo "$RAIZ_INFO" | awk '{print $1}')"
UUID="$(echo "$RAIZ_INFO" | awk '{print $2}')"
OPCIONES="$(echo "$RAIZ_INFO" | awk '{print $3}')"
[ "$FSTYPE" = "btrfs" ] || exit 0
[ -n "$UUID" ] || exit 0
case "$OPCIONES" in *subvol=/@,*|*subvol=/@|*subvol=@,*|*subvol=@) ;; *) exit 0 ;; esac

# Dónde vive el kernel para GRUB. Son dos repartos distintos y la ruta cambia:
#
#   · `/boot` en su propia partición -el de esta estación-: GRUB busca ESA
#     partición y el kernel está en su raíz, `/vmlinuz-…`.
#   · `/boot` dentro del subvolumen raíz: GRUB busca el btrfs y el kernel está
#     en `/@/boot/vmlinuz-…`.
#
# Equivocarse aquí produce un menú que arranca a «file not found», que es la
# peor forma de descubrir un error: cuando ya no queda otra cosa que probar.
BOOT_INFO="$(findmnt -no FSTYPE,UUID --target "$BOOT" 2>/dev/null)" || exit 0
BOOT_FSTYPE="$(echo "$BOOT_INFO" | awk '{print $1}')"
BOOT_UUID="$(echo "$BOOT_INFO" | awk '{print $2}')"
[ -n "$BOOT_UUID" ] || exit 0

if [ "$BOOT_UUID" = "$UUID" ]; then
    PREFIJO="/@/boot"
else
    PREFIJO=""
fi

case "$BOOT_FSTYPE" in
    ext2|ext3|ext4) MODULO="ext2" ;;
    btrfs)          MODULO="btrfs" ;;
    xfs)            MODULO="xfs" ;;
    vfat)           MODULO="fat" ;;
    *)              MODULO="" ;;
esac

# El kernel más nuevo de /boot. Es el mismo que arranca normalmente.
NUCLEO=""
for candidato in $(ls -1 "$BOOT"/vmlinuz-* 2>/dev/null | sort -V -r); do
    version="${candidato#"$BOOT"/vmlinuz-}"
    [ -r "$BOOT/initrd.img-$version" ] || continue
    NUCLEO="$version"
    break
done
[ -n "$NUCLEO" ] || exit 0

# Los mismos parámetros con los que arranca esta máquina, quitando los que
# vamos a poner nosotros. Copiarlos importa: ahí van cosas como `crashkernel`
# o el modo de la consola, y arrancar sin ellas cambia el comportamiento.
#
# Se filtra por TOKEN con awk, no con expresiones regulares. Se intentó con tres
# `sed` y salía `ro` dos veces: `\|` es alternancia de GNU y el sed de BSD la
# toma como un carácter literal, así que el guion se comportaba distinto según
# la máquina. Un token es un token; comparar la palabra entera no tiene
# ambigüedad ni dialecto. Lo encontró la prueba al usar la línea de arranque
# REAL de la estación como fixture en vez de una inventada.
EXTRA="$(awk '{
    fuera = ""
    for (i = 1; i <= NF; i++) {
        t = $i
        if (t ~ /^BOOT_IMAGE=/) continue
        if (t ~ /^root=/)       continue
        if (t ~ /^rootflags=/)  continue
        if (t == "ro" || t == "rw") continue
        fuera = fuera " " t
    }
    print fuera
}' "$CMDLINE" 2>/dev/null)"

# Una escalera, no un solo peldaño. Casi siempre lo que se rompe es el driver o
# el escritorio, y la raíz está perfectamente bien: para eso el primer peldaño
# arranca la raíz DE SIEMPRE sin escritorio, que es lo que mas probabilidades
# tiene de funcionar y no toca nada. Las instantáneas vienen después, para
# cuando lo roto es el contenido de la raíz.
abrir_submenu() {
    [ "$HAY" -eq 0 ] || return 0
    echo "submenu 'filmos — recuperación' {"
    cat <<CONSOLA
    menuentry 'filmos en modo consola (sin escritorio)' {
        load_video
        insmod gzio
        insmod part_gpt
        ${MODULO:+insmod $MODULO}
        search --no-floppy --fs-uuid --set=root $BOOT_UUID
        linux $PREFIJO/vmlinuz-$NUCLEO root=UUID=$UUID ro rootflags=subvol=@$EXTRA systemd.unit=multi-user.target
        initrd $PREFIJO/initrd.img-$NUCLEO
    }
CONSOLA
    HAY=1
}

# Nada se imprime hasta haberlo revisado. `grub-script-check`, que corre en el
# postinst, valida SINTAXIS: el `ro` duplicado que se encontró el 2026-08-28
# habría pasado sin una queja, y un `rootflags=` ausente también. Y este guion
# no se ejecuta una vez: corre en CADA `update-grub`, que dispara sola cualquier
# actualización de kernel, sin nadie delante. Así que la revisión tiene que
# estar aquí dentro, no en el instalador.
BUFFER="$(mktemp)" || exit 0
trap 'rm -f "$BUFFER"' EXIT INT TERM

HAY=0
# De la más nueva a la más vieja: en una emergencia se quiere la de antes de lo
# último que se hizo. Sin `ls -v` ni `tac`, que son de GNU: esto también se
# prueba desde un Mac, y un guion que no se puede probar no se prueba.
{
for carpeta in $(ls -1 "$SNAPSHOTS" 2>/dev/null | grep -E '^[0-9]+$' | sort -n -r); do
    [ -d "$SNAPSHOTS/$carpeta/snapshot" ] || continue

    CUANDO=""
    QUE=""
    if [ -r "$SNAPSHOTS/$carpeta/info.xml" ]; then
        # La etiqueta va entre comillas simples en el menuentry, así que un
        # apóstrofo en la descripción cerraría la comilla y rompería el
        # grub.cfg ENTERO. La descripción la escribe quien hace la
        # instantánea: no es texto de confianza. Se cambia por el apóstrofo
        # tipográfico, que se lee igual y no significa nada para GRUB; la barra
        # invertida se quita porque escapa lo siguiente.
        LIMPIA="s/'/’/g; s/\\\\//g"
        CUANDO="$(sed -n 's:.*<date>\(.*\)</date>.*:\1:p' "$SNAPSHOTS/$carpeta/info.xml" | head -1 | sed "$LIMPIA")"
        QUE="$(sed -n 's:.*<description>\(.*\)</description>.*:\1:p' "$SNAPSHOTS/$carpeta/info.xml" | head -1 | sed "$LIMPIA")"
    fi

    abrir_submenu

    ETIQUETA="instantánea $carpeta"
    [ -n "$CUANDO" ] && ETIQUETA="$ETIQUETA · $CUANDO"
    [ -n "$QUE" ] && ETIQUETA="$ETIQUETA · $QUE"

    cat <<ENTRADA
    menuentry '$ETIQUETA (solo lectura)' {
        load_video
        insmod gzio
        insmod part_gpt
        ${MODULO:+insmod $MODULO}
        search --no-floppy --fs-uuid --set=root $BOOT_UUID
        linux $PREFIJO/vmlinuz-$NUCLEO root=UUID=$UUID ro rootflags=subvol=@/.snapshots/$carpeta/snapshot$EXTRA
        initrd $PREFIJO/initrd.img-$NUCLEO
    }
ENTRADA

    CUANTAS=$((CUANTAS - 1))
    [ "$CUANTAS" -le 0 ] && break
done

# Aunque no haya ni una instantánea, el modo consola vale por sí solo.
abrir_submenu
[ "$HAY" -eq 1 ] && echo "}"
} > "$BUFFER"

[ -s "$BUFFER" ] || exit 0

# La revisión semántica, que es la que `grub-script-check` no hace. Mira las
# líneas `linux`, que son las que deciden qué raíz se monta:
#
#   · exactamente un `root=` y un `rootflags=`: dos se contradicen, cero arranca
#     cualquier cosa;
#   · `rootflags=subvol=@` presente: sin él la máquina no monta la raíz de
#     filmos, y perderlo no rompe el arranque de hoy sino la vuelta atrás del
#     día que haga falta;
#   · un solo `ro` y ningún `rw`: fue un fallo real, y el tipo de cosa que la
#     sintaxis da por buena.
#
# Se comprueba solo lo que componemos NOSOTROS. Se pensó en rechazar cualquier
# parámetro repetido, y se descartó: una línea de arranque ajena con un
# duplicado inofensivo dejaría la máquina sin salida de emergencia por algo que
# no hace daño. Un guardián que se dispara solo no protege, estorba.
#
# Si algo no cuadra no se imprime NADA. Un menú sin la salida de emergencia es
# molesto; un menú con una entrada que monta la raíz equivocada es peor que no
# tener menú.
if ! awk '
    /^[[:space:]]*linux / {
        entradas++
        raiz = 0; flags = 0; solo_ro = 0; escribible = 0
        for (i = 2; i <= NF; i++) {
            t = $i
            if (t ~ /^root=/)      raiz++
            if (t ~ /^rootflags=/) flags++
            if (t == "ro")         solo_ro++
            if (t == "rw")         escribible++
        }
        if (raiz != 1)     { print "no hay exactamente un root=" > "/dev/stderr"; malo = 1 }
        if (flags != 1)    { print "no hay exactamente un rootflags=" > "/dev/stderr"; malo = 1 }
        if (solo_ro != 1)  { print "no hay exactamente un ro" > "/dev/stderr"; malo = 1 }
        if (escribible)    { print "se cuela un rw que pelea con el ro" > "/dev/stderr"; malo = 1 }
        if ($0 !~ /rootflags=subvol=@/) { print "falta subvol=@" > "/dev/stderr"; malo = 1 }
    }
    END { exit (malo || entradas == 0) }
' "$BUFFER"; then
    echo "filmos: el menu de recuperacion no pasa su propia revision; no se añade" >&2
    exit 0
fi

cat "$BUFFER"
exit 0
