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.
452 lines
21 KiB
Bash
Executable File
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."
|