gioser

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 1184Shell 70Rust 62Markdown 501503 Sutichaykuna

censar: cuatro binarios que YA NO EXISTEN en disco — y el paso de rescate, que caduca

`/proc/<pid>/exe` termina en « (deleted)» para cuatro servicios de gioser: el fichero ya no está en
disco, sólo vive el inodo que sostiene su proceso. Y en tres de los cuatro hay AHORA otro fichero en
la misma ruta, de distinto tamaño:

    tejido          corre 12750368 B · en su ruta hay 13815864 B
    shuma-gateway   corre  8431896 B · en su ruta hay 10363504 B
    pacha-secretos  corre  8634240 B · en su ruta hay  8647456 B
    puerta-f6e393ff corre 197262440 B · en su ruta NO HAY NADA

Copiar la ruta NO FALLA: muda otra cosa, y el servicio nuevo no es el que estaba andando. Todo verde,
todo distinto — el modo de fallo más caro que hay en este frente.

Se recuperan leyendo `/proc/<pid>/exe`, y SÓLO mientras el proceso viva. En una mudanza que termina
BORRANDO el origen, un reinicio de gioser antes de este paso los pierde para siempre. Por eso:

· clase propia en `origen_binario` (`borrado`), no una coletilla dentro del texto de `suelto`: no es
  «hay que llevarlo», es «se pierde en el próximo reinicio y el que está en su ruta no es el mismo».
  El motivo trae el comando literal de rescate con su pid.
· paso `rescate` en el plan, ANTES del preflight, porque es el único paso que puede volverse
  IMPOSIBLE mientras se piensa el resto.
· su verificación COMPARA TAMAÑOS contra el que corre. No es celo: un `cat` de un `/proc` que ya no
  existe crea un fichero VACÍO y devuelve 0, así que sin comparar el rescate «pasa». Probado en los
  dos sentidos — el paso real sale 0, y con un fichero vacío a propósito dice `FALTA tejido` y sale 1.

Los cuatro ya están rescatados en `work/mudanza/rescate/` (gitignored), byte a byte iguales a los que
corren, con su SHA256SUMS.

Y el empalme se hizo comprobando que el marcador fuera ÚNICO antes de cortar, que es la lección del
`def pasos` duplicado de ayer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x

edc95325d940e09b7d24630240db2ad352975061 · ruwaq Sergio · (1 killa ñawpaq) · tayta c78c5ff91 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 5

scripts/mudanza/censar.py

scripts/mudanza/planear.py

3 qillqakuna · +100 −1

diff --git a/docs/29-mudanza.md b/docs/29-mudanza.md
index 2d598893..4308b9f3 100644
--- a/docs/29-mudanza.md
+++ b/docs/29-mudanza.md
@@ -301,6 +301,35 @@ reconstruyen el servidor desde cero**:
 Ninguno de los tres es un log de lo que se hizo: los tres son entradas que se vuelven a ejecutar.
 Ésa es la diferencia entre mudar un servidor y poder mudarlo otra vez.
 
+### 4.0 undecies · Cuatro binarios que ya no existen — el paso que CADUCA
+
+El más urgente de todo el censo, y lo encontró la lista de lo que el perfil no puede cubrir:
+`/proc/<pid>/exe` termina en `(deleted)` para **cuatro servicios de gioser**. El fichero ya no está
+en disco; sólo vive el inodo que sostiene su proceso.
+
+Y en tres de los cuatro hay **ahora otro fichero en la misma ruta, de distinto tamaño**:
+
+| servicio | corriendo | en su ruta hoy |
+|---|---|---|
+| `tejido` | 12 750 368 B | 13 815 864 B |
+| `shuma-gateway` | 8 431 896 B | 10 363 504 B |
+| `pacha-secretos` | 8 634 240 B | 8 647 456 B |
+| `puerta-f6e393ff…` | 197 262 440 B | *no hay nada* |
+
+Copiar la ruta **no falla**: muda otra cosa, y el servicio nuevo no es el que estaba andando. Es el
+modo de fallo más caro que hay en este documento — todo verde, todo distinto.
+
+Se recuperan leyendo `/proc/<pid>/exe`, **y sólo mientras el proceso viva**. En una mudanza que
+termina *borrando el origen*, un reinicio de gioser antes de este paso pierde esos binarios para
+siempre. Por eso el rescate va **antes del preflight**: es el único paso del plan que puede volverse
+imposible mientras se piensa el resto.
+
+Su verificación **compara tamaños** contra el que corre, y no por celo: un `cat` de un `/proc` que ya
+no existe crea un fichero **vacío** y devuelve 0. Probado en los dos sentidos — el paso real pasa, y
+con un fichero vacío a propósito el guardián dice `FALTA tejido` y sale 1.
+
+Los cuatro ya están rescatados en `work/mudanza/rescate/`, con su `SHA256SUMS`.
+
 ### 4.0 decies · Leer la config del servidor: un fósil se detecta SIN tocar la red
 
 La sonda DNS dice si un dominio resuelve; no si el servidor tiene algo que servirle. Eso lo dice el
