#!/usr/bin/env bash # ============================================================================= # install-fxserver.sh -- v1.1.0 # FiveM / FXServer Linux Installer # # Richtet ein: # - systemd-Unit fuer den FXServer # - update-fxserver.sh (automatischer Artefakt-Updater mit Rueckweg) # - Cron-Eintrag fuer planmaessige Updates (optional) # # Es wird nichts geschrieben, bevor die Zusammenfassung bestaetigt wurde. # # Aufruf als root: sudo ./install-fxserver.sh # Doku: https://www.musiker15.de/de/docs/fivem # ============================================================================= set -euo pipefail # ── Bash-Version pruefen (namerefs brauchen 4.3+) ───────────────────────────── if (( BASH_VERSINFO[0] < 4 || ( BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3 ) )); then echo "Fehler: Bash 4.3 oder neuer noetig (aktuell: $BASH_VERSION)." >&2 exit 1 fi # ── Farben ──────────────────────────────────────────────────────────────────── C_RED='\033[0;31m'; C_GRN='\033[0;32m'; C_YEL='\033[1;33m' C_CYN='\033[0;36m'; C_BLD='\033[1m'; C_DIM='\033[2m'; C_RST='\033[0m' log() { printf "${C_GRN} + ${C_RST}%s\n" "$*"; } info() { printf "${C_CYN} > ${C_RST}%s\n" "$*"; } warn() { printf "${C_YEL} ! ${C_RST}%s\n" "$*"; } err() { printf "${C_RED} x ${C_RST}%s\n" "$*" >&2; } die() { err "$*"; exit 1; } blank() { echo; } div() { printf "${C_DIM}--------------------------------------------------${C_RST}\n"; } step() { blank; div; printf "${C_BLD}${C_CYN} %s${C_RST}\n" "$*"; div; } # ── Eingabehilfen ───────────────────────────────────────────────────────────── ask_var() { local -n _ref="$1" local q="$2" dflt="${3:-}" local hint=""; [[ -n "$dflt" ]] && hint=" ${C_DIM}[${dflt}]${C_RST}" while true; do printf "${C_BLD} > %s${C_RST}%b\n ${C_DIM}>${C_RST} " "$q" "$hint" read -r _ref _ref="${_ref:-$dflt}" [[ -n "$_ref" ]] && break || warn "Eingabe darf nicht leer sein." done } yesno() { local q="$1" dflt="${2:-Y}" local opts [[ "${dflt^^}" == "Y" ]] && opts="${C_BLD}J${C_RST}/n" || opts="j/${C_BLD}N${C_RST}" printf "${C_BLD} > %s [%b]: ${C_RST}" "$q" "$opts" read -r yn yn="${yn:-$dflt}" case "${yn,,}" in j|ja|y|yes) return 0 ;; *) return 1 ;; esac } choose() { # choose local -n _sel="$1" local max="$2" prompt="$3" while true; do printf " ${C_BLD}%s [1-%s]: ${C_RST}" "$prompt" "$max" read -r _sel [[ "$_sel" =~ ^[0-9]+$ ]] && (( _sel >= 1 && _sel <= max )) && break warn "Bitte eine Zahl zwischen 1 und ${max} eingeben." done } # ── Konfigurationsvariablen ─────────────────────────────────────────────────── CFG_SERVER_DIR="" CFG_DATA_DIR="" CFG_USER="" CFG_GROUP="" CFG_SERVICE="" CFG_START_MODE="" # txadmin | direct CFG_SERVER_CFG="" CFG_TXADMIN_PORT="40120" CFG_UFW_DENY="no" CFG_UPDATE_BIN="" CFG_UPDATE_CHANNEL="" CFG_UPDATE_LOG="" CFG_CRON_ENABLE="no" CFG_CRON_EXPR="" CRON_MARKER="# managed-by: fxserver-install.sh" # ============================================================================= # WILLKOMMEN # ============================================================================= welcome() { clear printf "${C_BLD}${C_CYN}" cat << 'BANNER' ######## #### # # ###### ## ## ## ## ## ## ## ### ### ###### ## ## ## #### ## ### ## ## ## #### ## ## # ## ## #### ## ###### ## ## FXServer Linux Installer v1.1.0 BANNER printf "${C_RST}" info "Dieses Script richtet ein:" printf " ${C_DIM}-${C_RST} systemd-Unit fuer den FXServer\n" printf " ${C_DIM}-${C_RST} update-fxserver.sh (Artefakt-Updater mit Rueckweg)\n" printf " ${C_DIM}-${C_RST} Cron-Eintrag fuer planmaessige Updates (optional)\n" blank warn "Es wird nichts geaendert, bevor Du die Zusammenfassung bestaetigst." blank printf " ${C_BLD}Enter${C_RST} zum Fortfahren ..." read -r _ } # ============================================================================= # SCHRITT 0 -- Vorbedingungen # ============================================================================= preflight() { step "Schritt 0 / 6 -- Vorbedingungen" [[ $EUID -eq 0 ]] || die "Dieses Script muss als root laufen -> sudo $0" log "root-Rechte vorhanden." for bin in curl tar xz systemctl crontab; do if command -v "$bin" &>/dev/null; then log "$bin gefunden." else die "Benoetigtes Programm fehlt: ${bin} -> apt install ${bin}" fi done if ! command -v jq &>/dev/null; then warn "jq nicht gefunden (empfohlen fuer den Updater)." if yesno "jq jetzt installieren? (apt install jq)"; then apt-get install -y -q jq && log "jq installiert." \ || warn "Installation fehlgeschlagen, grep-Rueckfallweg wird genutzt." fi else log "jq gefunden." fi } # ============================================================================= # SCHRITT 1 -- Verzeichnisse # ============================================================================= q_dirs() { step "Schritt 1 / 6 -- Verzeichnisse" info "Artefakt-Verzeichnis: enthaelt run.sh und alpine/." info "NICHT das Verzeichnis mit Deinen resources/ und der server.cfg." blank while true; do ask_var CFG_SERVER_DIR "Pfad zum Artefakt-Verzeichnis" "/opt/fivem/server" CFG_SERVER_DIR="${CFG_SERVER_DIR%/}" if [[ -d "$CFG_SERVER_DIR" ]]; then if [[ -f "$CFG_SERVER_DIR/run.sh" || -d "$CFG_SERVER_DIR/alpine" ]]; then log "Verzeichnis gefunden, enthaelt FXServer-Dateien."; break else warn "Verzeichnis existiert, enthaelt aber kein run.sh / alpine/." yesno "Trotzdem verwenden?" "n" && break || continue fi else warn "Verzeichnis nicht gefunden: $CFG_SERVER_DIR" if yesno "Jetzt anlegen? (mkdir -p)"; then mkdir -p "$CFG_SERVER_DIR" && log "Verzeichnis angelegt." && break fi fi done blank info "Server-Daten: resources/, server.cfg, cache/. Wird nie ueberschrieben." local parent; parent="$(dirname "$CFG_SERVER_DIR")" ask_var CFG_DATA_DIR "Pfad zum Server-Daten-Verzeichnis" "${parent}/server-data" CFG_DATA_DIR="${CFG_DATA_DIR%/}" if [[ "$CFG_DATA_DIR" == "$CFG_SERVER_DIR" ]]; then die "Artefakt- und Daten-Verzeichnis duerfen nicht identisch sein. Ein Update wuerde sonst Deine Ressourcen ueberschreiben." fi [[ -d "$CFG_DATA_DIR" ]] && log "Daten-Verzeichnis gefunden." \ || warn "Daten-Verzeichnis existiert noch nicht." } # ============================================================================= # SCHRITT 2 -- Benutzer und Gruppe # ============================================================================= q_user() { step "Schritt 2 / 6 -- Benutzer und Gruppe" info "Unter diesem Konto laeuft der FXServer-Prozess. Niemals root." blank while true; do ask_var CFG_USER "Linux-Benutzer fuer den FiveM-Server" "fivem" if id "$CFG_USER" &>/dev/null; then log "Benutzer '${CFG_USER}' existiert."; break else warn "Benutzer '${CFG_USER}' existiert nicht." if yesno "'${CFG_USER}' als Systembenutzer anlegen?"; then if useradd --system --no-create-home --shell /usr/sbin/nologin "$CFG_USER" 2>/dev/null; then log "Benutzer '${CFG_USER}' angelegt."; break else err "Anlegen fehlgeschlagen."; continue fi fi fi done blank ask_var CFG_GROUP "Linux-Gruppe" "$CFG_USER" if getent group "$CFG_GROUP" &>/dev/null; then log "Gruppe '${CFG_GROUP}' gefunden." else warn "Gruppe '${CFG_GROUP}' existiert nicht." if yesno "Gruppe '${CFG_GROUP}' anlegen?"; then groupadd "$CFG_GROUP" && log "Gruppe '${CFG_GROUP}' angelegt." fi fi } # ============================================================================= # SCHRITT 3 -- systemd-Unit # ============================================================================= q_service() { step "Schritt 3 / 6 -- systemd-Unit" blank ask_var CFG_SERVICE "Name der systemd-Unit" "fivem" if [[ -f "/etc/systemd/system/${CFG_SERVICE}.service" ]]; then warn "'${CFG_SERVICE}.service' existiert bereits -> wird ueberschrieben." fi blank info "Startmodus:" printf " ${C_CYN}1)${C_RST} ${C_BLD}txAdmin${C_RST} - run.sh ohne +exec ${C_DIM}(txAdmin laedt die server.cfg)${C_RST}\n" printf " ${C_CYN}2)${C_RST} ${C_BLD}Direkt${C_RST} - run.sh +exec /pfad/zur/server.cfg\n" blank local _m; choose _m 2 "Auswahl" case "$_m" in 1) CFG_START_MODE="txadmin"; log "Modus: txAdmin" ;; 2) CFG_START_MODE="direct"; log "Modus: Direkt" ;; esac if [[ "$CFG_START_MODE" == "direct" ]]; then blank while true; do ask_var CFG_SERVER_CFG "Pfad zur server.cfg" "${CFG_DATA_DIR}/server.cfg" if [[ -f "$CFG_SERVER_CFG" ]]; then log "server.cfg gefunden."; break else warn "Datei nicht gefunden: $CFG_SERVER_CFG" yesno "Pfad trotzdem verwenden? (Datei kann spaeter entstehen)" "n" && break || continue fi done else blank ask_var CFG_TXADMIN_PORT "Port fuer das txAdmin-Panel" "40120" blank warn "txAdmin laesst sich NICHT allein auf localhost binden." info "Die Interface-Einstellung (txAdminInterface / TXHOST_INTERFACE)" info "zwingt auch den FXServer auf dasselbe Interface. Der Spielserver" info "waere danach fuer niemanden mehr erreichbar." info "Port ${CFG_TXADMIN_PORT} wird deshalb ueber die Firewall geschlossen." blank if command -v ufw >/dev/null 2>&1; then if yesno "Am Ende 'ufw deny ${CFG_TXADMIN_PORT}/tcp' setzen?"; then CFG_UFW_DENY="yes" else CFG_UFW_DENY="no" warn "Dann bitte selbst dafuer sorgen, dass ${CFG_TXADMIN_PORT} zu ist." fi else CFG_UFW_DENY="no" warn "ufw nicht gefunden. Port ${CFG_TXADMIN_PORT} selbst schliessen." fi fi } # ============================================================================= # SCHRITT 4 -- Update-Script # ============================================================================= q_update() { step "Schritt 4 / 6 -- Update-Script" blank ask_var CFG_UPDATE_BIN "Zielpfad fuer das Update-Script" "/usr/local/bin/update-fxserver.sh" [[ -f "$CFG_UPDATE_BIN" ]] && warn "'${CFG_UPDATE_BIN}' existiert bereits -> wird ueberschrieben." blank info "Update-Kanal:" printf " ${C_CYN}1)${C_RST} ${C_BLD}recommended${C_RST} - geprueft ${C_DIM}(richtig fuer Produktivsysteme)${C_RST}\n" printf " ${C_CYN}2)${C_RST} ${C_BLD}latest${C_RST} - neuester ${C_DIM}(kann Inkompatibilitaeten enthalten)${C_RST}\n" blank local _ch; choose _ch 2 "Auswahl" case "$_ch" in 1) CFG_UPDATE_CHANNEL="recommended"; log "Kanal: recommended" ;; 2) CFG_UPDATE_CHANNEL="latest" warn "'latest' auf einem Produktivsystem holt ungeprueft Neues." log "Kanal: latest" ;; esac blank ask_var CFG_UPDATE_LOG "Pfad zur Logdatei des Updaters" "/var/log/fxserver-update.log" } # ============================================================================= # SCHRITT 5 -- Cron # ============================================================================= q_cron() { step "Schritt 5 / 6 -- Planmaessige Updates (Cron)" blank info "Der Cron laeuft als root, stoppt den Server, tauscht das Artefakt" info "und startet neu. Ein Update ist also ein Neustart ohne Vorwarnung." warn "Zeitfenster deshalb in die Nacht legen, nicht in die Hauptzeit." blank if ! yesno "Cron-Eintrag fuer automatische Updates anlegen?"; then CFG_CRON_ENABLE="no" info "Es wird kein Cron-Eintrag angelegt." return fi CFG_CRON_ENABLE="yes" blank info "Zeitplan:" printf " ${C_CYN}1)${C_RST} Montags 05:00 ${C_DIM}0 5 * * 1${C_RST} ${C_DIM}(empfohlen)${C_RST}\n" printf " ${C_CYN}2)${C_RST} Sonntags 03:00 ${C_DIM}0 3 * * 0${C_RST}\n" printf " ${C_CYN}3)${C_RST} Mo und Do 04:00 ${C_DIM}0 4 * * 1,4${C_RST}\n" printf " ${C_CYN}4)${C_RST} Taeglich 04:00 ${C_DIM}0 4 * * *${C_RST}\n" printf " ${C_CYN}5)${C_RST} Eigener Ausdruck ...\n" blank local _cs; choose _cs 5 "Auswahl" case "$_cs" in 1) CFG_CRON_EXPR="0 5 * * 1" ;; 2) CFG_CRON_EXPR="0 3 * * 0" ;; 3) CFG_CRON_EXPR="0 4 * * 1,4" ;; 4) CFG_CRON_EXPR="0 4 * * *" ;; 5) blank info "Format: Minute Stunde Tag Monat Wochentag" info "Beispiel: '30 3 * * 0' = sonntags 03:30" blank while true; do ask_var CFG_CRON_EXPR "Cron-Ausdruck" local _wc; _wc="$(echo "$CFG_CRON_EXPR" | wc -w)" if (( _wc == 5 )); then break; fi warn "Ein Cron-Ausdruck hat genau 5 Felder (gezaehlt: ${_wc})." done ;; esac log "Zeitplan: $CFG_CRON_EXPR" } # ============================================================================= # ZUSAMMENFASSUNG # ============================================================================= build_exec_line() { if [[ "$CFG_START_MODE" == "direct" ]]; then printf '%s +exec %s' "${CFG_SERVER_DIR}/run.sh" "$CFG_SERVER_CFG" else # Bewusst ohne txAdminInterface: die Einstellung bindet auch den # FXServer und macht den Spielserver unerreichbar. local line="${CFG_SERVER_DIR}/run.sh +set serverProfile default" line+=" +set txAdminPort ${CFG_TXADMIN_PORT}" printf '%s' "$line" fi } show_summary() { step "Zusammenfassung -- bitte pruefen" blank local W=30 printf " ${C_BLD}%-${W}s${C_RST} %s\n" "Artefakt-Verzeichnis:" "$CFG_SERVER_DIR" printf " ${C_BLD}%-${W}s${C_RST} %s\n" "Daten-Verzeichnis:" "$CFG_DATA_DIR" printf " ${C_BLD}%-${W}s${C_RST} %s\n" "Benutzer / Gruppe:" "${CFG_USER} / ${CFG_GROUP}" printf " ${C_BLD}%-${W}s${C_RST} %s\n" "systemd-Unit:" "/etc/systemd/system/${CFG_SERVICE}.service" printf " ${C_BLD}%-${W}s${C_RST} %s\n" "Startbefehl:" "$(build_exec_line)" printf " ${C_BLD}%-${W}s${C_RST} %s\n" "Update-Script:" "$CFG_UPDATE_BIN" printf " ${C_BLD}%-${W}s${C_RST} %s\n" "Update-Kanal:" "$CFG_UPDATE_CHANNEL" printf " ${C_BLD}%-${W}s${C_RST} %s\n" "Update-Log:" "$CFG_UPDATE_LOG" if [[ "$CFG_CRON_ENABLE" == "yes" ]]; then printf " ${C_BLD}%-${W}s${C_RST} %s\n" "Cron (root):" "$CFG_CRON_EXPR" else printf " ${C_BLD}%-${W}s${C_RST} ${C_DIM}%s${C_RST}\n" "Cron:" "deaktiviert" fi blank; div; blank printf " ${C_BLD}Es werden folgende Aktionen ausgefuehrt:${C_RST}\n\n" printf " ${C_DIM}1.${C_RST} /etc/systemd/system/${CFG_SERVICE}.service anlegen oder ersetzen\n" printf " ${C_DIM}2.${C_RST} systemctl daemon-reload und enable ${CFG_SERVICE}\n" printf " ${C_DIM}3.${C_RST} ${CFG_UPDATE_BIN} anlegen oder ersetzen\n" printf " ${C_DIM}4.${C_RST} chown -R ${CFG_USER}:${CFG_GROUP} ${CFG_SERVER_DIR}\n" [[ "$CFG_CRON_ENABLE" == "yes" ]] && \ printf " ${C_DIM}5.${C_RST} Cron-Eintrag in die root-crontab schreiben\n" blank } final_confirm() { div if ! yesno "Alle Aenderungen jetzt anwenden?" "n"; then blank warn "Abgebrochen. Es wurde nichts geaendert." blank exit 0 fi } # ============================================================================= # ANWENDEN -- Update-Script schreiben # ============================================================================= do_write_update_script() { step "Schreibe Update-Script" # Block 1: Kopf mit den Werten. Wird JETZT expandiert. cat > "$CFG_UPDATE_BIN" << CONF #!/usr/bin/env bash # # update-fxserver.sh # Erzeugt von install-fxserver.sh am $(date '+%Y-%m-%d %H:%M:%S') # Doku: https://www.musiker15.de/de/docs/fivem/auto-update # set -euo pipefail FX_DIR="${CFG_SERVER_DIR}" FX_USER="${CFG_USER}" FX_GROUP="${CFG_GROUP}" SERVICE="${CFG_SERVICE}" CHANNEL="${CFG_UPDATE_CHANNEL}" HEALTH_WAIT="15" API="https://changelogs-live.fivem.net/api/changelog/versions/linux/server" CONF # Block 2: Logik. Quoted Heredoc, damit \$FX_DIR und \$0 im erzeugten # Script stehen bleiben statt hier ersetzt zu werden. cat >> "$CFG_UPDATE_BIN" << 'LOGIC' log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*"; } err() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; } die() { err "$*"; exit 1; } log "=== update-fxserver.sh $(date '+%Y-%m-%d %H:%M:%S') ===" [[ $EUID -eq 0 ]] || die "Bitte als root ausfuehren: sudo $0" for bin in curl tar systemctl; do command -v "$bin" >/dev/null 2>&1 || die "Benoetigtes Programm fehlt: $bin" done [[ -d "$FX_DIR" ]] || die "Artefakt-Verzeichnis nicht gefunden: $FX_DIR" id "$FX_USER" >/dev/null 2>&1 || die "Benutzer existiert nicht: $FX_USER" log "Frage FXServer-Version ab (Kanal: ${CHANNEL}) ..." JSON="$(curl -fsSL --retry 3 --retry-delay 5 "$API")" || die "Changelog-API nicht erreichbar." if command -v jq >/dev/null 2>&1; then DOWNLOAD_URL="$(printf '%s' "$JSON" | jq -r ".${CHANNEL}_download // empty")" VERSION="$( printf '%s' "$JSON" | jq -r ".${CHANNEL} // empty")" else warn "jq fehlt, nutze grep-Rueckfallweg (besser: apt install jq)." echo | grep -qP '' 2>/dev/null || die "grep ohne PCRE. Bitte 'apt install jq'." DOWNLOAD_URL="$(printf '%s' "$JSON" | grep -oP "\"${CHANNEL}_download\"\s*:\s*\"\K[^\"]+")" VERSION="$( printf '%s' "$JSON" | grep -oP "\"${CHANNEL}\"\s*:\s*\"\K[^\"]+" | head -n1)" fi [[ -n "${DOWNLOAD_URL:-}" && "$DOWNLOAD_URL" == https://* ]] \ || die "Download-URL konnte nicht ermittelt werden." log "Version : ${VERSION:-unbekannt}" log "URL : $DOWNLOAD_URL" STAMP="${FX_DIR}/.installed-version" if [[ -f "$STAMP" && -n "${VERSION:-}" ]] && [[ "$(cat "$STAMP")" == "$VERSION" ]]; then log "Version ${VERSION} ist bereits installiert. Nichts zu tun." exit 0 fi TMP="$(mktemp -d /tmp/fxserver-update.XXXXXX)" trap 'rm -rf "$TMP"' EXIT ARCHIVE="$TMP/fx.tar.xz" STAGING="$TMP/staging" mkdir -p "$STAGING" # Erst herunterladen und entpacken, dann erst den Dienst anfassen. log "Lade Artefakt herunter ..." curl -fSL --retry 3 --retry-delay 5 -o "$ARCHIVE" "$DOWNLOAD_URL" || die "Download fehlgeschlagen." [[ -s "$ARCHIVE" ]] || die "Heruntergeladene Datei ist leer." log "Entpacke Archiv ..." tar -xJf "$ARCHIVE" -C "$STAGING" || die "Entpacken fehlgeschlagen." RUNSH_PATH="$(find "$STAGING" -maxdepth 2 -name run.sh -type f | head -n1)" [[ -n "$RUNSH_PATH" ]] || die "run.sh im Archiv nicht gefunden." SRC_ROOT="$(dirname "$RUNSH_PATH")" [[ -d "$SRC_ROOT/alpine" ]] || die "alpine/ im Archiv nicht gefunden." if systemctl is-active --quiet "$SERVICE"; then log "Stoppe '${SERVICE}' ..." systemctl stop "$SERVICE" else warn "'${SERVICE}' war nicht aktiv." fi BACKUP="${FX_DIR}/.previous" rm -rf "$BACKUP"; mkdir -p "$BACKUP" [[ -d "$FX_DIR/alpine" ]] && mv "$FX_DIR/alpine" "$BACKUP/alpine" [[ -f "$FX_DIR/run.sh" ]] && mv "$FX_DIR/run.sh" "$BACKUP/run.sh" restore() { err "Update fehlgeschlagen, stelle das vorherige Artefakt wieder her." rm -rf "$FX_DIR/alpine"; rm -f "$FX_DIR/run.sh" [[ -d "$BACKUP/alpine" ]] && mv "$BACKUP/alpine" "$FX_DIR/alpine" [[ -f "$BACKUP/run.sh" ]] && mv "$BACKUP/run.sh" "$FX_DIR/run.sh" chown -R "${FX_USER}:${FX_GROUP}" "$FX_DIR" || true systemctl start "$SERVICE" || true err "Alter Stand ist zurueck. Log: journalctl -u ${SERVICE} -n 50" exit 1 } log "Tausche alpine/ und run.sh in ${FX_DIR} ..." cp -a "$SRC_ROOT/alpine" "$FX_DIR/alpine" || restore cp -a "$SRC_ROOT/run.sh" "$FX_DIR/run.sh" || restore chmod +x "$FX_DIR/run.sh" [[ -f "$FX_DIR/alpine/opt/cfx-server/FXServer" ]] && chmod +x "$FX_DIR/alpine/opt/cfx-server/FXServer" log "Setze Besitzer auf ${FX_USER}:${FX_GROUP} ..." chown -R "${FX_USER}:${FX_GROUP}" "$FX_DIR" || restore log "Starte '${SERVICE}' neu ..." systemctl restart "$SERVICE" || restore log "Warte ${HEALTH_WAIT}s und pruefe, ob der Dienst oben bleibt ..." sleep "$HEALTH_WAIT" if systemctl is-active --quiet "$SERVICE"; then printf '%s' "${VERSION:-unbekannt}" > "$STAMP" rm -rf "$BACKUP" log "Fertig. FXServer ${VERSION:-?} laeuft." else err "'${SERVICE}' laeuft nach dem Neustart NICHT." restore fi LOGIC chmod 0755 "$CFG_UPDATE_BIN" log "Update-Script angelegt: $CFG_UPDATE_BIN" if bash -n "$CFG_UPDATE_BIN"; then log "Syntaxpruefung des erzeugten Scripts bestanden." else die "Das erzeugte Script hat einen Syntaxfehler. Bitte melden." fi } # ============================================================================= # ANWENDEN -- systemd-Unit schreiben # ============================================================================= do_write_service() { step "Schreibe systemd-Unit" local service_file="/etc/systemd/system/${CFG_SERVICE}.service" local workdir="$CFG_DATA_DIR" [[ -d "$workdir" ]] || workdir="$CFG_SERVER_DIR" cat > "$service_file" << EOF [Unit] Description=FiveM FXServer (${CFG_SERVICE}) After=network-online.target Wants=network-online.target [Service] Type=simple User=${CFG_USER} Group=${CFG_GROUP} WorkingDirectory=${workdir} ExecStart=$(build_exec_line) Restart=always RestartSec=15 TimeoutStartSec=120 TimeoutStopSec=30 # run.sh ist nur ein Wrapper um die proot-Umgebung. Bei KillMode=process # ueberleben die Kindprozesse und halten den Spielport belegt. KillMode=mixed KillSignal=SIGINT StandardOutput=journal StandardError=journal SyslogIdentifier=${CFG_SERVICE} LimitNOFILE=65535 # Zurueckhaltendes Hardening. SystemCallFilter, RestrictNamespaces und # NoNewPrivileges brechen den proot-Build und fehlen deshalb bewusst. PrivateTmp=true ProtectKernelTunables=true ProtectControlGroups=true [Install] WantedBy=multi-user.target EOF log "Unit angelegt: $service_file" systemctl daemon-reload && log "daemon-reload erledigt." systemctl enable "${CFG_SERVICE}.service" && log "Autostart aktiviert." } # ============================================================================= # ANWENDEN -- Rechte # ============================================================================= do_set_perms() { step "Setze Rechte" [[ -f "$CFG_SERVER_DIR/run.sh" ]] && chmod +x "$CFG_SERVER_DIR/run.sh" chown -R "${CFG_USER}:${CFG_GROUP}" "$CFG_SERVER_DIR" log "chown -R ${CFG_USER}:${CFG_GROUP} ${CFG_SERVER_DIR} erledigt." } # ============================================================================= # ANWENDEN -- Cron # ============================================================================= do_write_cron() { [[ "$CFG_CRON_ENABLE" != "yes" ]] && return 0 step "Schreibe Cron-Eintrag in die root-crontab" local cron_line="${CFG_CRON_EXPR} ${CFG_UPDATE_BIN} >> ${CFG_UPDATE_LOG} 2>&1" local cron_path="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" # Eigene alte Zeilen entfernen, damit sich bei mehrfachem Lauf nichts # ansammelt. Die PATH-Zeile gehoert ausdruecklich dazu. ( crontab -l 2>/dev/null \ | grep -vF "$CRON_MARKER" \ | grep -vF "$CFG_UPDATE_BIN" \ | grep -vF "$cron_path" \ || true echo "$cron_path" echo "$CRON_MARKER" echo "$cron_line" ) | crontab - touch "$CFG_UPDATE_LOG" 2>/dev/null || true chmod 0640 "$CFG_UPDATE_LOG" 2>/dev/null || true log "Cron-Eintrag: $cron_line" log "Logdatei: $CFG_UPDATE_LOG" info "Kontrolle mit: crontab -l" } # ============================================================================= # FERTIG # ============================================================================= show_done() { step "Installation abgeschlossen" blank log "systemd-Unit -> /etc/systemd/system/${CFG_SERVICE}.service" log "Update-Script -> ${CFG_UPDATE_BIN}" [[ "$CFG_CRON_ENABLE" == "yes" ]] && log "Cron-Eintrag -> ${CFG_CRON_EXPR}" if [[ "$CFG_START_MODE" == "txadmin" ]]; then blank info "Erstsetup per SSH-Tunnel vom eigenen Rechner aus:" printf " ${C_CYN}ssh -N -L %s:127.0.0.1:%s root@${C_RST}\n" \ "$CFG_TXADMIN_PORT" "$CFG_TXADMIN_PORT" info "Danach im Browser: http://127.0.0.1:${CFG_TXADMIN_PORT}/" info "Dauerhaft besser: Reverse Proxy einrichten." printf " ${C_DIM}https://www.musiker15.de/de/docs/fivem/txadmin-reverse-proxy${C_RST}\n" fi if [[ "$CFG_UFW_DENY" == "yes" ]]; then blank step "Firewall" ufw deny "${CFG_TXADMIN_PORT}/tcp" && log "ufw deny ${CFG_TXADMIN_PORT}/tcp gesetzt." info "Spielport nicht vergessen, falls noch nicht offen:" printf " ${C_CYN}ufw allow 30120/tcp && ufw allow 30120/udp${C_RST}\n" info "Und von einem ANDEREN Rechner gegenpruefen:" printf " ${C_CYN}curl -m 5 http://:%s/${C_RST}\n" "$CFG_TXADMIN_PORT" elif command -v ufw >/dev/null 2>&1; then blank info "Firewall nicht vergessen:" printf " ${C_CYN}ufw allow 30120/tcp && ufw allow 30120/udp${C_RST}\n" [[ "$CFG_START_MODE" == "txadmin" ]] && \ printf " ${C_CYN}ufw deny %s/tcp${C_RST}\n" "$CFG_TXADMIN_PORT" fi blank; div printf " ${C_BLD}Nuetzliche Befehle:${C_RST}\n\n" printf " ${C_DIM}%-24s${C_RST} ${C_CYN}%s${C_RST}\n" "Starten:" "systemctl start ${CFG_SERVICE}" printf " ${C_DIM}%-24s${C_RST} ${C_CYN}%s${C_RST}\n" "Stoppen:" "systemctl stop ${CFG_SERVICE}" printf " ${C_DIM}%-24s${C_RST} ${C_CYN}%s${C_RST}\n" "Neu starten:" "systemctl restart ${CFG_SERVICE}" printf " ${C_DIM}%-24s${C_RST} ${C_CYN}%s${C_RST}\n" "Status:" "systemctl status ${CFG_SERVICE}" printf " ${C_DIM}%-24s${C_RST} ${C_CYN}%s${C_RST}\n" "Live-Log:" "journalctl -u ${CFG_SERVICE} -f" printf " ${C_DIM}%-24s${C_RST} ${C_CYN}%s${C_RST}\n" "Update-Log:" "tail -f ${CFG_UPDATE_LOG}" printf " ${C_DIM}%-24s${C_RST} ${C_CYN}%s${C_RST}\n" "Update jetzt:" "sudo ${CFG_UPDATE_BIN}" div blank if yesno "Den FiveM-Server jetzt starten?"; then systemctl start "${CFG_SERVICE}" sleep 3 if systemctl is-active --quiet "${CFG_SERVICE}"; then log "Server laeuft." else err "Server konnte nicht gestartet werden." warn "Pruefe: journalctl -u ${CFG_SERVICE} -n 50" fi blank systemctl status "${CFG_SERVICE}" --no-pager -l || true else info "Manuell starten mit: systemctl start ${CFG_SERVICE}" fi blank } # ============================================================================= # MAIN # ============================================================================= main() { welcome preflight q_dirs q_user q_service q_update q_cron show_summary final_confirm do_write_update_script do_write_service do_set_perms do_write_cron show_done } main "$@"