Files
CloudRecoveryAS/install.sh
hreyes f02cd1f4c3 feat(install): --update-in-place, actualización sin privilegios y reversible
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.
2026-07-30 15:40:37 -06:00

452 lines
21 KiB
Bash
Executable File

#!/usr/bin/env bash
# Instalador Linux de CloudRestoreAS.
#
# NO instala ni descarga NADA del sistema: el binario es 100% autocontenido (Qt/xcb,
# driver ODBC + Kerberos/OpenSSL, y 7-Zip van embebidos). Este script solo coloca el
# binario, marca permisos, hace el bootstrap de config/, opcionalmente siembra las
# credenciales del PANEL, y registra el arranque automático (servicio systemd headless
# o autostart de escritorio).
#
# Uso:
# sudo ./install.sh --service # 24/7 headless vía systemd (recomendado en servidor)
# ./install.sh --user-service # 24/7 headless SIN root: unit de systemd de usuario
# ./install.sh --update-in-place # actualiza en su sitio, SIN privilegios
# ./install.sh --desktop # autostart de escritorio (requiere sesión gráfica)
# ./install.sh # solo instala; sin arranque automático
#
# --update-in-place actualiza una instalación EXISTENTE sin privilegios, dejando el unit de
# systemd como está. Se apoya en dos hechos: `install` desvincula el destino antes de crearlo (a
# diferencia de `cp`, que da ETXTBSY), así que reemplazar el binario solo exige escritura en el
# DIRECTORIO; y el unit trae Restart=always, así que basta señalizar al proceso —desde la cuenta
# que lo corre— para que systemd lo relevante con el binario nuevo. No toca /etc ni systemctl.
# Se niega a actuar si hay una restauración en curso.
#
# --user-service no necesita privilegios: instala donde apunte PREFIX (que debe ser escribible
# por el usuario, típicamente bajo su home), registra el unit en ~/.config/systemd/user/ y lo
# arranca con `systemctl --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 que lo rearranca. Es la vía cuando la cuenta SSH no es root ni tiene sudo
# sin contraseña: al panel le bastan entonces el usuario y la contraseña que ya tiene guardados.
#
# Opciones:
# --panel-env-file <ruta> # archivo KEY=valor con CLOUDRESTORE_PANEL_* que se
# # fusiona en config/.env y se borra al terminar.
# # Lo usa el instalador remoto del PANEL para dejar
# # el servidor configurado sin intervención.
#
# Variables:
# PREFIX=/opt/cloudrestoreas # carpeta destino (default)
# SERVICE_USER=<usuario> # usuario del servicio systemd (default: quien invoca)
set -euo pipefail
MODE="none"
PANEL_ENV_FILE=""
while [[ $# -gt 0 ]]; do
case "$1" in
--service) MODE="service"; shift ;;
--user-service) MODE="user-service"; shift ;;
--update-in-place) MODE="update-in-place"; shift ;;
--desktop) MODE="desktop"; shift ;;
--panel-env-file)
[[ $# -ge 2 ]] || { echo "ERROR: --panel-env-file requiere una ruta" >&2; exit 2; }
PANEL_ENV_FILE="$2"; shift 2 ;;
--panel-env-file=*) PANEL_ENV_FILE="${1#*=}"; shift ;;
-h|--help)
grep '^#' "$0" | grep -v '^#!' | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "Opción desconocida: $1" >&2; exit 2 ;;
esac
done
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PREFIX="${PREFIX:-/opt/cloudrestoreas}"
BIN_NAME="CloudRestoreAS"
UNIT_NAME="cloudrestoreas.service"
# Patrón para localizar el agente vivo. El ancla ^ NO es opcional: sin ella, la propia
# línea de cron del vigilante contiene esta ruta, así que el `sh -c` que la ejecuta hace
# match consigo mismo, el vigilante cree que el agente corre y no lo rearranca nunca.
PGREP_PAT="^$PREFIX/$BIN_NAME"
# Localizar el binario: dist/CloudRestoreAS, dist/CloudRestoreAS-linux, o junto al script.
SRC=""
for cand in \
"$SCRIPT_DIR/dist/$BIN_NAME" \
"$SCRIPT_DIR/dist/${BIN_NAME}-linux" \
"$SCRIPT_DIR/$BIN_NAME" \
"$SCRIPT_DIR/${BIN_NAME}-linux"; do
if [[ -f "$cand" ]]; then SRC="$cand"; break; fi
done
if [[ -z "$SRC" ]]; then
echo "ERROR: no se encontró el binario ($BIN_NAME). Ejecuta ./build.sh primero" >&2
exit 1
fi
echo "==============================================="
echo "CloudRestoreAS - Instalación Linux"
echo " Binario : $SRC"
echo " Destino : $PREFIX"
echo " Modo : $MODE"
echo "==============================================="
# --- Detener el servicio si está corriendo ------------------------------------
# Ojo con la razón, que estuvo mal escrita mucho tiempo: `install` NO sufre ETXTBSY. A
# diferencia de `cp` —que abre con O_TRUNC—, `install` desvincula el destino antes de crearlo
# (coreutils fija unlink_dest_before_opening), que es justo por lo que `make install` funciona
# sobre binarios en ejecución. Comprobado: `cp` sobre un ELF corriendo da "Text file busy" y
# `install` no. Lo que hace falta para reemplazarlo es permiso de escritura en el DIRECTORIO.
#
# Se detiene igual en los modos de servicio porque conviene un apagado ordenado, no porque la
# copia lo exija. Se recuerda si estaba activo para volver a levantarlo al final.
WAS_ACTIVE=0
# En modo usuario el unit vive en la instancia de systemd del propio usuario, que necesita
# XDG_RUNTIME_DIR: un `exec` de SSH no es una sesión de login y no siempre lo trae.
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
SYSTEMCTL_USER=(systemctl --user)
# En modo en-sitio NO se detiene nada: `install` reemplaza el binario con el proceso corriendo, y
# matarlo antes de tener el binario nuevo abriría una ventana en la que systemd relevanta el VIEJO.
if [[ "$MODE" != "update-in-place" ]] && command -v systemctl >/dev/null 2>&1; then
if [[ "$MODE" == "user-service" ]]; then
if "${SYSTEMCTL_USER[@]}" is-active --quiet "$UNIT_NAME" 2>/dev/null; then
WAS_ACTIVE=1
echo "Deteniendo $UNIT_NAME (usuario) para reemplazar el binario..."
"${SYSTEMCTL_USER[@]}" stop "$UNIT_NAME" >/dev/null 2>&1 || \
echo "AVISO: no se pudo detener $UNIT_NAME; la copia puede fallar." >&2
fi
# Sin systemd de usuario disponible el arranque pudo haber quedado por cron: el binario
# sigue vivo y sobrescribirlo daría ETXTBSY, así que se termina igual.
if [[ "$WAS_ACTIVE" -eq 0 ]] && pgrep -f "$PGREP_PAT" >/dev/null 2>&1; then
WAS_ACTIVE=1
echo "Terminando el proceso en curso para reemplazar el binario..."
pkill -f "$PGREP_PAT" >/dev/null 2>&1 || true
sleep 2
fi
elif systemctl is-active --quiet "$UNIT_NAME" 2>/dev/null; then
WAS_ACTIVE=1
echo "Deteniendo $UNIT_NAME para reemplazar el binario..."
systemctl stop "$UNIT_NAME" >/dev/null 2>&1 || \
echo "AVISO: no se pudo detener $UNIT_NAME (¿falta sudo?); la copia puede fallar." >&2
fi
fi
# Una restauración en curso no se interrumpe. El agente no atiende SIGTERM, así que matarlo a
# media restauración deja el job atascado —ese ZIP queda vetado en cada escaneo posterior— y puede
# dejar la base en SINGLE_USER. Cualquier subcarpeta de Temp/ es un job en vuelo.
# Código 75 (EX_TEMPFAIL) y no 1: le dice al panel "reintenta luego", no "falló la instalación".
if [[ "$MODE" == "update-in-place" ]] && [[ -d "$PREFIX/Temp" ]]; then
if [[ -n "$(ls -A "$PREFIX/Temp" 2>/dev/null)" ]]; then
echo "ERROR: hay una restauración en curso ($PREFIX/Temp no está vacío)." >&2
echo " No se actualiza para no dejarla a medias. Reintenta cuando termine." >&2
exit 75
fi
fi
# Respaldo para poder volver atrás. Una actualización en sitio reemplaza el binario de un
# servidor en producción sin red de seguridad: si el nuevo no arranca, sin esto el servidor queda
# sin agente y sin forma de recuperarlo salvo entrando a mano.
BACKUP=""
if [[ "$MODE" == "update-in-place" && -f "$PREFIX/$BIN_NAME" ]]; then
BACKUP="$PREFIX/.$BIN_NAME.prev"
if ! cp -p "$PREFIX/$BIN_NAME" "$BACKUP"; then
echo "ERROR: no se pudo respaldar el binario actual en $BACKUP" >&2
echo " Se aborta: actualizar sin poder revertir no es aceptable." >&2
exit 1
fi
echo "Respaldo del binario actual en $BACKUP"
fi
mkdir -p "$PREFIX"
if ! install -m 0755 "$SRC" "$PREFIX/$BIN_NAME"; then
echo "ERROR: no se pudo instalar el binario en $PREFIX/$BIN_NAME" >&2
# `install` desvincula el destino antes de crearlo, así que un binario EN USO no es el
# problema (eso es cosa de `cp`). Lo que falta casi siempre es permiso en el DIRECTORIO.
echo " Se necesita permiso de escritura en el directorio $PREFIX." >&2
echo " Dueño actual: $(stat -c '%U:%G %a' "$PREFIX" 2>/dev/null || echo 'desconocido')" >&2
echo " Usuario actual: $(id -un)" >&2
exit 1
fi
echo "Binario instalado en $PREFIX/$BIN_NAME"
# Bootstrap de config/: el propio binario crea config/, .env y carpetas de trabajo
# al inicio de su arranque. Se corre una vez en modo offscreen (sin display) y se
# corta con timeout; así el usuario ya puede editar config/.env antes de habilitar
# el servicio.
# En una actualización en sitio se OMITE: el binario hace ensure_runtime_layout() en cada arranque,
# así que es redundante, y correr una segunda instancia junto a la viva es peligroso —al arrancar,
# el motor purga todas las subcarpetas de Temp, que son de la instancia en curso—.
if [[ "$MODE" != "update-in-place" ]]; then
echo "Inicializando config/ (bootstrap)..."
# `</dev/null` no es cosmético: es el único hijo que heredaría el stdin del canal SSH cuando el
# instalador corre en remoto. Cerrárselo hace estructural —y no accidental— que nada de lo que
# venga por ese canal pueda ser consumido aquí.
( cd "$PREFIX" && QT_QPA_PLATFORM=offscreen timeout 20 "$PREFIX/$BIN_NAME" --headless </dev/null >/dev/null 2>&1 || true )
fi
if [[ -f "$PREFIX/config/.env" ]]; then
echo "config/.env creado."
else
echo "NOTA: config/.env se creará en la primera ejecución."
fi
# --- Siembra de credenciales del PANEL ----------------------------------------
# Fusión replace-or-append: respeta el resto de config/.env y es idempotente, así que
# reinstalar no duplica claves ni pierde ajustes locales. Se escribe vía archivo y no
# por argumentos para que el token no quede visible en `ps` ni en el historial.
merge_env_keys() {
local target="$1"; shift
local -a pairs=("$@")
local tmp line key pair matched
tmp="$(mktemp)"
if [[ -f "$target" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
matched=""
for pair in "${pairs[@]}"; do
key="${pair%%=*}"
if [[ "$line" =~ ^[[:space:]]*"$key"[[:space:]]*= ]]; then
matched="$pair"
break
fi
done
if [[ -n "$matched" ]]; then
printf '%s\n' "$matched" >> "$tmp"
else
printf '%s\n' "$line" >> "$tmp"
fi
done < "$target"
fi
for pair in "${pairs[@]}"; do
key="${pair%%=*}"
if ! grep -qE "^[[:space:]]*${key}[[:space:]]*=" "$tmp" 2>/dev/null; then
printf '%s\n' "$pair" >> "$tmp"
fi
done
# cat > preserva el inodo y los permisos 0600 del .env; mv los reemplazaría.
cat "$tmp" > "$target"
rm -f "$tmp"
}
if [[ -n "$PANEL_ENV_FILE" ]]; then
if [[ ! -f "$PANEL_ENV_FILE" ]]; then
echo "ERROR: no existe el archivo indicado en --panel-env-file: $PANEL_ENV_FILE" >&2
exit 1
fi
ENV_PATH="$PREFIX/config/.env"
touch "$ENV_PATH"
chmod 0600 "$ENV_PATH" 2>/dev/null || true
# Lista blanca: el archivo llega por la red y no debe poder inyectar otras claves.
ALLOWED_KEYS="CLOUDRESTORE_PANEL_API_URL CLOUDRESTORE_PANEL_API_TOKEN"
ALLOWED_KEYS="$ALLOWED_KEYS CLOUDRESTORE_PANEL_INSTANCE_KEY CLOUDRESTORE_PANEL_VERIFY_SSL"
PAIRS=()
while IFS= read -r raw || [[ -n "$raw" ]]; do
raw="${raw%$'\r'}" # tolera CRLF si vino de Windows
raw="${raw#"${raw%%[![:space:]]*}"}" # recorta espacios a la izquierda
if [[ -z "$raw" || "${raw:0:1}" == "#" || "$raw" != *=* ]]; then
continue
fi
key="${raw%%=*}"
key="${key//[[:space:]]/}"
value="${raw#*=}"
for allowed in $ALLOWED_KEYS; do
if [[ "$key" == "$allowed" ]]; then
PAIRS+=("${key}=${value}")
break
fi
done
done < "$PANEL_ENV_FILE"
if [[ "${#PAIRS[@]}" -gt 0 ]]; then
merge_env_keys "$ENV_PATH" "${PAIRS[@]}"
echo "${#PAIRS[@]} clave(s) del PANEL escritas en config/.env"
else
echo "AVISO: --panel-env-file no traía claves CLOUDRESTORE_PANEL_* válidas." >&2
fi
# El archivo trae el token en claro: se borra en cuanto se consumió.
rm -f "$PANEL_ENV_FILE"
fi
case "$MODE" in
service)
SERVICE_USER="${SERVICE_USER:-${SUDO_USER:-$(id -un)}}"
UNIT_SRC="$SCRIPT_DIR/packaging/linux/$UNIT_NAME"
UNIT_DST="/etc/systemd/system/$UNIT_NAME"
if [[ ! -w "$(dirname "$UNIT_DST")" ]]; then
echo "ERROR: se requiere sudo para instalar el servicio systemd" >&2
exit 1
fi
sed -e "s|__USER__|$SERVICE_USER|g" \
-e "s|__WORKDIR__|$PREFIX|g" \
-e "s|__EXEC__|$PREFIX/$BIN_NAME|g" \
"$UNIT_SRC" > "$UNIT_DST"
systemctl daemon-reload
systemctl enable --now "$UNIT_NAME"
echo "Servicio systemd instalado y arrancado (usuario: $SERVICE_USER)."
echo " Estado : systemctl status cloudrestoreas"
echo " Logs : journalctl -u cloudrestoreas -f"
;;
user-service)
# Sin root: unit en el home y arranque con la instancia de systemd del propio usuario.
# Requisito previo: PREFIX escribible por este usuario (el panel lo apunta a su home).
UNIT_SRC="$SCRIPT_DIR/packaging/linux/cloudrestoreas-user.service"
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
UNIT_DST="$UNIT_DIR/$UNIT_NAME"
if [[ ! -f "$UNIT_SRC" ]]; then
echo "ERROR: falta la plantilla del unit de usuario ($UNIT_SRC)" >&2
exit 1
fi
mkdir -p "$UNIT_DIR"
# Sin __USER__: un unit de usuario no admite User=, ya corre como su dueño.
sed -e "s|__WORKDIR__|$PREFIX|g" \
-e "s|__EXEC__|$PREFIX/$BIN_NAME|g" \
"$UNIT_SRC" > "$UNIT_DST"
STARTED=""
if "${SYSTEMCTL_USER[@]}" daemon-reload >/dev/null 2>&1 && \
"${SYSTEMCTL_USER[@]}" enable --now "$UNIT_NAME" >/dev/null 2>&1; then
STARTED="systemd-usuario"
# Sin lingering, la instancia de usuario muere al cerrar la última sesión y con ella el
# restaurador. Muchos polkit permiten habilitarlo para uno mismo; si no, no es fatal:
# se complementa con cron más abajo.
if loginctl enable-linger "$(id -un)" >/dev/null 2>&1; then
echo "Lingering habilitado: el servicio sobrevive al cierre de sesión."
else
echo "AVISO: no se pudo habilitar lingering; se agrega respaldo por cron." >&2
STARTED="systemd-usuario+cron"
fi
else
echo "AVISO: systemd de usuario no disponible; se usará cron." >&2
fi
# Respaldo (o sustituto) por cron: @reboot para el arranque y un vigilante cada 5 min que
# cubre lo que en systemd hace Restart=always. El crontab del usuario no requiere root.
if [[ "$STARTED" != "systemd-usuario" ]]; then
if command -v crontab >/dev/null 2>&1; then
CRON_CMD="cd $PREFIX && QT_QPA_PLATFORM=offscreen $PREFIX/$BIN_NAME --start-engine --headless >/dev/null 2>&1 &"
CRON_TAG="# cloudrestoreas-autostart"
# Se reescribe el crontab completo filtrando las entradas propias anteriores, para que
# reinstalar no acumule duplicados.
{
crontab -l 2>/dev/null | grep -v "$CRON_TAG" || true
echo "@reboot $CRON_CMD $CRON_TAG"
echo "*/5 * * * * pgrep -f '$PGREP_PAT' >/dev/null || ($CRON_CMD) $CRON_TAG"
} | crontab -
STARTED="${STARTED:+$STARTED+}cron"
# El vigilante tarda hasta 5 min en actuar; se arranca ya para no dejar el hueco.
if ! pgrep -f "$PGREP_PAT" >/dev/null 2>&1; then
( cd "$PREFIX" && QT_QPA_PLATFORM=offscreen "$PREFIX/$BIN_NAME" --start-engine --headless </dev/null >/dev/null 2>&1 & )
fi
else
echo "ERROR: sin systemd de usuario y sin crontab no hay forma de dejarlo arrancado." >&2
echo " Instala cron, habilita lingering, o usa --service con privilegios." >&2
exit 1
fi
fi
echo "Servicio de usuario instalado y arrancado (mecanismo: $STARTED)."
echo " Estado : systemctl --user status cloudrestoreas"
echo " Logs : journalctl --user -u cloudrestoreas -f"
;;
update-in-place)
# El unit ya está registrado y no cambia: no se toca /etc, ni daemon-reload, ni enable.
# El binario nuevo ya está en su sitio; falta que el proceso lo tome.
if ! pgrep -f "$PGREP_PAT" >/dev/null 2>&1; then
echo "El agente no estaba corriendo; queda actualizado y systemd lo levantará."
else
# Segundo chequeo, inmediatamente antes de señalizar. El primero fue antes de copiar el
# binario y de sembrar el .env; en esos segundos pudo entrar una restauración, y matarla
# deja el respaldo vetado para siempre. Aquí ya no hay 270 MB de por medio: es barato.
if [[ -d "$PREFIX/Temp" ]] && [[ -n "$(ls -A "$PREFIX/Temp" 2>/dev/null)" ]]; then
echo "ERROR: entró una restauración mientras se actualizaba." >&2
echo " El binario nuevo YA está instalado y se activará en el próximo reinicio" >&2
echo " del agente; no se fuerza ahora para no interrumpirla." >&2
exit 75
fi
# `|| true` obligatorio: con pipefail, un pgrep sin resultados hace fallar la
# sustitución y `set -e` abortaría el script justo en el caso que hay que manejar.
OLD_PIDS="$(pgrep -f "$PGREP_PAT" | tr '\n' ' ' || true)"
echo "Señalizando al agente (PIDs: $OLD_PIDS) para que systemd lo relevante..."
# SIGTERM y no SIGKILL: si algún día el agente aprende a atender la señal, este camino ya
# le da la oportunidad de cerrar limpio.
pkill -TERM -f "$PGREP_PAT" >/dev/null 2>&1 || true
# Restart=always + RestartSec=5. Se espera con margen y se confirma que volvió: si el unit
# no tuviera Restart, matarlo lo dejaría muerto y eso NO puede pasar por bueno.
RESTARTED=0
for _ in $(seq 1 15); do
sleep 2
NEW_PID="$(pgrep -f "$PGREP_PAT" | head -1 || true)"
if [[ -n "$NEW_PID" ]] && [[ " $OLD_PIDS " != *" $NEW_PID "* ]]; then
RESTARTED=1
echo "Agente relevantado por systemd (PID $NEW_PID)."
break
fi
done
if [[ "$RESTARTED" -eq 0 ]]; then
# No volvió. Dejar el servidor sin agente no es una opción: se revierte al binario que
# sí funcionaba y se le da otra oportunidad a systemd.
echo "AVISO: el agente no volvió tras la señal; revirtiendo al binario anterior..." >&2
UNIT_STATE="$(systemctl is-active "$UNIT_NAME" 2>/dev/null || true)"
UNIT_RESTART="$(systemctl show -p Restart --value "$UNIT_NAME" 2>/dev/null || true)"
REVERTED=0
if [[ -n "$BACKUP" && -f "$BACKUP" ]]; then
if install -m 0755 "$BACKUP" "$PREFIX/$BIN_NAME"; then
for _ in $(seq 1 10); do
sleep 2
if pgrep -f "$PGREP_PAT" >/dev/null 2>&1; then REVERTED=1; break; fi
done
fi
fi
echo "ERROR: la actualización no dejó al agente corriendo." >&2
echo " Estado del unit: ${UNIT_STATE:-desconocido} (Restart=${UNIT_RESTART:-desconocido})" >&2
if [[ "${UNIT_RESTART}" != "always" ]]; then
echo " El unit no tiene Restart=always: nadie lo relevanta al terminar." >&2
elif [[ "$UNIT_STATE" == "failed" ]]; then
echo " systemd lo marcó como failed; puede haber agotado StartLimitBurst." >&2
echo " Reintentar con: systemctl reset-failed $UNIT_NAME && systemctl start $UNIT_NAME" >&2
fi
if [[ "$REVERTED" -eq 1 ]]; then
echo " REVERTIDO: el binario anterior está corriendo de nuevo. El servidor" >&2
echo " quedó como estaba; la versión nueva NO se aplicó." >&2
exit 1
fi
echo " NO se pudo revertir. El binario nuevo está en $PREFIX/$BIN_NAME y el" >&2
echo " anterior en ${BACKUP:-(sin respaldo)}. Hay que revisar el servidor a mano." >&2
exit 1
fi
# Solo tras confirmar que el agente nuevo corre se descarta el respaldo.
[[ -n "$BACKUP" && -f "$BACKUP" ]] && rm -f "$BACKUP" || true
fi
;;
desktop)
echo "Modo escritorio: la app registra su autostart .desktop al iniciarse."
echo "Ejecuta '$PREFIX/$BIN_NAME' en tu sesión gráfica."
;;
none)
echo "Instalación sin arranque automático."
echo "Ejecuta: QT_QPA_PLATFORM=offscreen $PREFIX/$BIN_NAME --start-engine --headless"
;;
esac
# Si se detuvo un servicio que estaba activo y el modo elegido no lo relevanta, se
# restaura: una actualización no debe dejar el restaurador apagado en silencio.
if [[ "$WAS_ACTIVE" -eq 1 && "$MODE" != "service" && "$MODE" != "user-service" \
&& "$MODE" != "update-in-place" ]]; then
echo "Reiniciando $UNIT_NAME (estaba activo antes de la actualización)..."
systemctl start "$UNIT_NAME" >/dev/null 2>&1 || \
echo "AVISO: no se pudo reiniciar $UNIT_NAME; hazlo a mano." >&2
fi
echo ""
echo "Siguiente paso: edita $PREFIX/config/.env (CLOUDRESTORE_PANEL_*) y reinicia."
[[ "$MODE" == "service" ]] && echo " Tras editar: sudo systemctl restart cloudrestoreas"
[[ "$MODE" == "user-service" ]] && echo " Tras editar: systemctl --user restart cloudrestoreas"
echo "Listo."