overlay: el commit no podía desmontar /bin en una máquina viva — y el código prometía el perezoso sin hacerlo
La primera instalación real de un paquete en la caja (`takana install zsh`, sin --prefix) dejó el sistema A MEDIAS: `commit` desmontó 5 de 7 targets y murió con `target is busy` en /bin, con dos overlays montados y el estado sin promocionar. La causa no es un fd abierto: los procesos tienen su EJECUTABLE mapeado desde /bin y /usr/bin —empezando por PID 1, /usr/bin/arje-zero— y un ejecutable mapeado pin-ea el montaje. En un FHS vivo eso no se puede evitar esperando. El comentario de `do_umount_lenient` prometía el desmontaje perezoso «desde la Fase 2» y el código NUNCA pasaba `-l`. Ahora, ante `busy`, reintenta perezoso: desengancha el montaje ya mismo (las rutas vuelven a resolver al directorio real, que es lo que el promote necesita) y libera cuando el último proceso que lo miraba se muere; los vivos ven el mismo contenido porque el commit copia el upper al lower justo después. Probado en la caja con un paquete nuevo: install tree ⇒ overlay; commit ⇒ WARN del perezoso sobre /usr/bin y «commit OK — 1 archivo(s) promocionados»; 0 overlays, /usr/bin/tree real, tree v2.3.2. El SDD 28 §5.10 anota las otras cuatro cosas que ese `sudo takana install zsh` destapó: el repo por defecto que no existía, el lab que no se encontraba desde /root, el binario del sistema incapaz de leer los .tkn nuevos, y —la peor— que la instalación queda PARTIDA porque el overlay cubre siete directorios y /usr/share no es uno: 1296 de los 1331 ficheros fueron directos al FHS real mientras 2 binarios esperaban el commit, y el mensaje decía `apply OK` igual.
This commit is contained in:
@@ -412,8 +412,7 @@ fn do_mount(lower: &Path, upper: &Path, work: &Path) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn do_umount_lenient(target: &Path) -> Result<()> {
|
fn do_umount_lenient(target: &Path) -> Result<()> {
|
||||||
// `umount` retorna 32 para "not mounted"; el flag --lazy fuerza el desmontaje incluso
|
// `umount` retorna 32 para "not mounted".
|
||||||
// si hay procesos con file descriptors abiertos sobre el merged. Para Fase 2 nos vale.
|
|
||||||
let out = Command::new("umount")
|
let out = Command::new("umount")
|
||||||
.arg(target)
|
.arg(target)
|
||||||
.output()
|
.output()
|
||||||
@@ -425,6 +424,40 @@ fn do_umount_lenient(target: &Path) -> Result<()> {
|
|||||||
if stderr.contains("not mounted") || stderr.contains("not found") {
|
if stderr.contains("not mounted") || stderr.contains("not found") {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⚠ `target is busy` NO es un error acá, y esto es medido (2026-09-21, caja `takana`): un
|
||||||
|
// `commit` sobre el FHS VIVO falla siempre en `/bin` y `/usr/bin`, porque los procesos de la
|
||||||
|
// máquina —empezando por PID 1, `/usr/bin/arje-zero`— tienen su ejecutable mapeado desde ahí,
|
||||||
|
// y un ejecutable mapeado pin-ea el montaje. Pasó con la primera instalación real de un
|
||||||
|
// paquete en la caja: desmontó 5 de 7 targets, murió en `/bin`, y dejó el sistema **a medias**
|
||||||
|
// —dos overlays montados y el estado sin promocionar— que es el peor de los finales.
|
||||||
|
//
|
||||||
|
// El desmontaje PEREZOSO es la salida correcta y la que este mismo comentario prometía desde
|
||||||
|
// la Fase 2 sin que nadie la hubiera cableado: desengancha el montaje del árbol ya mismo (las
|
||||||
|
// rutas vuelven a resolver al directorio real, que es lo que el promote necesita) y libera
|
||||||
|
// cuando el último proceso que lo mira se muera. Los que siguen vivos ven el mismo contenido,
|
||||||
|
// porque lo primero que hace el commit después es copiar el upper al lower.
|
||||||
|
if stderr.contains("busy") {
|
||||||
|
let lazy = Command::new("umount")
|
||||||
|
.arg("-l")
|
||||||
|
.arg(target)
|
||||||
|
.output()
|
||||||
|
.map_err(|e| Error::Umount(format!("spawn umount -l: {e}")))?;
|
||||||
|
if lazy.status.success() {
|
||||||
|
tracing::warn!(
|
||||||
|
target = %target.display(),
|
||||||
|
"umount: el target estaba ocupado (procesos con ejecutables mapeados ahí, \
|
||||||
|
normal en un FHS vivo) ⇒ desmontaje perezoso"
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
return Err(Error::Umount(format!(
|
||||||
|
"umount {} falló ocupado, y el perezoso tampoco pudo: {}",
|
||||||
|
target.display(),
|
||||||
|
String::from_utf8_lossy(&lazy.stderr).trim()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
Err(Error::Umount(format!(
|
Err(Error::Umount(format!(
|
||||||
"umount {} falló (exit {:?}): {}",
|
"umount {} falló (exit {:?}): {}",
|
||||||
target.display(),
|
target.display(),
|
||||||
|
|||||||
@@ -697,6 +697,70 @@ Hoy la caja no instala de su propio repo —viene de imágenes—, así que el a
|
|||||||
que lo haga; se pone igual para que el día que instale algo ya esté, y porque una línea por hora no
|
que lo haga; se pone igual para que el día que instale algo ya esté, y porque una línea por hora no
|
||||||
es ruido. Cada hora y no cada 30 minutos: el repo cambia cuando alguien publica.
|
es ruido. Cada hora y no cada 30 minutos: el repo cambia cuando alguien publica.
|
||||||
|
|
||||||
|
### 5.10 `takana install zsh` en el sistema de verdad — cinco cosas que faltaban *(2026-09-21)*
|
||||||
|
|
||||||
|
El usuario escribió `sudo takana install zsh` en la caja y le contestó **«paquete 'zsh' no está en
|
||||||
|
el repo /var/lib/hammer/repo. Disponibles: (ninguno)»**. Reproducido tal cual. Debajo de ese
|
||||||
|
mensaje había cinco cosas distintas, y ninguna se ve hasta que alguien instala de verdad.
|
||||||
|
|
||||||
|
**1. El repo por defecto no existía.** `DEFAULT_REPO` está cableado a `/var/lib/hammer/repo` y el
|
||||||
|
repo publicado vive en `/srv/repo`. Un enlace (`/var/lib/hammer/repo → /srv/repo`) hace que
|
||||||
|
`install <nombre>` sin `--repo` funcione **y sin pasar por la red**: un directorio local sirve
|
||||||
|
igual que la URL.
|
||||||
|
|
||||||
|
**2. El lab no se encontraba desde `/root`.** `install` reproduce desde fuente y el toolchain entra
|
||||||
|
en el `ArtifactHash`, así que sin `.dev-fs` ni siquiera puede calcular el hash a comparar (§5.4).
|
||||||
|
La resolución es `TAKANA_LAB` → hermano del store → hacia arriba desde el CWD; con el store en
|
||||||
|
`/store` y el CWD en `/root`, ninguna acertaba. Enlace `/.dev-fs → /work/dev-fs` y la regla del
|
||||||
|
**hermano del store** acierta desde cualquier directorio y para cualquier usuario. *(Los dos labs
|
||||||
|
de la caja tienen el mismo `apk db` byte a byte, así que el `LabFingerprint` es el mismo.)*
|
||||||
|
|
||||||
|
**3. El `takana` del sistema no podía leer los paquetes nuevos.** Los `.tkn` de hoy llevan los
|
||||||
|
parches como **lista** (§5.5) y un cliente anterior ignora ese campo —serde salta lo desconocido—,
|
||||||
|
así que reconstruiría **sin parches** y moriría contra el `expected_hash`. Se reemplazó por el
|
||||||
|
build release. ⚠ Y se comprobó ANTES de tocar que `/usr/bin/takana` tenía `enlaces=1` e inodo
|
||||||
|
distinto al del store: es una copia, no un hardlink. **Un `cp` encima de un fichero hidratado que
|
||||||
|
SÍ fuera hardlink reescribiría el artefacto del store por debajo** — se borra y se pone uno nuevo,
|
||||||
|
nunca se sobrescribe.
|
||||||
|
|
||||||
|
**4. ⚠ La instalación quedó PARTIDA, y eso no lo dice ningún mensaje.** Sin `--prefix`, `install`
|
||||||
|
abre un overlay sobre el FHS… pero sólo sobre **siete** directorios clásicos (`/bin`, `/usr/bin`,
|
||||||
|
`/sbin`, `/usr/sbin`, `/lib`, `/usr/lib`, `/etc`). **`/usr/share` no está entre ellos**, así que de
|
||||||
|
los 1331 ficheros de zsh, **1296 fueron directos al FHS real** y sólo los 2 binarios quedaron en el
|
||||||
|
upper esperando el `commit`. El mensaje final dice `apply OK` igual. Un `discard` en ese estado
|
||||||
|
habría borrado el binario y dejado 1296 ficheros huérfanos: *el experimento no era reversible y
|
||||||
|
parecía que sí*.
|
||||||
|
|
||||||
|
**5. ⚠ `commit` no puede desmontar `/bin` en una máquina viva.** Los procesos tienen su ejecutable
|
||||||
|
**mapeado** desde ahí —empezando por PID 1, `/usr/bin/arje-zero`— y un ejecutable mapeado pin-ea el
|
||||||
|
montaje. Resultado: desmontó 5 de 7 targets, murió con `target is busy` en `/bin`, y dejó **dos
|
||||||
|
overlays montados con el estado sin promocionar**, que es el peor de los finales. El comentario del
|
||||||
|
propio `do_umount_lenient` prometía el desmontaje perezoso desde la Fase 2 y **el código nunca
|
||||||
|
pasaba `-l`**. Arreglado: ante `busy` reintenta perezoso, que desengancha el montaje ya mismo (las
|
||||||
|
rutas vuelven a resolver al directorio real, que es lo que el promote necesita) y libera cuando el
|
||||||
|
último proceso que lo miraba se muere.
|
||||||
|
|
||||||
|
**El arreglo, probado en la caja viva** con un segundo paquete instalado de cero:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ takana install tree ⇒ overlay 1790022033-1575-000000
|
||||||
|
$ takana commit 1790022033-1575-000000
|
||||||
|
WARN takana_overlay: umount: el target estaba ocupado (procesos con ejecutables mapeados ahí,
|
||||||
|
normal en un FHS vivo) ⇒ desmontaje perezoso target=/usr/bin
|
||||||
|
commit OK — 1 archivo(s) promocionados, 0 eliminado(s) (anotado en el diario)
|
||||||
|
⇒ overlays montados: 0 · /usr/bin/tree real · `tree v2.3.2` corriendo
|
||||||
|
```
|
||||||
|
|
||||||
|
Mientras tanto la caja se dejó limpia a mano: los dos binarios copiados al `/bin` REAL —visto a
|
||||||
|
través de un `mount --bind /` no recursivo, porque el overlay tapaba el destino—, los dos montajes
|
||||||
|
sueltos en perezoso y el estado borrado. Ahora: `zsh 5.9` en `/bin/zsh`, 1296 ficheros en
|
||||||
|
`/usr/share/zsh`, **cero overlays**, y `takana installed` lo lista.
|
||||||
|
|
||||||
|
**Y la trampa del §5.7 se cobró su pieza**: root dejó un `/work/swm-recipes` **suyo** dentro del
|
||||||
|
`/work` del usuario (el catálogo de recetas que `install` materializa; `work_root` sale de
|
||||||
|
`dirname(store)/work`). Devuelto a `sergio`. El guion de publicar ya exporta `TAKANA_WORK` para
|
||||||
|
esto; `install` a secas no tiene esa defensa, y es deuda anotada.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. La mudanza como experimento: el criterio de borrado
|
## 6. La mudanza como experimento: el criterio de borrado
|
||||||
|
|||||||
Reference in New Issue
Block a user