diff --git a/scripts/mudanza/censar.py b/scripts/mudanza/censar.py
index 9a06142a..4badc9cb 100755
--- a/scripts/mudanza/censar.py
+++ b/scripts/mudanza/censar.py
@@ -101,6 +101,24 @@ def invocaciones(t, pids):
         # quedaban en «no se pudo leer su binario» por esto, y no por ser realmente desconocidos.
         exe, _ = t.run(f"readlink /proc/{pid}/exe 2>/dev/null")
         cmd, cwd, exe = cmd.strip(), cwd.strip(), exe.strip()
+        # ⚠⚠ `/proc/<pid>/exe` termina en « (deleted)» cuando el fichero YA NO ESTÁ en disco: el
+        # proceso sostiene el inodo y nadie más lo puede abrir por su ruta. Medido en gioser: CUATRO
+        # servicios están así, y en dos de ellos hay AHORA otro fichero en la misma ruta, de distinto
+        # tamaño — `tejido` corre 12 750 368 bytes y en su ruta hay 13 815 864. Copiar la ruta muda
+        # OTRA COSA, en silencio, y el servicio nuevo no es el que estaba andando.
+        #
+        # Se recupera leyendo `/proc/<pid>/exe`, y SÓLO mientras el proceso viva: al reiniciar gioser
+        # —o al morirse ese pid— ese binario desaparece para siempre. En una mudanza que termina
+        # BORRANDO el origen, esto es lo más urgente del censo.
+        borrado = exe.endswith(" (deleted)")
+        if borrado:
+            exe = exe[:-len(" (deleted)")]
+        vivo_b = disco_b = ""
+        if borrado:
+            v, _ = t.run(f"stat -L -c %s /proc/{pid}/exe 2>/dev/null")
+            d, _ = t.run(f"stat -c %s {shlex.quote(exe)} 2>/dev/null")
+            vivo_b = v.strip() if v.strip().isdigit() else ""
+            disco_b = d.strip() if d.strip().isdigit() else ""
         for v in ("cmd", "cwd", "exe"):
             if locals()[v].startswith("(sonda falló"):
                 cmd, cwd, exe = (("" if v == "cmd" else cmd), ("" if v == "cwd" else cwd),
@@ -111,7 +129,9 @@ def invocaciones(t, pids):
         if cmd or cwd or exe:
             out[nombre] = {"cmdline": cmd, "cwd": cwd,
                            "exe": exe if exe.startswith("/") else "",
-                           "usuario": (sup or {}).get("usuario", "")}
+                           "usuario": (sup or {}).get("usuario", ""),
+                           "exe_borrado": borrado, "pid": pid,
+                           "bytes_vivo": vivo_b, "bytes_disco": disco_b}
     return out
 
 
@@ -642,6 +662,11 @@ def emitir_toml(c):
         L.append(f"exec = {toml_escape(s.get('exec',''))}")
         if s.get('usuario'):
             L.append(f"usuario = {toml_escape(s['usuario'])}")
+        if s.get('exec_borrado'):
+            L.append("exec_borrado = true")
+            L.append(f"pid_vivo = {int(s.get('pid_vivo') or 0)}")
+            L.append(f"bytes_vivo = {toml_escape(s.get('bytes_vivo',''))}")
+            L.append(f"bytes_disco = {toml_escape(s.get('bytes_disco',''))}")
         if s.get('exec_motivo'):
             L.append(f"exec_motivo = {toml_escape(s['exec_motivo'])}")
         L.append(f"paquete = {toml_escape(s.get('paquete',''))}")
@@ -750,6 +775,11 @@ def main():
         e['cmdline'] = i.get('cmdline', '')
         e['cwd'] = i.get('cwd', '')
         e['exe_real'] = i.get('exe', '')
+        if i.get('exe_borrado'):
+            e['exec_borrado'] = True
+            e['pid_vivo'] = i.get('pid', 0)
+            e['bytes_vivo'] = i.get('bytes_vivo', '')
+            e['bytes_disco'] = i.get('bytes_disco', '')
         if i.get('usuario'):
             e['usuario'] = i['usuario']
     # `/proc/<pid>/exe` MANDA sobre `argv[0]`: es el binario de verdad y no lo puede reescribir el
