Actualizar a 1.1.4 fallaba con "no escribio config\.version tras la
actualizacion" aunque el binario nuevo estuviera instalado y corriendo desde la
ruta correcta. La causa era el ORDEN dentro de ensure_runtime_layout(): el sello
iba al final, detras del re-despliegue de las deps embebidas (7-Zip y ODBC). Al
cambiar de version esas deps se re-copian ENTERAS, asi que el sello quedaba por
detras de esa copia y del desempaquetado del onefile de ~254 MB con el antivirus
escaneando cada archivo. El PANEL se rendia esperandolo y daba por fallida una
actualizacion que iba bien.
- El sello se escribe lo primero, en cuanto existen las carpetas. Es tambien mas
honesto sobre lo que significa —"que binario esta corriendo"—, que es cierto
desde que el proceso arranca. El sello de DEPS sigue yendo al final, donde su
comentario explica por que: si la copia falla a medias, el proximo arranque
reintenta en vez de quedar marcado como al dia.
- La version va en la PRIMERA linea del log de arranque. Permite comprobar que
binario corre de verdad mirando solo config/logs, sin depender del sello ni del
reporte al panel: verificar una actualizacion deja de obligar a creerse lo que
diga otro sistema.
La prueba nueva observa el estado del sello EN EL MOMENTO en que empieza la copia
de deps, no al final, que es la unica forma de fijar el orden. Comprobado que
muerde: devolviendo el sello al final falla con `assert None == '1.1.5'`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sustituye a 1.1.3, cuyo artefacto publicado se genero antes de los arreglos: su
install.ps1 no traia -UpdateInPlace y su binario llevaba el runner.py que ignora
--headless en Windows. Actualizar con el mataba el agente, cambiaba la tarea
programada a SYSTEM y la dejaba sin arrancar, porque Qt no puede crear su
plataforma como SYSTEM en la sesion 0 sin offscreen.
Se quema el numero en lugar de reemplazar 1.1.3 con --force: los paquetes
genericos de Gitea son inmutables, y dos contenidos distintos con la misma
version fue exactamente lo que hizo caro el diagnostico.
Hay que reconstruir los binarios aunque el codigo ya estuviera arreglado, porque
__version__ va compilado dentro del ejecutable y el PANEL compara el sello
config/.version contra la version que creia estar instalando.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BUILD.md gana la seccion del fallo silencioso y como reproducirlo con
scripts/emular-actualizacion-windows.ps1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
README afirmaba que la app no puede correr como servicio de Windows y sugería
NSSM, contradiciendo a install.ps1 desde que existe. LEEME.txt solo documentaba
el camino manual para Windows, mientras que para Linux ya traía el instalador.
BUILD.md gana -UpdateInPlace, las tres garantías al reemplazar el binario y el
remedio del token filtrado por UAC.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.