tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1101Rust 61Shell 47Markdown 371349 Definiciones

fix(qorpa): el EBUSY del overlay preguntaba por otra `run` y el culpable era un nieto fugado

Hoy el usuario no pudo abrir una sesión: el overlay de `claude-sergio-7` estaba tomado y el mensaje
decía «¿Hay otra `hammer qorpa run claude-sergio-7` viva?». **No había ninguna.** Lo retenía un
`minga host` —demonio del espejo de tawasuyu— que alguien arrancó DENTRO de la jaula, que sobrevivió
a su sesión reparentado a pid 1, y que al seguir vivo mantiene vivo el **namespace de montaje** de la
jaula y con él el overlay. La pregunta del mensaje mandaba a buscar donde no estaba.

Es el mismo patrón que el `flock` de CLAUDE.md: **el recurso lo retiene algo que nadie asocia con el
recurso**. Ahí la respuesta fue `fuser -v <lock>`; acá no sirve —el montaje vive en OTRO namespace y
`/proc/mounts` del anfitrión no lo ve— así que se recorre `/proc/*/mountinfo`, que sí lo ve.

Ahora el error no adivina, NOMBRA:

```
Error: el overlay de `claude-sergio-8` sigue tomado tras 6 intentos.
Dos overlays con el mismo `upper` corromperían la capa mutable, y por eso el
kernel lo niega.
   lo retiene(n), con el overlay en SU namespace de montaje:
     pid 20043    sleep            sleep 300
   Suele ser un NIETO FUGADO: un demonio arrancado dentro de la jaula
   que sobrevivió a su sesión y mantiene vivo su namespace de montaje.
   Si no lo necesitás, `kill <pid>` libera el overlay. Matalo POR PID,
   nunca con `pkill -f`, que se lleva tu propia shell por delante.
```

En el incidente real habría impreso `pid 21764  minga  …/minga --repo … host …` y el arreglo habría
sido una línea en vez de veinte minutos de `/proc`.

Y si de verdad no hay nadie, lo dice así en vez de culpar a un proceso inexistente: entonces el
EBUSY es otra cosa y el mensaje orienta hacia el `upper`/`work`.

Probado con un control sintético que reproduce el nieto fugado —`unshare -m` + mount del overlay de
una instancia LIBRE + sleep— sin tocar ninguna de las sesiones vivas del usuario: el mensaje nombra
al retenedor, y tras matarlo la instancia corre (`DENTRO-OK`).

