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…
granja: el lock ya no se lo puede quedar un proceso fugado
1#!/usr/bin/env bash
2# latido.sh — el latido de la granja SIN depender del init.
3#
4# POR QUÉ EXISTE (2026-07-22): el latido vivía en el crontab, y eso resultó frágil por dos motivos
5# que se destaparon juntos tras un corte sucio:
6# 1. Este laptop arranca a veces con `init=/usr/local/sbin/arje-zero` y a veces con OpenRC (KDE).
7# No hay systemd en NINGUNO de los dos. `cronie` es un servicio OpenRC ⇒ en el arranque arje
8# simplemente no está, y el latido no late. Peor: no late EN SILENCIO, nadie se entera.
9# 2. Aun arrancando OpenRC, se vio el runlevel `default` quedar a medias (cronie/metalog/acpid
10# caídos, NetworkManager/dbus arriba) ⇒ ni siquiera ahí es garantía.
11# El síntoma final es caro: sin latido el hub no siembra, el worker agota la cola y se queda idle
12# quemando € sin moler (viola [[vps-nunca-idle]]).
13#
14# LA IDEA: colgar el latido de la SESIÓN en vez del init. Cualquier terminal que abras, en cualquier
15# arranque, se asegura de que haya exactamente un latido vivo. Sin root, sin unidad de servicio, sin
16# nada que mantener por duplicado en dos inits.
17#
18# CONTRA (asumido a propósito): no late con la máquina encendida pero sin sesión tuya abierta. Es un
19# laptop, no un server: cuando no hay sesión tampoco hay nadie esperando la cosecha. Y el primer
20# ciclo dispara AL INSTANTE de abrir la terminal, así que un arranque nunca arrastra el hueco (el
21# cron, en cambio, esperaba hasta 30 min al próximo tick).
22#
23# SINGLETON: el lock lo lleva `cosecha-cron.sh` (así vale también si el cron de OpenRC dispara a la
24# vez); acá hay un segundo lock, el del BUCLE, para no acumular un demonio por terminal abierta.
25#
26# Uso:
27# latido.sh --ensure # idempotente y barato: si no hay latido, lo lanza. Esto va en ~/.zshrc
28# latido.sh --loop # el bucle en sí (lo lanza --ensure; no hace falta llamarlo a mano)
29# latido.sh --status # ¿late? desde cuándo, y cuándo fue el último ciclo
30# latido.sh --stop # parar el latido
31# Env: INTERVALO (def 1800s = los mismos 30 min que tenía el crontab)
32set -uo pipefail
33
34ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
35INTERVALO="${INTERVALO:-1800}"
36LOCK="$ROOT/work/.latido.lock"
37LOG="$ROOT/work/latido.log"
38COSECHA_LOG="$ROOT/work/cosecha-cron.log"
39mkdir -p "$ROOT/work"
40
41ts() { date -u +%FT%TZ; }
42
43case "${1:---ensure}" in
44 --ensure)
45 # Barato a propósito: esto corre en CADA terminal que abrís. Un flock que falla es ~1ms y no
46 # imprime nada — abrir una terminal no puede volverse lento ni ruidoso por el latido.
47 exec 9>"$LOCK"
48 if flock -n 9; then
49 flock -u 9
50 # setsid + nohup: que sobreviva al cierre de ESTA terminal. Si no, el latido se muere con la
51 # ventana que lo parió y volvemos al problema de origen.
52 setsid nohup "$0" --loop </dev/null >/dev/null 2>&1 &
53 disown 2>/dev/null || true
54 fi
55 exit 0
56 ;;
57
58 --status)
59 if exec 9>"$LOCK" && flock -n 9; then
60 flock -u 9
61 echo "latido: PARADO"
62 exit 1
63 else
64 echo "latido: VIVO"
65 [ -f "$LOG" ] && tail -3 "$LOG"
66 exit 0
67 fi
68 ;;
69
70 --stop)
71 pkill -f "latido.sh --loop" && echo "latido: parado" || echo "latido: no había ninguno vivo"
72 exit 0
73 ;;
74
75 --loop) ;;
76 *) echo "uso: latido.sh --ensure|--loop|--status|--stop" >&2; exit 2 ;;
77esac
78
79# --- el bucle ---
80# El lock se mantiene TOMADO mientras el bucle vive: es lo que hace que --ensure sepa que ya hay uno
81# y que --status pueda responder sin pidfiles (un pidfile miente si el proceso murió; el lock no).
82exec 9>"$LOCK"
83flock -n 9 || exit 0 # otro latido ganó la carrera (dos terminales abiertas a la vez)
84
85echo "── $(ts) latido arranca (pid $$, intervalo ${INTERVALO}s)" >>"$LOG"
86trap 'echo "── $(ts) latido termina (pid $$)" >>"$LOG"' EXIT
87
88while :; do
89 # `|| true` + `set -uo pipefail` sin `-e`: un ciclo que falla (worker caído, red, gitea) NO puede
90 # matar el latido. Se loguea y se reintenta al próximo tick; ésa es toda la robustez que hace falta.
91 # `9>&-` cierra el fd DEL LOCK DEL LATIDO en el hijo. Sin esto, un proceso fugado de la cosecha
92 # lo retiene para siempre y el latido queda MUERTO PERO APARENTANDO ESTAR VIVO: `--status` lee
93 # el lock, lo ve tomado y dice VIVO, y `--ensure` se niega a levantar otro. Es el peor modo de
94 # fallo posible acá — la máquina deja de latir y todo indica que late. Medido 2026-09-08 con
95 # dos `firefox` colgados que retuvieron el lock de build hora y media (regla 1 de CLAUDE.md).
96 # ⚠ Y acá NO va `flock -o`, al revés que en cosecha-cron/campana-deuda/harvest: en este script
97 # el fd retenido ES el mecanismo de vida a propósito (así `--status` responde sin pidfile, que
98 # mentiría si el proceso muriera). Lo que sobra es que lo hereden los HIJOS, no que lo tenga el
99 # bucle. Por eso el arreglo es distinto aunque el defecto sea el mismo.
100 ( cd "$ROOT" && ./scripts/farm/cosecha-cron.sh ) 9>&- >>"$COSECHA_LOG" 2>&1 || \
101 echo "── $(ts) latido: el ciclo salió != 0 (ver cosecha-cron.log) — sigo" >>"$LOG"
102 sleep "$INTERVALO"
103done