diff --git a/scripts/mudanza/planear.py b/scripts/mudanza/planear.py
index b1dcb8ef..ab406277 100755
--- a/scripts/mudanza/planear.py
+++ b/scripts/mudanza/planear.py
@@ -297,6 +297,19 @@ def origen_binario(e, repo=None):
         return ("paquete-ajeno",
                 f"lo provee el gestor del origen: paquete `{pkg}`. En el destino hace falta una "
                 f"receta takana, o `qorpa` si se acepta una jaula con la distro ajena (ADR 0015)")
+    if exe and e.get("exec_borrado"):
+        # Su propia clase, y la más urgente del censo: el fichero YA NO EXISTE. No es «hay que
+        # llevarlo», es «se pierde en el próximo reinicio y el que está en su ruta NO es el mismo».
+        pid = e.get("pid_vivo") or (e.get("pids") or [0])[0]
+        bv, bd = e.get("bytes_vivo", ""), e.get("bytes_disco", "")
+        dif = (f" El que hay AHORA en esa ruta pesa {bd} bytes y el que corre {bv}: NO SON EL MISMO."
+               if bv and bd and bv != bd else
+               (" En esa ruta ya no hay nada." if not bd else ""))
+        return ("borrado",
+                f"⚠ el binario que CORRE fue borrado o reemplazado: `{exe}` ya no existe en disco."
+                f"{dif} Sólo vive mientras viva el pid {pid}; al reiniciar el origen se pierde PARA "
+                f"SIEMPRE. Rescatalo ya:  sudo cat /proc/{pid}/exe > {os.path.basename(exe)}  "
+                f"&& chmod +x {os.path.basename(exe)}")
     if exe:
         return ("suelto",
                 f"NADIE lo provee: binario puesto a mano en {exe}. Hay que LLEVARLO (con lo que "
@@ -544,6 +557,33 @@ def pasos(censo, target, key):
     muere = [(t, n, e) for t, n, e in entradas(censo) if e.get("decision") == "muere"]
     contenido = censo.get("contenido", [])
 
+    # ── 0 bis. RESCATE, antes que nada ──────────────────────────────────────────────────────────
+    # Va PRIMERO, antes del preflight, porque es el único paso del plan que puede volverse
+    # IMPOSIBLE mientras se piensa el resto: estos binarios ya no existen en disco y sólo viven en el
+    # inodo que sostiene su proceso. Un reinicio del origen —o que se caiga ese pid— y se pierden
+    # para siempre. Y en dos de los cuatro hay AHORA otro fichero en la misma ruta, de distinto
+    # tamaño: copiar la ruta no falla, muda otra cosa.
+    borrados = [s for s in censo.get("servicio", [])
+                if s.get("exec_borrado") and s.get("decision") == "muda"]
+    if borrados:
+        lineas, ver = [], []
+        for s in borrados:
+            pid = s.get("pid_vivo") or (s.get("pids") or [0])[0]
+            nom = os.path.basename(s.get("exec") or s["name"])
+            lineas.append(f"sudo cat /proc/{pid}/exe > rescate/{shlex.quote(nom)} && "
+                          f"chmod +x rescate/{shlex.quote(nom)}")
+            if s.get("bytes_vivo"):
+                # La verificación COMPARA contra el tamaño del que CORRE. Un `cat` de un /proc que ya
+                # no existe crea un fichero VACÍO y devuelve 0: sin comparar, el rescate «pasa».
+                ver.append(f"[ \"$(stat -c %s rescate/{shlex.quote(nom)} 2>/dev/null)\" = "
+                           f"\"{s['bytes_vivo']}\" ] || {{ echo 'FALTA {nom}'; exit 1; }}")
+        paso("rescate", f"rescatar {len(borrados)} binario(s) BORRADOS del disco (sólo mientras corran)",
+             "mkdir -p rescate\n" + "\n".join(lineas),
+             verifica="\n".join(ver) + "\necho '  ✓ los binarios rescatados coinciden con los que corren'",
+             nota="⚠ ESTE PASO CADUCA. `/proc/<pid>/exe` deja de existir cuando muere el proceso, y "
+                  "estos ficheros ya no están en su ruta. Si el origen se reinicia antes de correr "
+                  "esto, el binario que estaba andando NO se puede recuperar de ninguna manera.")
+
     # ── 0. preflight ────────────────────────────────────────────────────────────────────────────
     # ⚠ El sitio se mide POR SISTEMA DE FICHEROS, no contra `/`. Medido en la caja takana: `/` tiene
     # 3,5 G libres, `/store` 19 G y `/work` 66 G — un preflight que sume 33,6 G y los compare contra