⚠ Detalle de forma que costó dos intentos: las líneas del aviso se concatenan sueltas, no con `\`
de continuación — el `\` arrastra la sangría del FUENTE al mensaje y el texto salía escalonado.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

159b5d57c4d3126557823867ac0a217b83e2392e · por Sergio · (hace 9 d) · padre deed01974 · ver el árbol en este commit

Delta semántico · 2

crates/takana-cli/src/qorpa.rs

1 archivos · +86 −1

diff --git a/crates/takana-cli/src/qorpa.rs b/crates/takana-cli/src/qorpa.rs
index 0ae14708..19b4259e 100644
--- a/crates/takana-cli/src/qorpa.rs
+++ b/crates/takana-cli/src/qorpa.rs
@@ -1516,6 +1516,64 @@ struct Ajustes {
     evidencia: bool,
 }
 
+/// Quién tiene el overlay de esta instancia montado **en su propio namespace**.
+///
+/// ══ POR QUÉ HACE FALTA, Y QUÉ COSTÓ NO TENERLO ════════════════════════════════════════════════
+/// El mensaje de abajo preguntaba «¿Hay otra `qorpa run <id>` viva?», y el 2026-10-13 la respuesta
+/// fue **NO** y el overlay estaba tomado igual. Lo tenía un **nieto fugado**: un `minga host`
+/// —demonio del espejo de tawasuyu— que alguien arrancó DENTRO de la jaula, que sobrevivió a su
+/// sesión reparentado a pid 1, y que al seguir vivo mantiene vivo el **namespace de montaje** de la
+/// jaula… y con él el overlay. El `qorpa run` que lo creó ya no existe, así que la pregunta del
+/// mensaje llevaba a buscar donde no estaba.
+///
+/// Es el mismo patrón que el `flock` de `CLAUDE.md`: *el recurso lo retiene algo que nadie asocia
+/// con el recurso*. Ahí la respuesta fue `fuser -v <lock>`; acá no hay `fuser` que valga —el mount
+/// vive en otro namespace y `/proc/mounts` del anfitrión no lo ve— así que se recorre
+/// `/proc/*/mountinfo`, que sí.
+///
+/// Devuelve `(pid, comm, cmdline recortada)`. Vacío significa que de verdad no hay nadie: entonces
+/// el EBUSY es otra cosa y el mensaje lo dice así en vez de culpar a un proceso inexistente.
+fn quien_retiene_overlay(id: &str) -> Vec<(String, String, String)> {
+    let yo = std::process::id().to_string();
+    let mut fuera = Vec::new();
+    let Ok(entradas) = std::fs::read_dir("/proc") else {
+        return fuera;
+    };
+    for e in entradas.flatten() {
+        let pid = e.file_name().to_string_lossy().to_string();
+        if pid == yo || !pid.chars().all(|c| c.is_ascii_digit()) {
+            continue;
+        }
+        // `mountinfo` y no `mounts`: el primero es por-proceso y muestra SU namespace, que es
+        // justo donde se esconde el montaje que el anfitrión no ve.
+        let Ok(mi) = std::fs::read_to_string(format!("/proc/{pid}/mountinfo")) else {
+            continue;
+        };
+        if !mi.contains(id) {
+            continue;
+        }
+        let comm = std::fs::read_to_string(format!("/proc/{pid}/comm"))
+            .unwrap_or_default()
+            .trim()
+            .to_string();
+        let cmd = std::fs::read("/proc/".to_string() + &pid + "/cmdline")
+            .map(|b| {
+                b.split(|&c| c == 0)
+                    .filter(|s| !s.is_empty())
+                    .map(|s| String::from_utf8_lossy(s).to_string())
+                    .collect::<Vec<_>>()
+                    .join(" ")
+            })
+            .unwrap_or_default();
+        let cmd: String = cmd.chars().take(100).collect();
+        fuera.push((pid, comm, cmd));
+        if fuera.len() >= 6 {
+            break;
+        }
+    }
+    fuera
+}
+
 fn run_instance(
     root: &Path, id: &str, cmd: &[String], aj: Ajustes,
 ) -> Result<()> {
@@ -1813,8 +1871,35 @@ fn run_instance(
             break;
         }
         if intento == INTENTOS {
+            // NO se adivina quién lo tiene: se mira. El mensaje viejo preguntaba por otra
+            // `qorpa run` y el culpable medido fue un demonio nieto en el namespace de la jaula
+            // muerta, así que la pregunta mandaba a buscar donde no estaba.
+            let culpables = quien_retiene_overlay(&id);
+            let detalle = if culpables.is_empty() {
+                // Cada línea suelta, sin `\` de continuación: el `\` arrastra la sangría del
+                // FUENTE al mensaje y el aviso salía escalonado en la terminal.
+                [
+                    "   y NADIE lo tiene montado: ningún proceso tiene este overlay en su",
+                    "   `mountinfo`. Entonces el EBUSY no es otra corrida — mirá si el `upper`",
+                    "   o el `work` quedaron en un estado raro, o si el namespace que lo montó",
+                    "   acaba de morir y el kernel todavía no lo soltó.",
+                ]
+                .join("\n")
+            } else {
+                let mut t = String::from("   lo retiene(n), con el overlay en SU namespace de montaje:\n");
+                for (pid, comm, cmd) in &culpables {
+                    t.push_str(&format!("     pid {pid:<8} {comm:<16} {cmd}\n"));
+                }
+                t.push_str("   Suele ser un NIETO FUGADO: un demonio arrancado dentro de la jaula\n");
+                t.push_str("   que sobrevivió a su sesión y mantiene vivo su namespace de montaje.\n");
+                t.push_str("   Si no lo necesitás, `kill <pid>` libera el overlay. Matalo POR PID,\n");
+                t.push_str("   nunca con `pkill -f`, que se lleva tu propia shell por delante.");
+                t
+            };
             bail!(
-                "el overlay de `{id}` sigue tomado tras {INTENTOS} intentos.\n                   ¿Hay otra `hammer qorpa run {id}` viva? Dos overlays con el mismo `upper` \n                   corromperían la capa mutable, y por eso el kernel lo niega."
+                "el overlay de `{id}` sigue tomado tras {INTENTOS} intentos.\n\
+                 Dos overlays con el mismo `upper` corromperían la capa mutable, y por eso el\n\
+                 kernel lo niega.\n{detalle}"
             );
         }
         eprintln!("  el overlay lo tiene todavía la corrida anterior — reintento {intento}/{INTENTOS}");