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