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>
443 lines
26 KiB
Bash
Executable File
443 lines
26 KiB
Bash
Executable File
#!/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
|