feat(install): modo --user-service, instalación sin privilegios
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.
This commit is contained in:
103
install.sh
103
install.sh
@@ -9,9 +9,17 @@
|
||||
#
|
||||
# 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 --desktop # autostart de escritorio (requiere sesión gráfica)
|
||||
# ./install.sh # solo instala; sin arranque automático
|
||||
#
|
||||
# --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.
|
||||
@@ -29,6 +37,7 @@ PANEL_ENV_FILE=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--service) MODE="service"; shift ;;
|
||||
--user-service) MODE="user-service"; shift ;;
|
||||
--desktop) MODE="desktop"; shift ;;
|
||||
--panel-env-file)
|
||||
[[ $# -ge 2 ]] || { echo "ERROR: --panel-env-file requiere una ruta" >&2; exit 2; }
|
||||
@@ -44,6 +53,10 @@ 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=""
|
||||
@@ -71,8 +84,27 @@ echo "==============================================="
|
||||
# tiene que detenerlo antes de reemplazar el binario. Se recuerda si estaba activo
|
||||
# para volver a levantarlo al final incluso en los modos que no tocan systemd.
|
||||
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)
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
if systemctl is-active --quiet "$UNIT_NAME" 2>/dev/null; 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 || \
|
||||
@@ -94,7 +126,10 @@ echo "Binario instalado en $PREFIX/$BIN_NAME"
|
||||
# corta con timeout; así el usuario ya puede editar config/.env antes de habilitar
|
||||
# el servicio.
|
||||
echo "Inicializando config/ (bootstrap)..."
|
||||
( cd "$PREFIX" && QT_QPA_PLATFORM=offscreen timeout 20 "$PREFIX/$BIN_NAME" --headless >/dev/null 2>&1 || true )
|
||||
# `</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 )
|
||||
if [[ -f "$PREFIX/config/.env" ]]; then
|
||||
echo "config/.env creado."
|
||||
else
|
||||
@@ -203,6 +238,67 @@ case "$MODE" in
|
||||
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"
|
||||
;;
|
||||
desktop)
|
||||
echo "Modo escritorio: la app registra su autostart .desktop al iniciarse."
|
||||
echo "Ejecuta '$PREFIX/$BIN_NAME' en tu sesión gráfica."
|
||||
@@ -215,7 +311,7 @@ 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" ]]; then
|
||||
if [[ "$WAS_ACTIVE" -eq 1 && "$MODE" != "service" && "$MODE" != "user-service" ]]; 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
|
||||
@@ -224,4 +320,5 @@ 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."
|
||||
|
||||
Reference in New Issue
Block a user