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>
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>
Actualizar un servidor con el agente en una carpeta NO estandar terminaba en
verde y lo dejaba con la version anterior. Todo el camino de Windows
identificaba al agente por NOMBRE, mientras que lo unico que se actualiza se
identifica por RUTA; en cuanto las dos no coincidian, nada fallaba y nada
cambiaba.
- Se alinea la tarea programada con el binario instalado. `Start-ScheduledTask`
ejecuta la ruta registrada en su accion, no el -Prefix: si difieren, se copiaba
el binario nuevo en un sitio y se arrancaba el viejo del otro. Ahora se
reapunta conservando disparador, principal, ajustes y argumentos; si no se
puede corregir, FALLA — arrancar a sabiendas el binario anterior es peor.
- La confirmacion de arranque mira la RUTA del proceso. Un agente viejo que
nunca se detuvo satisfacia igual de bien un `Get-Process -Name`. Si la ruta no
es legible (un proceso de SYSTEM no la expone sin elevacion) se acepta por
nombre y se avisa, en vez de revertir una actualizacion correcta por falta de
informacion.
- Corregido Merge-EnvFile con un config\.env de UNA linea: al asignar la salida
de un `if`, PowerShell desenrolla un array de un elemento a escalar, asi que
$lines.Count reventaba con Set-StrictMode y la siembra abortaba la instalacion.
Nuevo scripts/emular-actualizacion-windows.ps1: monta un agente falso (un .exe
real que se queda vivo), una instalacion en una carpeta y una tarea apuntando a
otra, corre el instalador y dice si la actualizacion surtio efecto. Sin elevacion
y sin tocar la instalacion real de la maquina. Es lo que destapo los dos
defectos: contra el instalador anterior reproduce el sintoma exacto —codigo de
salida 0 y "El agente esta corriendo con el binario nuevo" sobre un servidor
intacto— y contra este confirma que ya surte efecto, sin tocar la tarea cuando
ya estaba bien.
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>
install.sh recibió la maquinaria de actualización segura y install.ps1 nunca
recibió el equivalente. La asimetría se notaba en producción: actualizar desde
el PANEL dejaba el servidor sin agente.
- -UpdateInPlace: actualiza conservando la tarea y la configuración, sin correr
el bootstrap (una segunda instancia purga el Temp\ de la que está viva).
- No se interrumpe una restauración en curso: sale con 75 (EX_TEMPFAIL), que el
PANEL traduce a "reintenta luego". Windows no tenía esta guarda y una
reinstalación a destiempo dejaba el respaldo vetado y la base en SINGLE_USER.
- Respaldo del binario anterior y reversión automática si el nuevo no arranca.
- Rearranque garantizado en TODOS los modos: la detención corría siempre, pero
solo -Service volvía a arrancar algo.
- Espera de liberación del .exe de 5s a 30s con reintentos de la copia.
- Se distingue "no es administrador" de "es administrador con el token filtrado
por UAC", que es lo que recibe una sesión de OpenSSH. Se veían idénticos y el
remedio es el opuesto.
Y --headless deja de ser un no-op en Windows: _ensure_qt_platform() salía de
inmediato en win32, así que la tarea ONSTART arrancaba como SYSTEM en la sesión 0
con el plugin Qt 'windows' intentando crear una ventana real. En Linux el unit
fija QT_QPA_PLATFORM=offscreen por fuera, y esa asimetría escondió el defecto.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Instalando con sudo, el agente quedaba sin poder leer ni escribir NADA de lo
suyo, y tanto el panel como su sonda lo reportaban como éxito.
El bootstrap ejecuta el binario como root, y ese arranque crea todo el árbol bajo
PREFIX: config/, config/data/app.db, config/logs/, Entrada/, Procesados/,
Fallados/ y Temp/. La siembra escribe config/.env en 0600, también de root. Pero
el unit se registra con User=$SUDO_USER, así que el agente arranca como una
cuenta común que no puede leer su configuración (load_dotenv sin try/except),
ni guardar jobs en su base, ni mover ZIPs entre las carpetas de trabajo.
install.sh no hacía chown en ninguna parte.
Reproducido en un contenedor antes de arreglarlo: .env root:root 600, app.db y
Entrada/ de root, las tres operaciones del agente fallando — y `test -f` de la
sonda del panel dando ok, o sea verde justo en el caso roto.
SERVICE_USER se resuelve ahora al principio (antes se calculaba dentro del case
de --service, después del bootstrap y de la siembra), conservando la misma
cadena de precedencia. El chown va después de ambos pasos, solo cuando el script
corre como root y el servicio no es root, y solo del usuario —no del grupo—;
`chown` no toca los modos, así que el 0600 del .env sobrevive.
Con dos cuidados que no son opcionales:
- Guarda contra un PREFIX de sistema: un `chown -R` sobre / o /opt sería
catastrófico. Se rechazan las rutas de sistema y las de un solo componente,
avisando en vez de abortar una instalación ya hecha.
- Si el chown falla se reporta como ERROR con el comando de arreglo, no se traga.
Es el mismo criterio que el repo ya aplica al icacls de Windows: no se asume
que un comando de endurecimiento tuvo éxito.
Probado: arreglo, guarda de /opt, usuario inexistente, y sin sudo (no intenta
nada). Windows no necesita el arreglo simétrico y queda documentado por qué: la
tarea corre como SYSTEM con RunLevel Highest y install.ps1 no restringe ninguna
ACL, así que hereda de su carpeta padre y puede leer todo lo del Administrador.
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.