222 lines
6.7 KiB
Bash
222 lines
6.7 KiB
Bash
#!/usr/bin/env bash
|
||
#!/bin/bash
|
||
# Beschreibung: Minecraft → ntfy Notifier (Join/Leave + optional Up/Down) mit Cron-safe Locking
|
||
# Autor: Patrick Asmus
|
||
# Web: https://www.cleveradmin.de
|
||
# Repository: https://git.techniverse.net/scriptos/minecraft-ntfy-notify
|
||
# Version: 1.3
|
||
# Datum: 18.09.2025
|
||
# Modifikation: Cron-sicheres Locking (Lockdir+PID), Running-Mark, Cleanup via trap
|
||
#####################################################
|
||
set -euo pipefail
|
||
|
||
# Robustes PATH für Cron
|
||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
if [[ -f "${SCRIPT_DIR}/.env" ]]; then
|
||
set -a
|
||
. "${SCRIPT_DIR}/.env"
|
||
set +a
|
||
fi
|
||
|
||
# ===== Konfiguration via ENV =====
|
||
MC_HOST="${MC_HOST:-127.0.0.1}"
|
||
RCON_PORT="${RCON_PORT:-25575}"
|
||
RCON_PASSWORD="${RCON_PASSWORD:-changeme}"
|
||
|
||
POLL_SECONDS="${POLL_SECONDS:-10}"
|
||
ANNOUNCE_SERVER_UPDOWN="${ANNOUNCE_SERVER_UPDOWN:-true}"
|
||
|
||
# ntfy
|
||
NTFY_SERVER="${NTFY_SERVER:-}"
|
||
NTFY_TOPIC="${NTFY_TOPIC:-}"
|
||
NTFY_TOKEN="${NTFY_TOKEN:-}"
|
||
NTFY_TITLE_PREFIX="${NTFY_TITLE_PREFIX:-Minecraft}"
|
||
NTFY_TAGS_BASE="${NTFY_TAGS_BASE:-minecraft}"
|
||
NTFY_PRIORITY_JOIN="${NTFY_PRIORITY_JOIN:-3}"
|
||
NTFY_PRIORITY_LEAVE="${NTFY_PRIORITY_LEAVE:-3}"
|
||
NTFY_PRIORITY_UP="${NTFY_PRIORITY_UP:-4}"
|
||
NTFY_PRIORITY_DOWN="${NTFY_PRIORITY_DOWN:-5}"
|
||
NTFY_MARKDOWN="${NTFY_MARKDOWN:-false}"
|
||
|
||
# State (standardmäßig im Script-Ordner, root-frei)
|
||
STATE_DIR="${STATE_DIR:-${SCRIPT_DIR}/state}"
|
||
STATE_PLAYERS="${STATE_DIR}/players.prev"
|
||
STATE_UP="${STATE_DIR}/server_up.prev"
|
||
|
||
# Lock & Running-Mark (pro MC_HOST:RCON_PORT:NTFY_TOPIC eindeutig)
|
||
# -> erlaubt mehrere parallele Instanzen für verschiedene Server/Topics
|
||
LOCK_KEY="$(printf '%s' "${MC_HOST}_${RCON_PORT}_${NTFY_TOPIC}" | tr -c 'A-Za-z0-9._-' '_')"
|
||
RUN_DIR="${RUN_DIR:-${STATE_DIR}}"
|
||
LOCK_DIR="${RUN_DIR}/lock.${LOCK_KEY}"
|
||
PID_FILE="${LOCK_DIR}/pid"
|
||
RUN_MARK="${RUN_DIR}/running.${LOCK_KEY}"
|
||
|
||
# Optional Debug
|
||
DEBUG="${DEBUG:-false}"
|
||
|
||
# ===== Helpers =====
|
||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||
need_bin() { command -v "$1" >/dev/null 2>&1 || die "Benötigtes Tool fehlt: $1"; }
|
||
dbg() { [[ "$DEBUG" == "true" ]] && echo "DBG: $*" >&2 || true; }
|
||
|
||
cleanup() {
|
||
# Lauf-Mark und Lock sauber entfernen
|
||
rm -f "$RUN_MARK" 2>/dev/null || true
|
||
rm -f "$PID_FILE" 2>/dev/null || true
|
||
rmdir "$LOCK_DIR" 2>/dev/null || true
|
||
}
|
||
|
||
acquire_lock() {
|
||
mkdir -p "$RUN_DIR"
|
||
# Atomarer Lock-Versuch
|
||
if mkdir "$LOCK_DIR" 2>/dev/null; then
|
||
echo "$$" > "$PID_FILE"
|
||
trap cleanup EXIT INT TERM
|
||
return 0
|
||
fi
|
||
|
||
# Lock existiert -> prüfen, ob Prozess noch lebt
|
||
if [[ -f "$PID_FILE" ]]; then
|
||
oldpid="$(cat "$PID_FILE" 2>/dev/null || true)"
|
||
if [[ -n "${oldpid:-}" ]] && kill -0 "$oldpid" 2>/dev/null; then
|
||
# Schon aktiv → leise beenden (Cron-safe)
|
||
dbg "Bereits laufend (PID $oldpid), beende."
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
# Stale Lock entfernen und erneut versuchen
|
||
rm -rf "$LOCK_DIR" 2>/dev/null || true
|
||
if mkdir "$LOCK_DIR" 2>/dev/null; then
|
||
echo "$$" > "$PID_FILE"
|
||
trap cleanup EXIT INT TERM
|
||
return 0
|
||
fi
|
||
|
||
# Falls wir hier landen, ist wirklich etwas schief
|
||
die "Konnte Lock nicht übernehmen: ${LOCK_DIR}"
|
||
}
|
||
|
||
ntfy_notify() {
|
||
local title="$1" body="$2" tags="$3" priority="$4"
|
||
[[ -z "$NTFY_SERVER" || -z "$NTFY_TOPIC" ]] && { echo "ntfy Server/Topic fehlt – skip"; return 1; }
|
||
local url="${NTFY_SERVER%/}/${NTFY_TOPIC}"
|
||
local args=(-sS -X POST "$url" -H "Title: ${title}" -H "Priority: ${priority}" -H "Tags: ${tags}")
|
||
[[ "$NTFY_MARKDOWN" == "true" ]] && args+=(-H "Markdown: yes")
|
||
[[ -n "$NTFY_TOKEN" ]] && args+=(-H "Authorization: Bearer ${NTFY_TOKEN}")
|
||
curl "${args[@]}" --data-raw "$body" >/dev/null 2>&1 || return 1
|
||
return 0
|
||
}
|
||
|
||
ensure_state() {
|
||
mkdir -p "$STATE_DIR"
|
||
touch "$STATE_PLAYERS"
|
||
[[ -f "$STATE_UP" ]] || echo "unknown" >"$STATE_UP"
|
||
}
|
||
|
||
# Strippt ANSI-Escape-Sequenzen, MC-Farbcodes (§x), CR und Steuerzeichen
|
||
sanitize() {
|
||
sed -E $'s/\x1B\\[[0-9;]*[A-Za-z]//g' \
|
||
| sed -E 's/§[0-9A-FK-ORa-fk-or]//g' \
|
||
| tr -d '\r' \
|
||
| sed -E 's/[\x00-\x1F\x7F]//g'
|
||
}
|
||
|
||
get_players() {
|
||
local out
|
||
if ! out=$(mcrcon -H "$MC_HOST" -P "$RCON_PORT" -p "$RCON_PASSWORD" "list" 2>/dev/null); then
|
||
return 1
|
||
fi
|
||
dbg "RAW: $out"
|
||
out="$(printf '%s' "$out" | sanitize)"
|
||
|
||
if ! grep -q "players online:" <<<"$out"; then
|
||
return 0
|
||
fi
|
||
|
||
local names="${out#*:}"
|
||
names="$(echo "$names" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
||
if [[ -z "$names" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
printf '%s\n' "$names" \
|
||
| tr ',' '\n' \
|
||
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
|
||
| sed '/^$/d' \
|
||
| LC_ALL=C sort -u
|
||
|
||
return 0
|
||
}
|
||
|
||
# ===== Checks & Lock =====
|
||
need_bin curl
|
||
need_bin mcrcon
|
||
[[ -n "${NTFY_SERVER}" && -n "${NTFY_TOPIC}" ]] || die "NTFY_SERVER/NTFY_TOPIC nicht gesetzt"
|
||
ensure_state
|
||
acquire_lock
|
||
|
||
echo "Starte Polling ${MC_HOST}:${RCON_PORT} → ntfy ${NTFY_SERVER}/${NTFY_TOPIC} (PID $$)"
|
||
prev_up="$(cat "$STATE_UP")"
|
||
|
||
# ===== Main Loop =====
|
||
while :; do
|
||
# Running-Mark aktualisieren (für Monitoring/„läuft“-Check)
|
||
date -Iseconds > "$RUN_MARK"
|
||
echo "$$" >> "$RUN_MARK"
|
||
|
||
tmp_players="$(mktemp)"
|
||
if get_players >"$tmp_players"; then
|
||
server_up="true"
|
||
else
|
||
server_up="false"
|
||
: >"$tmp_players"
|
||
fi
|
||
|
||
if [[ "$ANNOUNCE_SERVER_UPDOWN" == "true" && "$prev_up" != "unknown" && "$server_up" != "$prev_up" ]]; then
|
||
if [[ "$server_up" == "true" ]]; then
|
||
ntfy_notify "${NTFY_TITLE_PREFIX}: Server up" \
|
||
"Server ist wieder erreichbar (${MC_HOST}:${RCON_PORT})." \
|
||
"${NTFY_TAGS_BASE},up" "$NTFY_PRIORITY_UP" || true
|
||
else
|
||
ntfy_notify "${NTFY_TITLE_PREFIX}: Server down" \
|
||
"Server ist nicht erreichbar (${MC_HOST}:${RCON_PORT})." \
|
||
"${NTFY_TAGS_BASE},down" "$NTFY_PRIORITY_DOWN" || true
|
||
fi
|
||
fi
|
||
|
||
if [[ "$server_up" == "true" ]]; then
|
||
LC_ALL=C sort -u "$STATE_PLAYERS" -o "$STATE_PLAYERS"
|
||
|
||
joined="$(comm -13 "$STATE_PLAYERS" "$tmp_players" || true)"
|
||
left="$(comm -23 "$STATE_PLAYERS" "$tmp_players" || true)"
|
||
|
||
if [[ -n "$joined" ]]; then
|
||
while IFS= read -r name; do
|
||
[[ -z "$name" ]] && continue
|
||
ntfy_notify "${NTFY_TITLE_PREFIX}: Join" \
|
||
"Player \"${name}\" ist beigetreten." \
|
||
"${NTFY_TAGS_BASE},join" "$NTFY_PRIORITY_JOIN" || true
|
||
done <<<"$joined"
|
||
fi
|
||
if [[ -n "$left" ]]; then
|
||
while IFS= read -r name; do
|
||
[[ -z "$name" ]] && continue
|
||
ntfy_notify "${NTFY_TITLE_PREFIX}: Leave" \
|
||
"Player \"${name}\" hat den Server verlassen." \
|
||
"${NTFY_TAGS_BASE},leave" "$NTFY_PRIORITY_LEAVE" || true
|
||
done <<<"$left"
|
||
fi
|
||
|
||
mv "$tmp_players" "$STATE_PLAYERS"
|
||
else
|
||
rm -f "$tmp_players"
|
||
fi
|
||
|
||
echo -n "$server_up" >"$STATE_UP"
|
||
prev_up="$server_up"
|
||
sleep "$POLL_SECONDS"
|
||
done
|