Esa ruta es el peor caso posible y existe en produccion: la ruta por omision
C:\Aduanasoft\CloudRestoreAS es PREFIJO DE CADENA de ella, asi que cualquier
comparacion hecha con startsWith daria por iguales dos instalaciones distintas
— y el resultado seria el fallo silencioso otra vez, actualizar una carpeta y
arrancar la otra.
El flujo ya la manejaba bien (Test-SamePath compara por igualdad exacta tras
normalizar), pero nada lo probaba: la emulacion usaba declarada/otra-carpeta,
nombres sin relacion entre si, que un startsWith mal puesto pasaria sin problema.
- Escenario `sufijo` en emular-actualizacion-windows.ps1: instalacion en
...\CloudRestoreAS-win y tarea apuntando a ...\CloudRestoreAS. Verificado en
Windows: reapunta la tarea, el proceso queda corriendo desde -win y el sello en
la version nueva.
- Nuevo scripts/probar-funciones-install.ps1: extrae las funciones del instalador
por AST y las ejercita contra una tarea simulada, sin elevacion. Cubre los casos
limite de la comparacion de rutas (el par de prefijo en ambos sentidos, comillas,
barra final, mayusculas, `..`, ruta vacia) y que Sync-AgentTaskPath falle cuando
no puede corregir.
BUILD.md documenta por que se compara por igualdad exacta y no por prefijo.
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.