Hay entornos donde no se usa root en absoluto, así que ni `sudo -n` ni una cuenta
root son opciones. Este modo actualiza una instalación existente dejando su unit
de systemd intacto, que es lo único que una actualización necesita de verdad:
reemplazar el binario y reiniciar el proceso.
Se apoya en dos hechos, uno de ellos contrario a lo que decía el propio repo:
- `install` NO sufre ETXTBSY. A diferencia de `cp` —que abre con O_TRUNC—,
desvincula el destino antes de crearlo, y por eso `make install` funciona sobre
binarios en ejecución. Comprobado: `cp` sobre un ELF corriendo da "Text file
busy" y `install` no. La consecuencia es la que importa: reemplazar el binario
exige escritura en el DIRECTORIO, no en el archivo. El comentario de install.sh,
BUILD.md y el CHANGELOG afirmaban lo contrario y mandaban al operador a
diagnosticar un archivo en uso cuando lo que tenía era un EACCES.
- El unit corre como el usuario que instaló (cadena SUDO_USER) y trae
Restart=always, así que esa cuenta puede señalizar el proceso y systemd lo
relevanta con el binario nuevo. No hace falta systemctl ni tocar /etc.
Robustez, que es donde estaba el trabajo real:
- **Reversible.** Respalda el binario antes de reemplazarlo y, si el nuevo no
arranca, lo restaura y confirma que el proceso volvió. Sin esto, una
actualización fallida deja el servidor sin agente. Si tampoco puede revertir,
conserva el respaldo y lo dice en vez de fingir éxito.
- **No interrumpe restauraciones.** El agente no atiende SIGTERM: matarlo a media
restauración deja ese respaldo vetado para siempre (has_blocking_job_by_hash) y
puede dejar la base en SINGLE_USER. Se comprueba Temp/ dos veces —antes de
copiar y otra vez justo antes de señalizar, para cerrar la ventana— y sale con
75 (EX_TEMPFAIL), que el panel traduce a "reintenta luego" y no a un fallo.
- **Diagnostica por qué no volvió**: distingue un unit sin Restart=always de un
StartLimitBurst agotado, con el comando de recuperación.
- Omite el bootstrap de 20 s: es redundante en una actualización
(ensure_runtime_layout corre en cada arranque) y una segunda instancia junto a
la viva purgaría el Temp de la que está trabajando.
Un bug que solo aparecía fuera del camino feliz: con `set -euo pipefail`, un
`$(pgrep ... | head -1)` sin resultados hace fallar la sustitución y `set -e`
mataba el script en silencio — justo en el caso "el proceso no volvió", que es el
que había que manejar. Por eso el rollback no se ejecutaba nunca.
El instalador solo necesitaba root por dos razones circunstanciales: el PREFIX
por omisión en /opt y el unit en /etc/systemd/system. El agente en sí no lo
necesita — su unit corre como el usuario que instala, y las rutas de data_folder
las escribe SQL Server vía las cláusulas MOVE del T-SQL, no el agente.
--user-service instala donde apunte PREFIX (escribible por el usuario, típicamente
bajo su home) y registra el unit en ~/.config/systemd/user/. Para que sobreviva al
cierre de sesión intenta habilitar lingering; si el destino no lo permite, cae a una
entrada @reboot en el crontab del usuario más un vigilante cada 5 min que cubre lo
que en systemd hace Restart=always. Ninguno de los dos mecanismos requiere root.
Con esto, el instalador remoto del panel puede actualizar un servidor cuya cuenta
SSH no es root ni tiene sudo sin contraseña, sin pasarle nunca la contraseña a sudo.
Detalles que costaron una prueba cada uno:
- `pgrep -f "$PREFIX/$BIN_NAME"` se auto-detectaba: la línea de cron del vigilante
contiene esa ruta, así que el `sh -c` que la ejecuta hacía match consigo mismo y
el vigilante nunca rearrancaba. Va anclado con `^`.
- El sed de la plantilla sustituía los marcadores dentro de su propio comentario,
dejando rutas absolutas en un texto sin sentido.
- El bootstrap de 20 s era el único hijo que heredaba el stdin del canal SSH; ahora
lleva `</dev/null`, lo que hace estructural que no pueda consumir nada de él.
Reinstalar es idempotente: no duplica entradas de cron.