Files
hammer/scripts/cosmic/metal-desktop-image.sh
T
sergioandClaude Opus 5 213ebfbb03 📶 metal: el enlace WiFi se caía solo y cortaba la depuración remota a mitad
Depurando la OptiPlex por SSH sobre un hotspot, la máquina desaparecía de la red cada
pocos minutos. No es «la WiFi anda mal»: son dos causas que se suman y ninguna deja
rastro en dmesg.

  - `udhcpc -q` (el que usa wifi-up) pide la dirección UNA vez y termina. Nadie renueva
    el lease.
  - el AP olvida a los clientes ociosos y deja de contestar ARP. Desde fuera se ve «No
    route to host» mientras la máquina se cree perfectamente conectada.

El síntoma engaña justo en la dirección peor: la máquina no reporta nada, así que parece
que se colgó lo que estabas depurando.

`wifi-keep` hace ping al gateway cada 10s, que resuelve las dos a la vez — detecta la
caída para reasociar Y mantiene viva la entrada del cliente en la tabla del AP.

Sin esto cada sesión remota se interrumpe sola y hay que volver físicamente al teclado,
que es exactamente lo que la red venía a evitar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 11:33:30 -04:00

443 lines
26 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# metal-desktop-image.sh — imagen de disco EFI del escritorio COSMIC para METAL REAL, con
# **aceleración de hardware Intel (iris)**. Hermano de scripts/kde/metal-desktop-image-dual.sh, pero
# con una diferencia de fondo que es el motivo de existir.
#
# ── POR QUÉ ESTA IMAGEN ES HW-ACCEL Y LA DE KDE ES SOFTWARE ─────────────────────────────────────
# La de KDE va por `mesa-llvmpipe` a propósito: tenía que aguantar DOS máquinas (Intel y una NVIDIA
# Pascal) y la mesa HW de nouveau sólo existe como binario Alpine, o sea rompiendo soberanía.
#
# Acá el objetivo es otro y por eso la decisión se invierte: **cerrar el frente de ScreenCast**. El
# diagnóstico del 2026-08-05 (ver el runbook) midió que la cadena entera del portal funciona —el nodo
# `cosmic-screencast` existe en el grafo de pipewire— y que lo que falla es el COMPOSITOR en una
# llamada EGL, 8 ms antes de que el stream quede en `Paused`, porque en QEMU mesa es `kms_swrast`:
# software, **sin exportación dmabuf**. Un stream de vídeo continuo la necesita; una captura de una
# sola toma no (por eso `cosmic-screenshot` sí anda, va por shm).
#
# El `mesa` del corpus se construye con `-Dgallium-drivers=iris` y **ya está en el rootfs de COSMIC**
# (`usr/lib/dri/iris_dri.so`). En QEMU nunca se usaba; en un TigerLake es el driver correcto y trae
# dmabuf de verdad. O sea que este viaje no es «lo mismo pero en metal»: es la única forma de saber si
# ScreenCast está bien.
#
# ⚠ POR ESO NO SE INYECTA llvmpipe ACÁ. Mezclarlos no es aditivo: llvmpipe trae su propio
# `libgbm`/`libEGL`/`libgallium` y sobrescribirlos DESACTIVA iris. Si esta máquina no tuviera Intel,
# la imagen correcta sería la de KDE, no ésta con un parche.
#
# ── LO QUE **NO** HACE FALTA INYECTAR, Y ES MEDIBLE ─────────────────────────────────────────────
# La imagen de KDE inyecta `libLLVM.so.18`, `libgcc_s.so.1` y `libstdc++.so.6` porque el JIT de
# llvmpipe los NEEDea. Acá NO: `iris_dri.so` pide sólo `libglapi`, `libdrm`, `libz`, `libzstd` y
# `libc` —mesa va con `-Dllvm=disabled`— y `cosmic-comp` sólo `libdisplay-info`, `libgbm`, `libseat`,
# `libudev`, `libinput`, `libpixman`, `libxkbcommon` y `libc`. **Cero C++ y cero libgcc**: la imagen
# COSMIC de metal no arrastra un solo binario ajeno de Alpine. Verificado con clausura ELF completa
# del rootfs: 42 raíces de runtime, 0 con NEEDED sin proveedor.
#
# work/metal-rootfs base: arje-zero PID1 + busybox + firmware i915 (estática ⇒ inyectar musl)
# work/cosmic-rootfs COSMIC runtime-completo (89 recetas: compositor, apps, portal, pipewire,
# wireplumber, portal-probe) — ya trae iris_dri.so
# + kernel linux-metal-dual (i915 builtin)
# → work/hammer-cosmic-metal.img
#
# Uso: scripts/cosmic/metal-desktop-image.sh
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT"
BASE="${BASE:-work/metal-rootfs}"
COSMIC="${COSMIC:-work/cosmic-rootfs}"
KERNEL="${KERNEL:-$(ls -dt store/*-linux-metal-dual/boot/bzImage 2>/dev/null | head -1)}"
MERGED="${MERGED:-work/cosmic-metal-rootfs}"
IMG="${IMG:-work/hammer-cosmic-metal.img}"
for x in "$BASE" "$COSMIC"; do [ -d "$x" ] || { echo "falta $x (corré scripts/cosmic/hydrate-cosmic.sh)" >&2; exit 1; }; done
[ -n "$KERNEL" ] && [ -r "$KERNEL" ] || { echo "falta linux-metal-dual (hammer build recipes/linux-metal-dual.toml)" >&2; exit 1; }
echo "==> fundiendo rootfs por hardlinks (base metal + COSMIC)"
rm -rf "$MERGED"
cp -al "$BASE" "$MERGED"
cp -al --remove-destination "$COSMIC"/. "$MERGED"/
. "$ROOT/scripts/lib/pid1-desde-store.sh"
inyectar_pid1 "$MERGED" "$BASE"
# El driver HW tiene que estar SÍ O SÍ: si no está, esta imagen no tiene razón de ser y es mejor
# fallar acá que descubrirlo con el USB en la mano frente a una pantalla negra.
[ -r "$MERGED/usr/lib/dri/iris_dri.so" ] || { echo " ✗ falta iris_dri.so — sin GPU Intel esta imagen no sirve" >&2; exit 1; }
echo "==> GL por HARDWARE: $(ls "$MERGED"/usr/lib/dri/ | tr '\n' ' ')"
echo "==> inyectando musl (loader + libc) — COSMIC es dinámico, la base metal es estática"
MUSL="${MUSL:-/usr/lib/musl/lib/libc.so}"
[ -r "$MUSL" ] || { echo "no encuentro musl libc ($MUSL)" >&2; exit 1; }
mkdir -p "$MERGED/usr/lib/musl/lib" "$MERGED/lib"
cp -a "$MUSL" "$MERGED/usr/lib/musl/lib/libc.so"
ln -sf /usr/lib/musl/lib/libc.so "$MERGED/lib/ld-musl-x86_64.so.1"
ln -sf /usr/lib/musl/lib/libc.so "$MERGED/usr/lib/libc.so"
ln -sf /usr/lib/musl/lib/libc.so "$MERGED/usr/lib/libc.musl-x86_64.so.1"
# ── LA PREPARACIÓN DE SESIÓN QUE ESTA IMAGEN NO TENÍA ───────────────────────────────────────────
# Todo lo que sigue existía sólo en qemu-desktop-image.sh. La imagen de metal se armó copiando el
# arranque pero NO esta parte, y el resultado es que el escritorio no podía funcionar en metal por
# razones que nada tienen que ver con la GPU: sin `video` no arranca seatd, sin `messagebus` no
# arranca el bus de SISTEMA — y sin bus de sistema NO HAY PORTAL, o sea que `portal-probe screencast`
# no habría tenido con quién hablar aunque el GL fuese perfecto. Se descubrió corriendo `cosmic-start`
# sobre la imagen de metal por primera vez (antes sólo se validaba que llegara al prompt).
echo "==> inyectando arje-logind-compat (el login1 del fractal) + su política"
if ALC="${ALC:-store/$(./target/release/hammer --store store hash recipes/arje-logind-compat.toml 2>/dev/null | tail -1 | sed 's/^b3://')-arje-logind-compat}"; [ -d "$ALC" ]; then
install -Dm755 "$ALC"/usr/bin/arje-logind-compat "$MERGED"/usr/bin/arje-logind-compat
install -d "$MERGED/usr/share/dbus-1/system.d"
cp -a --remove-destination "$ALC"/usr/share/dbus-1/system.d/. "$MERGED"/usr/share/dbus-1/system.d/ 2>/dev/null || true
echo " ✓ $(basename "$ALC" | cut -c1-12)"
else
echo " ✗ falta arje-logind-compat en store/ — cosmic-comp no hallará sesión" >&2; exit 1
fi
[ -f "$MERGED/usr/share/dbus-1/system.d/org.freedesktop.login1.conf" ] \
|| { echo " ✗ falta org.freedesktop.login1.conf — re-sellá arje-logind-compat" >&2; exit 1; }
echo "==> grupo video (seatd lo exige por NOMBRE) y usuario messagebus (el bus de sistema hace setuid)"
if ! grep -q '^video:' "$MERGED/etc/group" 2>/dev/null; then
{ cat "$MERGED/etc/group" 2>/dev/null; echo "video:x:44:"; } > "$MERGED/etc/group.new"
chmod 644 "$MERGED/etc/group.new"; mv "$MERGED/etc/group.new" "$MERGED/etc/group"
fi
for f in passwd:messagebus:x:81:81:messagebus:/dev/null:/sbin/nologin group:messagebus:x:81:; do
file="${f%%:*}"; line="${f#*:}"
if ! grep -q '^messagebus:' "$MERGED/etc/$file" 2>/dev/null; then
{ cat "$MERGED/etc/$file" 2>/dev/null; echo "$line"; } > "$MERGED/etc/$file.new"
chmod 644 "$MERGED/etc/$file.new"; mv "$MERGED/etc/$file.new" "$MERGED/etc/$file"
fi
done
for h in usr/libexec/dbus-daemon-launch-helper usr/lib/dbus-1.0/dbus-daemon-launch-helper; do
[ -f "$MERGED/$h" ] && { chown 0:81 "$MERGED/$h" 2>/dev/null || true; chmod 4750 "$MERGED/$h"; }
done
grep -q '^video:' "$MERGED/etc/group" && grep -q '^messagebus:' "$MERGED/etc/passwd" \
|| { echo " ✗ video/messagebus no quedaron en /etc — seatd y dbus fallarían" >&2; exit 1; }
echo " ✓ video + messagebus"
# arje-logind-compat busca el bus de sistema en la ruta LEGACY /var/run y dbus-daemon escucha en /run.
# Esta base metal es tan mínima que ni siquiera tiene /var/run: sin el enlace, el daemon se va a idle
# y nunca registra login1.
ln -sfn /run "$MERGED/var/run"
printf 'COSMIC_MODE=%s\n' "${COSMIC_MODE:-bare}" > "$MERGED/etc/cosmic-mode"
# ── el arranque del escritorio ──────────────────────────────────────────────────────────────────
# Se instala EL MISMO script que corre en QEMU. Eso es deseable —hace que «validado en QEMU»
# signifique algo para el metal— pero SÓLO si el script no tiene supuestos del emulador escondidos.
#
# ⚠ ACÁ HABÍA UN COMENTARIO QUE AFIRMABA QUE NO LOS TENÍA, Y ERA FALSO. El script exportaba
# `LIBGL_ALWAYS_SOFTWARE=1` y `MESA_LOADER_DRIVER_OVERRIDE=kms_swrast` incondicionalmente, que es
# correcto sobre virtio-gpu y letal sobre un Intel real: la mesa del corpus es `iris` y no existe
# ningún `kms_swrast_dri.so` que cargar ⇒ EGL falla y el compositor no arranca. Costó un viaje
# físico el 2026-08-05. Ahora el script DETECTA el modo de GL, y esta imagen lo verifica abajo.
echo "==> instalando /usr/bin/cosmic-start (detecta HW/software; ya no fuerza kms_swrast)"
install -Dm755 scripts/cosmic/cosmic-start.sh "$MERGED/usr/bin/cosmic-start"
grep -q "MESA_LOADER_DRIVER_OVERRIDE=kms_swrast$" "$MERGED/usr/bin/cosmic-start" && {
echo " ✗ cosmic-start todavía fuerza kms_swrast incondicionalmente — esta imagen sería inútil" >&2; exit 1; }
# ── firmware i915 para OTRAS generaciones Intel, no sólo la del laptop ───────────────────────────
# La base metal trae sólo `tgl_*` (TigerLake). En el Coffee Lake del OptiPlex 3060 el kernel pidió
# `i915/kbl_dmc_ver1_04.bin` y no estaba. NO es fatal —el propio kernel dice «Disabling runtime power
# management» y sigue haciendo modeset— pero es gratis arreglarlo y evita perseguir un fantasma.
FWSRC="${FWSRC:-/usr/lib/firmware/i915}"
if [ -d "$FWSRC" ]; then
n=0
for f in "$FWSRC"/{kbl,skl,cfl,icl,ehl,jsl,adl,dg}*.bin*; do
[ -r "$f" ] || continue
b="$(basename "$f")"
case "$b" in
*.zst) zstd -qdf "$f" -o "$MERGED/lib/firmware/i915/${b%.zst}" 2>/dev/null && n=$((n+1)) ;;
*) cp -a "$f" "$MERGED/lib/firmware/i915/" && n=$((n+1)) ;;
esac
done
echo "==> firmware i915: +$n blobs de otras generaciones (la base sólo traía tgl_*)"
else
echo " ⚠ sin $FWSRC — la imagen sólo servirá plenamente en TigerLake"
fi
# ── SSH: la puerta trasera que convierte un viaje físico en una sesión remota ────────────────────
# sshd ya arrancaba y escuchaba en :22, pero era INALCANZABLE: `PasswordAuthentication no` y sin
# /root/.ssh/authorized_keys. O sea que en metal sólo quedaba depurar a ciegas sobre la pantalla.
# Con una clave horneada, el próximo fallo se mira por red y se itera sin quemar el USB de nuevo.
AKEY="${AKEY:-work/metal-authorized_keys}"
if [ -r "$AKEY" ]; then
install -Dm600 "$AKEY" "$MERGED/root/.ssh/authorized_keys"
chmod 700 "$MERGED/root/.ssh"
echo "==> SSH: authorized_keys horneada ($(wc -l < "$AKEY") clave/s) — root por clave, sin password"
else
echo " ⚠ sin $AKEY ⇒ sshd escuchará pero será inalcanzable (depuración sólo a ciegas)"
fi
# ── cosmic-diag: UN comando que deja toda la evidencia en disco ─────────────────────────────────
# Depurar en metal cuesta un viaje físico por iteración, así que el objetivo es que una corrida
# capture TODO de una vez y sin que nadie tenga que acordarse de los pasos. Lo que importa acá y no
# se estaba capturando: el `dmesg` DESPUÉS de la sesión. Si cosmic-comp muere por SIGSEGV —que es lo
# que indica su log cortado a los 0,4 s, sin panic y con RUST_BACKTRACE=1— el kernel imprime
# `cosmic-comp[PID]: segfault at ... in <BIBLIOTECA>`, y esa biblioteca ES el diagnóstico.
cat > "$MERGED/usr/bin/cosmic-diag" <<'DIAG'
#!/bin/sh
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
L=/var/log/cosmic
mkdir -p "$L"
echo "== cosmic-diag: corriendo la sesión y capturando todo en $L =="
/bin/busybox dmesg > "$L/dmesg-antes.txt" 2>&1
cosmic-start > "$L/run.txt" 2>&1 &
# ⚠ NO usar un `sleep` fijo. La 1ª versión esperaba 50s y en el OptiPlex 3060 la sesión tarda ~105s
# en llegar al compositor (seatd, bus, logind, pipewire, wireplumber, cada uno con su espera) ⇒ el
# dmesg «después» se capturaba ANTES de que cosmic-comp arrancara, salía byte a byte idéntico al de
# antes, y el viaje se gastaba sin dato. Ahora se espera al HECHO, no al reloj: que cosmic-comp
# aparezca y después desaparezca. Ésa es la ventana donde el kernel diría un segfault.
vivo() { for c in /proc/[0-9]*/comm; do [ "$(cat "$c" 2>/dev/null)" = cosmic-comp ] && return 0; done; return 1; }
i=0; visto=0
while [ $i -lt 72 ]; do # 72 × 5s = 6 min de techo
if vivo; then
[ $visto = 0 ] && echo " cosmic-comp vivo (t=$((i*5))s), esperando a que muera o se estabilice"
visto=1
elif [ $visto = 1 ]; then
echo " cosmic-comp DESAPARECIÓ (t=$((i*5))s) — capturando dmesg"
break
fi
i=$((i + 1)); sleep 5
done
[ $visto = 0 ] && echo " ⚠ cosmic-comp nunca apareció en $((i*5))s — capturo igual"
sleep 2
/bin/busybox dmesg > "$L/dmesg-despues.txt" 2>&1
/bin/busybox sync
echo "== listo. Lo que importa: =="
# OJO con el patrón: `Code: ` (mayúsc/minúsc ignoradas) matchea `microcode: Current revision:` y
# reporta un falso positivo que parece un hallazgo. Se ancla a lo que de verdad imprime el kernel
# ante una señal, y sólo para el proceso que nos importa.
grep -aE "segfault|general protection fault|traps:|Killed process" "$L/dmesg-despues.txt" | tail -5 || true
echo "== (si no salió nada arriba, no fue SIGSEGV: cosmic-comp salió por su cuenta) =="
echo "== evidencia completa en $L — apagá y traé el USB =="
DIAG
chmod 0755 "$MERGED/usr/bin/cosmic-diag"
# ── WiFi: la QCA9377 del OptiPlex, y por qué hace falta un re-probe ──────────────────────────────
# `pci 0000:02:00.0: [168c:0042]` = Qualcomm Atheros QCA9377, que va por `ath10k`. El kernel ahora lo
# lleva compilado dentro (MODULES está desactivado en esta receta), y ahí aparece un problema de
# ORDEN: un driver builtin prueba el dispositivo a los ~5 s y pide su firmware, pero el rootfs recién
# se monta a los ~13 s ⇒ `Direct firmware load failed -2` y la tarjeta queda muda para siempre. No hay
# módulo que cargar después. La salida es forzar un RE-PROBE del dispositivo PCI cuando /lib/firmware
# ya existe: `remove` + `rescan`. Es el mismo tipo de carrera que la del initramfs con el USB.
FWATH="${FWATH:-/usr/lib/firmware/ath10k}"
if [ -d "$FWATH/QCA9377" ]; then
n=0
for f in "$FWATH"/QCA9377/hw1.0/*.bin*; do
[ -r "$f" ] || continue
b="$(basename "$f")"; d="$MERGED/lib/firmware/ath10k/QCA9377/hw1.0"
mkdir -p "$d"
case "$b" in
*.zst) zstd -qdf "$f" -o "$d/${b%.zst}" 2>/dev/null && n=$((n+1)) ;;
*) cp -a "$f" "$d/" && n=$((n+1)) ;;
esac
done
echo "==> firmware ath10k/QCA9377: $n blobs"
else
echo " ⚠ sin $FWATH/QCA9377 — el WiFi de esa máquina no va a levantar"
fi
cat > "$MERGED/usr/bin/wifi-up" <<'WIFI'
#!/bin/sh
# wifi-up <SSID> <clave> — conecta la WiFi y muestra la IP para entrar por SSH.
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
SSID="$1"; PSK="$2"
[ -n "$SSID" ] || { echo "uso: wifi-up <SSID> <clave>"; exit 1; }
# Re-probe: ath10k pidió su firmware antes de que existiera /lib/firmware (ver el comentario de la
# imagen). Sin esto la tarjeta no aparece como interfaz aunque el driver esté.
for d in /sys/bus/pci/devices/*; do
[ "$(cat "$d/vendor" 2>/dev/null)" = "0x168c" ] || continue
echo "re-probando $(basename "$d") para que ath10k reintente el firmware…"
echo 1 > "$d/remove" 2>/dev/null; sleep 2
echo 1 > /sys/bus/pci/rescan 2>/dev/null; sleep 4
done
IF=""
for i in /sys/class/net/*; do
case "$(basename "$i")" in wlan*|wlp*) IF="$(basename "$i")"; break ;; esac
done
[ -n "$IF" ] || { echo "no apareció interfaz wifi. Mirá: dmesg | grep -i ath10k"; exit 1; }
echo "interfaz: $IF"
ip link set "$IF" up
# ctrl_interface NO es opcional: sin esa línea wpa_supplicant no abre socket de control y `wpa_cli`
# no tiene con qué hablar — el síntoma es «wpa_cli no conecta», que parece un fallo de red y es
# simplemente que no hay puerta. Y sin wpa_cli no se puede saber si asoció, que es el único dato que
# distingue «clave mal» de «DHCP mudo».
mkdir -p /run/wpa_supplicant
printf 'ctrl_interface=/run/wpa_supplicant\nupdate_config=1\nnetwork={\n\tssid="%s"\n\tpsk="%s"\n}\n' "$SSID" "$PSK" > /tmp/wpa.conf
wpa_supplicant -B -i "$IF" -c /tmp/wpa.conf -Dnl80211 || { echo "wpa_supplicant falló"; exit 1; }
# Esperar a la ASOCIACIÓN, no al reloj. Con `sleep 6` fijo, udhcpc salía a preguntar antes de que
# hubiera enlace y devolvía «sin IPv4», que se lee como «clave equivocada» cuando la clave está bien.
# Un 802.11ac tarda más: escaneo de dos bandas + 4-way handshake.
i=0
while [ $i -lt 40 ]; do
ST=$(wpa_cli -i "$IF" status 2>/dev/null | sed -n 's/^wpa_state=//p')
[ "$ST" = COMPLETED ] && { echo "asociado a $SSID (${i}s)"; break; }
[ $((i % 5)) -eq 0 ] && echo " esperando asociación… wpa_state=${ST:-?} (${i}s)"
i=$((i + 1)); sleep 1
done
[ "$ST" = COMPLETED ] || echo " ⚠ no asoció (wpa_state=${ST:-?}). 4WAY_HANDSHAKE=clave; SCANNING=no ve la red"
cat > /tmp/udhcpc.script <<'US'
#!/bin/sh
case "$1" in
bound|renew)
ifconfig "$interface" "$ip" netmask "${subnet:-255.255.255.0}"
[ -n "$router" ] && route add default gw "${router%% *}" dev "$interface" 2>/dev/null
[ -n "$dns" ] && { : > /etc/resolv.conf; for d in $dns; do echo "nameserver $d" >> /etc/resolv.conf; done; }
;;
esac
exit 0
US
chmod +x /tmp/udhcpc.script
# Tres intentos: el AP a veces no contesta el primer DISCOVER justo tras asociar.
for t in 1 2 3; do
udhcpc -i "$IF" -n -q -s /tmp/udhcpc.script && break
echo " DHCP intento $t sin respuesta, reintento…"; sleep 3
done
ip -4 addr show "$IF" | grep -w inet \
|| echo "sin IPv4. Mirá 'wpa_cli -i $IF status': si wpa_state=COMPLETED el problema es DHCP, no la clave"
echo "== desde el laptop: ssh -i ~/.ssh/hammer-metal root@<la IP de arriba> =="
WIFI
chmod 0755 "$MERGED/usr/bin/wifi-up"
# ── el enlace WiFi se caía solo y cortaba la depuración a mitad ─────────────────────────────────
# Medido el 2026-08-06 depurando la OptiPlex por SSH sobre un hotspot: la máquina desaparecía de la
# red cada pocos minutos. Dos causas que se suman, y ninguna es «la WiFi anda mal»:
# - `udhcpc -q` (el que usa wifi-up) pide la dirección UNA vez y termina: nadie renueva el lease.
# - el AP olvida a los clientes ociosos y deja de contestar ARP ⇒ «No route to host» desde fuera,
# mientras la máquina se cree perfectamente conectada. El síntoma engaña: no hay nada en dmesg.
# El ping periódico resuelve las dos a la vez — detecta la caída Y mantiene viva la entrada en el AP.
# Sin esto, cada sesión de depuración remota se interrumpe sola y hay que volver físicamente al
# teclado, que es justo lo que la red venía a evitar.
cat > "$MERGED/usr/bin/wifi-keep" <<'KEEP'
#!/bin/sh
# wifi-keep <gateway> <SSID> <clave> — mantiene el enlace vivo y reasocia si se cae.
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
GW="$1"; SSID="$2"; PSK="$3"
[ -n "$PSK" ] || { echo "uso: wifi-keep <gateway> <SSID> <clave>"; exit 1; }
L=/var/log/wifi-keep.log
echo "$(date): vigilante arriba (gw=$GW ssid=$SSID)" >> $L
while :; do
if ping -c1 -W3 "$GW" >/dev/null 2>&1; then
sleep 10
else
echo "$(date): sin respuesta del gateway — reasociando" >> $L
pkill wpa_supplicant 2>/dev/null
sleep 2
wifi-up "$SSID" "$PSK" >> $L 2>&1
sleep 10
fi
done
KEEP
chmod 0755 "$MERGED/usr/bin/wifi-keep"
# ── getty en tty1: el prompt TIENE que salir por la PANTALLA ────────────────────────────────────
# Lección del 1er viaje físico de KDE, que costó un USB quemado: el CMDLINE horneado es
# `console=tty0 console=ttyS0,115200 …` y el kernel hace /dev/console = la ÚLTIMA `console=` ⇒ ttyS0.
# La seed card de la base supervisa un solo getty, sobre `console` ⇒ en un metal sin puerto serie el
# shell nace INVISIBLE: la pantalla se congela en el último printk y parece que el arranque murió,
# cuando está vivo. Nunca se vio en QEMU porque siempre se validaba con `-nographic`, donde el serial
# ES la pantalla.
#
# Se arregla acá y no en el cmdline: un getty sobre tty1 no depende del cmdline (en metal siempre hay
# VT) y no obliga a re-sellar el kernel. El getty de `console` se conserva para debug por serie.
echo '==> añadiendo getty en tty1 (el prompt de `console` va al SERIAL, invisible en metal)'
cat > "$MERGED/usr/bin/console-login" <<'LOGIN'
#!/bin/sh
# arje-zero lanza los getty con envp VACÍO ⇒ sin PATH no se resuelve ni `cat` ni `cosmic-start`.
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
export HOME=/root TERM=linux
[ -r /etc/motd ] && /bin/busybox cat /etc/motd
exec /bin/sh
LOGIN
chmod 0755 "$MERGED/usr/bin/console-login"
# ── netup pedía DHCP antes de que existiera el enlace ────────────────────────────────────────────
# Medido en el dmesg del OptiPlex 3060: el r8169 levanta el enlace a los 20,2 s (con downshift a
# 100 Mbps), y el ente sshd corría `netup` a los 13,7 s ⇒ DHCP sobre un cable que aún no estaba y
# «sin respuesta DHCP» para siempre, sin reintento. En QEMU no se ve: el virtio-net tiene carrier
# desde el instante cero. Sin red no hay SSH, y sin SSH el metal se depura a ciegas.
cat > "$MERGED/usr/bin/netup-wait" <<'NETUP'
#!/bin/sh
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
i=0
while [ $i -lt 40 ]; do
for c in /sys/class/net/*/carrier; do
case "$c" in */lo/*) continue ;; esac # lo siempre dice 1: no es señal de nada
[ -r "$c" ] || continue
[ "$(cat "$c" 2>/dev/null)" = 1 ] && exec /usr/bin/netup
done
i=$((i + 1)); sleep 1
done
echo "netup-wait: ninguna interfaz con carrier tras ${i}s — pido DHCP igual"
exec /usr/bin/netup
NETUP
chmod 0755 "$MERGED/usr/bin/netup-wait"
python3 - "$MERGED/ente/seed.card.json" <<'PY'
# OJO: $MERGED es un árbol de HARDLINKS a la base metal ⇒ escribir in-place mutaría también
# work/metal-rootfs (y toda otra imagen que comparta el inodo). Se escribe a un temporal y se
# os.replace ⇒ se reemplaza la ENTRADA de directorio, el inodo compartido queda intacto.
import json, sys, copy, os
p = sys.argv[1]
card = json.load(open(p))
gen = card["genesis"]
gen[:] = [g for g in gen if g["label"] != "tty1-getty"] # idempotente
base = next(g for g in gen if g["label"] == "console-getty")
tty1 = copy.deepcopy(base)
tty1["id"] = "01HQAR53D4M2NBV8KZTYXFQA04"
tty1["label"] = "tty1-getty"
tty1["payload"]["Native"]["argv"] = ["getty", "-n", "-l", "/usr/bin/console-login", "38400", "tty1"]
gen.append(tty1)
# el ente sshd espera al carrier antes de pedir DHCP (ver netup-wait); idempotente
for g in gen:
if g["label"] == "sshd":
a = g["payload"]["Native"]["argv"]
a[:] = [x.replace("/usr/bin/netup;", "/usr/bin/netup-wait;") for x in a]
json.dump(card, open(p + ".new", "w"), indent=2, sort_keys=True)
os.replace(p + ".new", p)
print(" ✓ genesis: " + ", ".join(g["label"] for g in gen))
PY
# `rm -f` antes del heredoc para ROMPER EL HARDLINK: `cat >` escribiría el inodo COMPARTIDO con
# work/metal-rootfs y le cambiaría el motd a todas las demás imágenes.
rm -f "$MERGED/etc/motd"
cat > "$MERGED/etc/motd" <<'MOTD'
#-- hammer :: escritorio COSMIC (metal, Intel i915 + iris HW) ---------------
GL por HARDWARE (iris). Es la diferencia con la imagen de KDE, y el motivo
de este viaje: en QEMU mesa era software y ScreenCast no podía exportar
dmabuf.
Arrancar el escritorio: cosmic-start
⇒ tiene que decir "GL por HARDWARE". Si dice SOFTWARE, el viaje no
sirve: ScreenCast fallaría igual que en QEMU.
Si NO levanta: cosmic-diag <-- UN comando, deja todo en
/var/log/cosmic/ (incluido el dmesg de después,
que es donde el kernel nombra el segfault)
Probar el portal: portal-probe version
portal-probe screencast <-- LO QUE VINIMOS A VER
GPU detectada: ls /dev/dri ; dmesg | grep -iE 'i915|drm'
Logs (en DISCO, sobreviven al apagón): /var/log/cosmic/
Red y SSH: netup-wait ; ip addr (root por clave, sin password)
WiFi: wifi-up <SSID> <clave>
WiFi que no se cae: wifi-keep <gateway> <SSID> <clave> & (reasocia sola)
---------------------------------------------------------------------------
MOTD
# ── /var/log/cosmic VACÍO, y no es un detalle ───────────────────────────────────────────────────
# Los logs de la sesión van a disco (para que sobrevivan al apagón en metal), pero eso crea una
# trampa: si se valida la imagen arrancándola en QEMU, esa corrida ESCRIBE DENTRO DEL FICHERO DE
# IMAGEN, y al quemarla el USB se lleva el log de la prueba. Pasó: el usuario grepeó el log en metal
# y leyó `drm=virtio-pci` — mi corrida en QEMU, no la suya. Un log viejo que parece nuevo es peor que
# no tener log.
# REGLA: validar sobre una COPIA de la imagen, o regenerarla antes de quemar. Esto es el cinturón.
rm -rf "$MERGED/var/log/cosmic"
install -d -m 755 "$MERGED/var/log/cosmic"
mkdir -p "$MERGED/root/.config" "$MERGED/root/.cache" "$MERGED/run/user/0" "$MERGED/tmp"
chmod 1777 "$MERGED/tmp"
echo "==> rootfs fundido: $(du -sh "$MERGED" | cut -f1)"
need_mb=$(du -sm "$MERGED" | cut -f1)
ROOT_SIZE="${ROOT_SIZE:-$(( need_mb + 1536 ))}"
echo "==> ROOT_SIZE=${ROOT_SIZE} MiB · kernel: $(basename "$(dirname "$(dirname "$KERNEL")")")"
export ROOTFS="$MERGED" IMG ROOT_SIZE KERNEL
export ESP_MB="${ESP_MB:-160}" STORE_SIZE="${STORE_SIZE:-512}" STATE_SIZE="${STATE_SIZE:-768}"
exec ./scripts/install-image-efi.sh