#!/usr/bin/env bash # # Installe (ou met à jour) l'agent Stream Control sur une VM Linux. # # curl -fsSL https://git.jeanbonapp.com/api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.sh \ # | sudo bash -s -- \ # --registry https://git.jeanbonapp.com \ # --server ws://:8080/ws/agent \ # --token # # Réexécuter le script met à jour le binaire sans toucher à l'identité d'un # agent déjà enrôlé. set -euo pipefail REGISTRY="" OWNER="jeanbon" VERSION="latest" PACKAGE_NAME="stream-control-agent" PACKAGE_TOKEN="" # Vide = option non fournie. Les valeurs par défaut ne s'appliquent qu'à une # première installation ; sur une mise à jour, seules les options réellement # passées écrasent la configuration existante. SERVER_URL="" TOKEN="" AGENT_NAME="" OBS_HOST="" OBS_PORT="" OBS_PASSWORD="" OBS_PASSWORD_SET=false # un mot de passe vide est une valeur légitime RESET_IDENTITY=false RUN_USER="" INSTALL_DIR="/opt/stream-control-agent" SERVICE="stream-control-agent" die() { echo "✗ $*" >&2; exit 1; } info() { echo "· $*"; } usage() { cat <<'EOF' Installe (ou met à jour) l'agent Stream Control sur une VM Linux. curl -fsSL https://git.jeanbonapp.com/api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.sh \ | sudo bash -s -- \ --registry https://git.jeanbonapp.com \ --server ws://:8080/ws/agent \ --token Réexécuter le script met à jour le binaire et applique les options fournies. L'identité de l'agent (jeton permanent, agentId) est préservée : sans cela il réapparaîtrait en double dans le dashboard. Options : --registry URL Base de l'instance Gitea (obligatoire) --server URL WebSocket du plan de contrôle (obligatoire à la 1re install) --token JETON Jeton d'enrôlement (obligatoire à la 1re install) --name NOM Nom affiché dans le dashboard (défaut : hostname) --user UTILISATEUR Compte exécutant l'agent (défaut : l'utilisateur sudo) --obs-host HOTE Défaut 127.0.0.1 --obs-port PORT Défaut 4455 --obs-password MDP Mot de passe obs-websocket --reset-identity Réenrôler avec le --token fourni (efface l'identité existante — crée un nouvel agent dans le dashboard) --version VERSION Version du paquet à installer (défaut : latest) --owner PROPRIETAIRE Propriétaire du paquet Gitea (défaut : jeanbon) --package-token JETON Jeton de lecture si le paquet est privé --help EOF exit 0 } while [ $# -gt 0 ]; do case "$1" in --registry) REGISTRY="${2:?}"; shift 2 ;; --server) SERVER_URL="${2:?}"; shift 2 ;; --token) TOKEN="${2:?}"; shift 2 ;; --name) AGENT_NAME="${2:?}"; shift 2 ;; --user) RUN_USER="${2:?}"; shift 2 ;; --obs-host) OBS_HOST="${2:?}"; shift 2 ;; --obs-port) OBS_PORT="${2:?}"; shift 2 ;; --obs-password) OBS_PASSWORD="${2-}"; OBS_PASSWORD_SET=true; shift 2 ;; --reset-identity) RESET_IDENTITY=true; shift ;; --version) VERSION="${2:?}"; shift 2 ;; --owner) OWNER="${2:?}"; shift 2 ;; --package-token) PACKAGE_TOKEN="${2:?}"; shift 2 ;; --help|-h) usage ;; *) die "Option inconnue : $1 (--help pour l'aide)" ;; esac done [ "$(id -u)" -eq 0 ] || die "À exécuter en root (sudo)." [ -n "$REGISTRY" ] || die "--registry est obligatoire." REGISTRY="${REGISTRY%/}" # L'agent doit tourner sous le compte qui ouvre la session graphique d'OBS : # c'est ce qui lui donne accès à l'affichage pour le rappel plein écran. if [ -z "$RUN_USER" ]; then RUN_USER="${SUDO_USER:-root}" fi id "$RUN_USER" >/dev/null 2>&1 || die "L'utilisateur « $RUN_USER » n'existe pas." CONFIG_FILE="$INSTALL_DIR/agent.config.json" IS_UPGRADE=false [ -f "$CONFIG_FILE" ] && IS_UPGRADE=true if [ "$IS_UPGRADE" = false ]; then [ -n "$SERVER_URL" ] || die "--server est obligatoire pour une première installation." [ -n "$TOKEN" ] || die "--token est obligatoire pour une première installation." # Les défauts ne s'appliquent qu'ici : sur une mise à jour, une option absente # doit laisser la valeur existante intacte, pas la remplacer par un défaut. [ -n "$AGENT_NAME" ] || AGENT_NAME="$(hostname)" [ -n "$OBS_HOST" ] || OBS_HOST="127.0.0.1" [ -n "$OBS_PORT" ] || OBS_PORT="4455" fi echo echo "Installation de l'agent Stream Control" echo " utilisateur : $RUN_USER" echo " destination : $INSTALL_DIR" echo " version : $VERSION" echo # --- Node.js ---------------------------------------------------------------- NODE_MAJOR=0 if command -v node >/dev/null 2>&1; then NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)" fi if [ "$NODE_MAJOR" -lt 22 ]; then info "Node.js 22+ requis (trouvé : ${NODE_MAJOR:-aucun}) — installation depuis NodeSource" command -v apt-get >/dev/null 2>&1 \ || die "Distribution non gérée automatiquement : installe Node.js 22+ puis relance." curl -fsSL https://deb.nodesource.com/setup_24.x | bash - apt-get install -y nodejs else info "Node.js $(node -v) présent" fi # --- Prérequis du rappel plein écran ---------------------------------------- # Firefox est le prérequis du mode de pilotage « WebDriver BiDi », le seul qui # fonctionne en session Wayland. xdotool ne sert plus qu'au mode historique. if ! command -v firefox >/dev/null 2>&1; then echo "! Firefox absent : le pilotage « WebDriver BiDi » sera indisponible." echo " Installe-le sur cette VM (snap install firefox, ou apt install firefox)." fi if ! command -v xdotool >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then info "Installation de xdotool (mode de pilotage historique)" apt-get install -y xdotool >/dev/null else echo "! xdotool absent : seul le pilotage « WebDriver BiDi » sera disponible." fi fi # --- Téléchargement du bundle ----------------------------------------------- URL="$REGISTRY/api/packages/$OWNER/generic/$PACKAGE_NAME/$VERSION/agent.cjs" info "Téléchargement depuis $URL" mkdir -p "$INSTALL_DIR" CURL_AUTH=() [ -n "$PACKAGE_TOKEN" ] && CURL_AUTH=(--header "Authorization: token $PACKAGE_TOKEN") # Le fichier temporaire doit garder l'extension .cjs : `node --check` déduit le # format du module de l'extension et rejette tout ce qu'il ne reconnaît pas # (« Unknown file extension .tmp »), quel que soit le contenu. TMP_FILE="$INSTALL_DIR/agent-new.cjs" curl -fsSL "${CURL_AUTH[@]}" -o "$TMP_FILE" "$URL" \ || die "Téléchargement impossible. Vérifie --registry, --version, et --package-token si le paquet est privé." if ! CHECK_ERR="$(node --check "$TMP_FILE" 2>&1)"; then echo "$CHECK_ERR" >&2 echo " (fichier conservé pour inspection : $TMP_FILE)" >&2 die "Le fichier téléchargé n'est pas un script Node valide — voir l'erreur ci-dessus." fi mv "$TMP_FILE" "$INSTALL_DIR/agent.cjs" # --- Configuration ---------------------------------------------------------- if [ "$IS_UPGRADE" = true ]; then # Fusion, et non conservation en bloc : l'identité (jeton, agentId) est # préservée — l'écraser ferait réapparaître l'agent en double dans le # dashboard — mais toute option explicitement passée doit s'appliquer. RESET_FLAG=0 [ "$RESET_IDENTITY" = true ] && RESET_FLAG=1 PW_FLAG=0 [ "$OBS_PASSWORD_SET" = true ] && PW_FLAG=1 node -e ' const fs = require("node:fs"); const a = process.argv.slice(1); const [file, serverUrl, name, obsHost, obsPort, pwFlag, obsPassword, resetFlag, token, packageUrl] = a; const current = JSON.parse(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "")); const next = { ...current, obs: { ...(current.obs || {}) } }; const applied = []; // Toujours rafraichie : infrastructure et non identite. Sans elle, un agent // ne saurait pas ou chercher sa propre mise a jour. // (Pas d apostrophe dans ce bloc : il vit dans une chaine bash entre quotes.) if (packageUrl && packageUrl !== current.packageUrl) { next.packageUrl = packageUrl; applied.push("packageUrl"); } if (serverUrl) { next.serverUrl = serverUrl; applied.push("serverUrl"); } if (name) { next.name = name; applied.push("name"); } if (obsHost) { next.obs.host = obsHost; applied.push("obs.host"); } if (obsPort) { next.obs.port = Number(obsPort); applied.push("obs.port"); } if (pwFlag === "1") { next.obs.password = obsPassword; applied.push("obs.password"); } if (resetFlag === "1" && token) { next.token = token; delete next.agentId; applied.push("identité réinitialisée"); } else if (token && token !== current.token) { console.log(" (--token ignoré : identité conservée. Utilise --reset-identity pour forcer.)"); } fs.writeFileSync(file, JSON.stringify(next, null, 2) + "\n"); console.log(applied.length ? " champs mis à jour : " + applied.join(", ") : " aucun changement demandé"); ' "$CONFIG_FILE" "$SERVER_URL" "$AGENT_NAME" "$OBS_HOST" "$OBS_PORT" \ "$PW_FLAG" "$OBS_PASSWORD" "$RESET_FLAG" "$TOKEN" "$URL" \ || die "Fusion de la configuration existante impossible ($CONFIG_FILE)." info "Configuration mise à jour, identité de l'agent préservée" else umask 077 cat > "$CONFIG_FILE" < "/etc/systemd/system/$SERVICE.service" <, Wayland lui donne # un suffixe aléatoire) et change d'une session à l'autre. L'agent le résout à # l'exécution. En déclarer un ici serait pire que rien : un ~/.Xauthority # périmé, laissé par une ancienne session, serait retenu de préférence au bon. Environment=DISPLAY=:0 ExecStart=$(command -v node) $INSTALL_DIR/agent.cjs Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable "$SERVICE" >/dev/null 2>&1 || true systemctl restart "$SERVICE" # --- Vérification ----------------------------------------------------------- echo # Mêmes variables que l'unité systemd, sinon le diagnostic signale un DISPLAY # absent que le service, lui, aura bien. Le cookie X est résolu par l'agent. CHECK_OK=true sudo -u "$RUN_USER" \ AGENT_CONFIG="$CONFIG_FILE" \ DISPLAY=":0" \ "$(command -v node)" "$INSTALL_DIR/agent.cjs" --check || CHECK_OK=false sleep 2 if systemctl is-active --quiet "$SERVICE"; then if [ "$CHECK_OK" = true ]; then echo "✓ Agent installé et démarré." else echo "✓ Agent installé et démarré — mais le diagnostic ci-dessus signale un" echo " problème bloquant. L'agent retentera en boucle jusqu'à sa résolution ;" echo " il n'apparaîtra pas dans le dashboard d'ici là." fi else echo "✗ Le service n'est pas actif. Journal :" journalctl -u "$SERVICE" -n 20 --no-pager || true exit 1 fi cat <