Initial commit

This commit is contained in:
admin committed 2026-09-05 22:47:35 +02:00
commit 8641464a6c
44 files changed
+2192

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -Euo pipefail
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
failures=0
ok() { printf '✓ %s\n' "$*"; }
bad() { printf '✗ %s\n' "$*"; failures=$((failures + 1)); }
[[ -r "$CONFIG_FILE" ]] || {
bad "$CONFIG_FILE fehlt"
exit 1
}
# shellcheck disable=SC1090
source "$CONFIG_FILE"
for command in git curl jq tar zstd systemctl hermes; do
command -v "$command" >/dev/null 2>&1 && ok "$command vorhanden" || bad "$command fehlt"
done
for script in "$HOME/.local/libexec"/hermes-stack-*; do
[[ -f "$script" ]] || continue
bash -n "$script" && ok "Syntax: ${script##*/}" || bad "Syntaxfehler: ${script##*/}"
done
if [[ -d "$HERMES_REPO/.git" ]]; then
ok "Hermes-Git-Checkout vorhanden"
dirty="$(git -C "$HERMES_REPO" status --porcelain --untracked-files=all |
grep -vxF '?? .install_method' || true)"
[[ -z "$dirty" ]] && ok "Hermes-Checkout sauber" || bad "Hermes-Checkout enthält lokale Änderungen"
else
bad "Hermes-Git-Checkout fehlt"
fi
systemctl --user is-active --quiet "$GATEWAY_SERVICE" && ok "Gateway aktiv" || bad "Gateway inaktiv"
hermes gateway status >/dev/null 2>&1 && ok "Gateway gesund" || bad "Gateway-Status fehlgeschlagen"
if curl --fail --silent --max-time 5 "$LLAMA_URL/health" |
jq -e '.status == "ok"' >/dev/null 2>&1; then
ok "llama.cpp-Endpunkt gesund"
else
bad "llama.cpp-Endpunkt nicht gesund"
fi
set +u
set -a
# shellcheck disable=SC1090
source "$HERMES_ENV" 2>/dev/null || true
set +a
set -u
if [[ -n "${SIGNAL_HTTP_URL:-}" ]]; then
status="$(curl --silent --output /dev/null --max-time 5 --write-out '%{http_code}' \
"${SIGNAL_HTTP_URL%/}/api/v1/check" || true)"
[[ "$status" =~ ^2[0-9][0-9]$ ]] && ok "signal-cli HTTP erreichbar" || bad "signal-cli HTTP nicht erreichbar"
fi
printf '\nFehler: %d\n' "$failures"
((failures == 0))
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -Euo pipefail
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-maintenance"
NOTIFIER="$HOME/.local/libexec/hermes-stack-notify"
mkdir -p "$STATE_DIR"
# shellcheck disable=SC1090
source "$CONFIG_FILE"
# Niemals den absichtlich gestoppten Gateway mitten im Update neu starten.
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
flock -n 9 || exit 0
state_file="$STATE_DIR/health-state"
problems=()
if ! systemctl --user is-active --quiet "$GATEWAY_SERVICE"; then
problems+=("$GATEWAY_SERVICE war inaktiv; Neustart wurde versucht")
systemctl --user restart "$GATEWAY_SERVICE" || true
fi
if ! hermes gateway status >/dev/null 2>&1; then
problems+=("Hermes-Gateway antwortete nicht gesund; Neustart wurde versucht")
systemctl --user restart "$GATEWAY_SERVICE" || true
fi
if [[ "${LLAMA_HEALTH_AUTORESTART:-0}" == 1 ]] &&
! systemctl --user is-active --quiet "$LLAMA_SERVICE"; then
problems+=("$LLAMA_SERVICE war inaktiv; Neustart wurde versucht")
systemctl --user restart "$LLAMA_SERVICE" || true
fi
if ! curl --fail --silent --max-time 10 "$LLAMA_URL/health" |
jq -e '.status == "ok"' >/dev/null 2>&1; then
problems+=("llama-server antwortete nicht gesund; kein HTTP-bedingter Neustart laufender Inferenz")
fi
if ((${#problems[@]} == 0)); then
current="healthy"
previous="$(cat "$state_file" 2>/dev/null || true)"
if [[ "$previous" == unhealthy ]]; then
"$NOTIFIER" "Hermes-Stack: Dienste sind wieder gesund." || true
fi
else
current="unhealthy"
previous="$(cat "$state_file" 2>/dev/null || true)"
if [[ "$previous" != unhealthy ]]; then
message="$(IFS='; '; printf '%s' "${problems[*]}")"
"$NOTIFIER" "Hermes-Stack: Gesundheitsprüfung fehlgeschlagen. $message. Details: journalctl --user -u $LLAMA_SERVICE -u $GATEWAY_SERVICE" || true
fi
fi
printf '%s\n' "$current" >"$state_file"
[[ "$current" == healthy ]]
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-maintenance"
NOTIFIER="$HOME/.local/libexec/hermes-stack-notify"
mkdir -p "$STATE_DIR"
# shellcheck disable=SC1090
source "$CONFIG_FILE"
log() {
printf '%s\n' "$*" | systemd-cat -t hermes-stack-maintenance
}
notify_user() {
local text="$1"
if [[ -x "$NOTIFIER" ]]; then
"$NOTIFIER" "$text" || true
else
log "$text"
fi
}
fetch_origin_main_with_retry() {
local -a retry_delays=(0 5 20)
local -a git_options=()
local attempt delay
for ((attempt = 1; attempt <= ${#retry_delays[@]}; attempt++)); do
delay="${retry_delays[attempt - 1]}"
if ((delay > 0)); then
log "Hermes-Git-Fetch: neuer Versuch $attempt/${#retry_delays[@]} in ${delay}s."
sleep "$delay"
fi
git_options=()
if ((attempt == ${#retry_delays[@]})); then
git_options=(-c http.version=HTTP/1.1)
log "Hermes-Git-Fetch: letzter Versuch mit HTTP/1.1."
fi
if GIT_TERMINAL_PROMPT=0 timeout --foreground 90s \
git "${git_options[@]}" -C "$HERMES_REPO" fetch --no-tags \
"$HERMES_REMOTE" \
"+refs/heads/$HERMES_BRANCH:refs/remotes/$HERMES_REMOTE/$HERMES_BRANCH"; then
((attempt == 1)) ||
log "Hermes-Git-Fetch war im Versuch $attempt/${#retry_delays[@]} erfolgreich."
return 0
fi
log "Hermes-Git-Fetch fehlgeschlagen (Versuch $attempt/${#retry_delays[@]})."
done
return 1
}
run_hermes_update_attempt() {
local attempt="$1"
local -a pipe_status
printf '\n===== Hermes update: Versuch %s/2 (%s) =====\n' \
"$attempt" "$(date --iso-8601=seconds)" >>"$update_log"
set +e
hermes update 2>&1 | tee -a "$update_log"
pipe_status=("${PIPESTATUS[@]}")
set -e
((pipe_status[1] == 0)) || return "${pipe_status[1]}"
return "${pipe_status[0]}"
}
is_retryable_update_fetch_failure() {
local current_commit dirty_after
current_commit="$(git -C "$HERMES_REPO" rev-parse HEAD)" || return 1
[[ "$current_commit" == "$old_commit" ]] || return 1
dirty_after="$(
git -C "$HERMES_REPO" status --porcelain --untracked-files=all |
grep -vxF '?? .install_method' || true
)"
[[ -z "$dirty_after" ]] || return 1
grep -Fq 'Fetching updates' "$update_log" || return 1
grep -Fq 'Pulling updates' "$update_log" && return 1
grep -Eqi \
'HTTP (429|500|502|503|504)|returned error: (429|500|502|503|504)|rate.?limit|could not read Username|Authentication failed|Could not resolve host|Failed to connect|Connection (timed out|reset)|TLS|early EOF|Failed to fetch updates|Network error' \
"$update_log"
}
for command in git tar zstd jq curl systemctl timeout flock hermes; do
command -v "$command" >/dev/null 2>&1 || {
notify_user "Hermes-Update abgebrochen: benötigtes Kommando fehlt: $command"
exit 1
}
done
LOCK_FILE="${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
if [[ ! -d "$HERMES_REPO/.git" ]]; then
notify_user "Hermes-Update abgebrochen: Git-Installation nicht gefunden."
exit 1
fi
case "$HERMES_REPO" in
"$HOME"/*/hermes-agent) ;;
*)
notify_user "Hermes-Update abgebrochen: unsicherer Repository-Pfad: $HERMES_REPO"
exit 1
;;
esac
dirty="$(
git -C "$HERMES_REPO" status --porcelain --untracked-files=all |
grep -vxF '?? .install_method' || true
)"
if [[ -n "$dirty" ]]; then
notify_user "Hermes-Update abgebrochen: lokale Änderungen im Hermes-Repository:
$dirty"
exit 1
fi
old_commit="$(git -C "$HERMES_REPO" rev-parse HEAD)"
if ! fetch_origin_main_with_retry; then
notify_user "Hermes-Updateprüfung fehlgeschlagen: $HERMES_REMOTE/$HERMES_BRANCH ist nicht erreichbar. Gateway und Installation bleiben unverändert."
exit 1
fi
remote_commit="$(git -C "$HERMES_REPO" rev-parse "refs/remotes/$HERMES_REMOTE/$HERMES_BRANCH")"
if [[ ! "$remote_commit" =~ ^[0-9a-f]{40}$ ]]; then
notify_user "Hermes-Updateprüfung fehlgeschlagen: Remote lieferte keinen gültigen Commit."
exit 1
fi
if [[ "$remote_commit" == "$old_commit" ]]; then
notify_user "Hermes ist bereits aktuell: $old_commit"
exit 0
fi
backup_dir="$STATE_DIR/backups"
mkdir -p "$backup_dir"
timestamp="$(date +%Y%m%d-%H%M%S)"
backup="$backup_dir/hermes-$timestamp.tar.zst"
update_log="$STATE_DIR/hermes-update-$timestamp.log"
paths=("${HERMES_REPO#"$HOME/"}")
for path in \
"$HOME/.hermes/config.yaml" "$HOME/.hermes/.env" "$HOME/.hermes/auth.json" \
"${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/$GATEWAY_SERVICE"; do
[[ -e "$path" && "$path" == "$HOME/"* ]] && paths+=("${path#"$HOME/"}")
done
tar --zstd -cf "$backup" -C "$HOME" "${paths[@]}"
rollback() {
trap - ERR
set +e
systemctl --user stop "$GATEWAY_SERVICE"
if [[ "$HERMES_REPO" == "$HOME/"*/hermes-agent ]]; then
rm -rf -- "$HERMES_REPO"
tar --zstd -xf "$backup" -C "$HOME"
fi
systemctl --user daemon-reload
systemctl --user restart "$GATEWAY_SERVICE"
notify_user "Hermes-Update zurückgerollt auf $old_commit. Backup: $backup"
exit 1
}
trap rollback ERR
systemctl --user stop "$GATEWAY_SERVICE"
if run_hermes_update_attempt 1; then
update_exit=0
else
update_exit=$?
log "Hermes-Updater meldete Exitcode $update_exit."
if is_retryable_update_fetch_failure; then
log "Transienter Fetch-Fehler erkannt; Hermes-Update wird einmal wiederholt."
sleep 20
if run_hermes_update_attempt 2; then
update_exit=0
else
update_exit=$?
log "Hermes-Updater-Retry meldete Exitcode $update_exit."
fi
fi
fi
if ! hermes config check >/dev/null 2>&1; then
hermes config migrate
hermes config check
fi
systemctl --user daemon-reload
systemctl --user reset-failed "$GATEWAY_SERVICE" || true
systemctl --user restart "$GATEWAY_SERVICE"
gateway_ready=0
for ((attempt = 1; attempt <= ${GATEWAY_HEALTH_ATTEMPTS:-12}; attempt++)); do
if systemctl --user is-active --quiet "$GATEWAY_SERVICE" &&
hermes gateway status >/dev/null 2>&1; then
gateway_ready=1
break
fi
sleep "${GATEWAY_HEALTH_DELAY:-5}"
done
if ((gateway_ready == 0)); then
notify_user "Hermes-Gateway wurde nach dem Update nicht gesund. Rollback wird ausgeführt."
false
fi
if ! curl --fail --silent --max-time 10 "$LLAMA_URL/health" |
jq -e '.status == "ok"' >/dev/null; then
notify_user "Hermes und Gateway sind aktualisiert, aber der lokale Modell-Endpunkt ist nicht gesund: $LLAMA_URL. Kein Hermes-Rollback, da llama-server unabhängig ist."
fi
trap - ERR
mapfile -t old_backups < <(
find "$backup_dir" -maxdepth 1 -type f -name 'hermes-*.tar.zst' \
-printf '%T@ %p\n' | sort -nr | tail -n "+$((${BACKUP_KEEP:-3} + 1))" |
cut -d' ' -f2-
)
((${#old_backups[@]} == 0)) || rm -f -- "${old_backups[@]}"
new_commit="$(git -C "$HERMES_REPO" rev-parse HEAD)"
if [[ "$new_commit" == "$old_commit" ]]; then
notify_user "Hermes-Update wurde nicht installiert (Updater-Exitcode $update_exit); der bisherige Commit $new_commit und der frisch gestartete Gateway sind gesund."
exit 1
elif ((update_exit != 0)); then
notify_user "Hermes erfolgreich aktualisiert: $old_commit -> $new_commit. Eine Updater-Restartwarnung wurde durch den frischen Gateway-Neustart behoben."
else
notify_user "Hermes erfolgreich aktualisiert: $old_commit -> $new_commit"
fi
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
runner="$(dirname -- "${BASH_SOURCE[0]}")/hermes-stack-llama-run"
case "${1:-list}" in
list)
printf '%-12s %8s %10s\n' Profil Kontext Fit-MiB \
baseline 65536 1536 balanced 49152 1536 vram-plus 49152 1280 compact 32768 1536
exit 0 ;;
baseline|balanced|vram-plus|compact) selected="$1" ;;
*) printf 'Aufruf: %s [list|baseline|balanced|vram-plus|compact]\n' "$0" >&2; exit 2 ;;
esac
[[ $# -eq 1 ]] || exit 2
# Profilwechsel nur für bewusst migrierte Konfigurationen erlauben.
# shellcheck disable=SC1090
source "$config_dir/llama.env"
[[ -n "${LLAMA_PROFILE:-}" ]] || {
printf 'Zuerst llama.env nach docs/WORKSTATION.md migrieren. Keine Änderung.\n' >&2; exit 1;
}
"$runner" --profile "$selected" --check
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
flock -n 9 || { printf 'Maintenance läuft. Später erneut versuchen.\n' >&2; exit 1; }
target="$config_dir/llama-profile.env"
if [[ -e "$target" ]]; then
backup="$(mktemp "$config_dir/llama-profile.backup.XXXXXX")"
cp -p -- "$target" "$backup"
printf 'Vorherige Profilwahl: %s\n' "$backup"
fi
candidate="$(mktemp "$config_dir/llama-profile.env.XXXXXX")"
printf 'LLAMA_PROFILE=%q\n' "$selected" >"$candidate"
mv -T -- "$candidate" "$target"
printf 'Profil %s gespeichert. Laufende Jobs bleiben unverändert.\n' "$selected"
printf 'Erst nach Jobende: hermes-stack-llama-restart (siehe docs/WORKSTATION.md).\n'
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Nur lesen: kein Modellstart, keine Inferenz, kein Neustart.
set -Euo pipefail
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
if [[ -r "$config_dir/config.env" ]]; then
# shellcheck disable=SC1090
source "$config_dir/config.env"
fi
printf '\nGPU (Gesamtspeicher, belegt, frei in MiB):\n'
nvidia-smi --query-gpu=name,memory.total,memory.used,memory.free,utilization.gpu,power.draw,temperature.gpu \
--format=csv || true
printf '\nDienstzustand:\n'
systemctl --user show "${LLAMA_SERVICE:-llama-qwen.service}" \
-p ActiveState -p SubState -p MainPID -p ExecMainStartTimestamp --no-pager || true
printf '\nLetzte Speicher-/Leistungsdaten (keine vollständigen Prompts):\n'
journalctl --user -u "${LLAMA_SERVICE:-llama-qwen.service}" -n 1000 --no-pager |
rg 'offload|buffer size|n_ctx|prompt eval time|eval time|sleeping|waking' |
tail -n 35 || true
printf '\nDas ist eine Momentaufnahme, kein Spitzenlast-Benchmark.\n'
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -Eeuo pipefail
if [[ "${1:-}" != --jobs-finished || $# -ne 1 ]]; then
printf 'Stoppt laufende Inferenz! Erst nach Jobende ausführen:\n %s --jobs-finished\n' "$0" >&2
exit 2
fi
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
# shellcheck disable=SC1090
source "$config_dir/config.env"
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
flock -n 9 || { printf 'Maintenance läuft; kein Neustart.\n' >&2; exit 1; }
"$(dirname -- "${BASH_SOURCE[0]}")/hermes-stack-llama-run" --check
systemctl --user restart "$LLAMA_SERVICE"
printf 'Neustart ausgelöst. Modell kann noch laden; Logs und Inferenz prüfen.\n'
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -Eeuo pipefail
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
CONFIG_FILE="${HERMES_LLAMA_CONFIG:-$config_dir/llama.env}"
LLAMA_ARGS=()
# shellcheck disable=SC1090
source "$CONFIG_FILE"
mode=run
profile_override=""
while (($#)); do
case "$1" in
--check) mode=check ;;
--print) mode=print ;;
--profile) [[ $# -ge 2 ]] || exit 2; profile_override="$2"; shift ;;
*) printf 'Aufruf: %s [--check|--print] [--profile NAME]\n' "$0" >&2; exit 2 ;;
esac
shift
done
if [[ -n "${LLAMA_PROFILE:-}" && -r "$config_dir/llama-profile.env" ]]; then
# shellcheck disable=SC1090
source "$config_dir/llama-profile.env"
fi
[[ -z "$profile_override" ]] || LLAMA_PROFILE="$profile_override"
if [[ -z "${LLAMA_SERVER_BIN:-}" ]]; then
LLAMA_SERVER_BIN="$HOME/.local/opt/llama.cpp/current/bin/llama-server"
if [[ ! -x "$LLAMA_SERVER_BIN" ]]; then
LLAMA_SERVER_BIN="$(command -v llama-server || true)"
fi
fi
[[ -x "$LLAMA_SERVER_BIN" ]] || {
printf 'llama-server nicht ausführbar: %s\n' "$LLAMA_SERVER_BIN" >&2
exit 1
}
model_args=()
if [[ -n "${LLAMA_MODEL:-}" && -n "${LLAMA_HF_REPO:-}" ]]; then
printf 'Nur LLAMA_MODEL ODER LLAMA_HF_REPO setzen.\n' >&2
exit 1
elif [[ -n "${LLAMA_MODEL:-}" ]]; then
[[ -r "$LLAMA_MODEL" ]] || { printf 'Modell nicht lesbar.\n' >&2; exit 1; }
model_args=(--model "$LLAMA_MODEL")
elif [[ -n "${LLAMA_HF_REPO:-}" ]]; then
model_args=(--hf-repo "$LLAMA_HF_REPO")
else
printf 'LLAMA_MODEL oder LLAMA_HF_REPO fehlt.\n' >&2
exit 1
fi
if [[ -n "${LLAMA_MMPROJ:-}" ]]; then
[[ -r "$LLAMA_MMPROJ" ]] || { printf 'Projektor nicht lesbar.\n' >&2; exit 1; }
model_args+=(--mmproj "$LLAMA_MMPROJ")
fi
[[ -z "${LLAMA_ALIAS:-}" ]] || model_args+=(--alias "$LLAMA_ALIAS")
profile_args=()
if [[ -n "${LLAMA_PROFILE:-}" ]]; then
case "$LLAMA_PROFILE" in
baseline) ctx=65536; margin=1536 ;;
balanced) ctx=49152; margin=1536 ;;
vram-plus) ctx=49152; margin=1280 ;;
compact) ctx=32768; margin=1536 ;;
*) printf 'Unbekanntes Profil: %s\n' "$LLAMA_PROFILE" >&2; exit 2 ;;
esac
profile_args=(
--ctx-size "$ctx" --parallel 1 --jinja
--reasoning auto --reasoning-budget 512 --reasoning-preserve
--flash-attn auto --threads 6 --threads-batch 16
--n-gpu-layers auto --fit on --fit-target "$margin"
--cache-type-k q8_0 --cache-type-v q8_0
--sleep-idle-seconds 300 --image-min-tokens 1024
--cors-origins localhost --no-ui
)
# Alte vollständige LLAMA_ARGS nicht still mit einem Profil vermischen.
if ((${#LLAMA_ARGS[@]})); then
printf 'Bei Profilbetrieb LLAMA_ARGS leeren; eigene vollständige Args nur ohne LLAMA_PROFILE.\n' >&2
exit 2
fi
fi
args=("${model_args[@]}" --host "${LLAMA_HOST:-127.0.0.1}" --port "${LLAMA_PORT:-8080}"
"${profile_args[@]}" "${LLAMA_ARGS[@]}")
if [[ "$mode" == print ]]; then
printf '%q ' "$LLAMA_SERVER_BIN" "${args[@]}"
printf '\n'
exit 0
fi
# Kein Download/Modellstart: nur prüfen, ob die installierte Version die Namen kennt.
help_text="$(timeout 30 "$LLAMA_SERVER_BIN" --help 2>&1)" || {
printf 'llama-server --help fehlgeschlagen.\n' >&2; exit 1;
}
for arg in "${args[@]}"; do
[[ "$arg" == --* ]] || continue
option="${arg%%=*}"
if ! grep -Eq -- "(^|[[:space:],])${option}([[:space:],=]|$)" <<<"$help_text"; then
printf 'Die installierte llama.cpp-Version kennt %s nicht. Kein automatisches Weglassen.\n' "$option" >&2
exit 1
fi
done
if [[ "$mode" == check ]]; then
printf 'Konfiguration und Optionsnamen OK; GPU/Modell/Optionswerte noch nicht laufzeitgetestet.\n'
exit 0
fi
exec "$LLAMA_SERVER_BIN" "${args[@]}"
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -Euo pipefail
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
log() {
printf '%s\n' "$*" | systemd-cat -t hermes-stack-maintenance 2>/dev/null || true
}
text="${*:-}"
if [[ -z "$text" ]]; then
printf 'Verwendung: %s TEXT\n' "${0##*/}" >&2
exit 2
fi
log "$text"
if [[ ! -r "$CONFIG_FILE" ]]; then
log "Benachrichtigung übersprungen: $CONFIG_FILE ist nicht lesbar."
exit 0
fi
set +u
# shellcheck disable=SC1090
source "$CONFIG_FILE" || {
set -u
log "Benachrichtigung übersprungen: Maintenance-Konfiguration ist ungültig."
exit 0
}
telegram_notify="${TELEGRAM_NOTIFY:-0}"
signal_notify="${SIGNAL_NOTIFY:-0}"
if [[ -r "${HERMES_ENV:-}" ]] &&
[[ "$telegram_notify" == 1 || "$signal_notify" == 1 ]]; then
set -a
# shellcheck disable=SC1090
source "$HERMES_ENV" || true
set +a
fi
set -u
if [[ "$telegram_notify" == 1 &&
-n "${TELEGRAM_BOT_TOKEN:-}" &&
-n "${TELEGRAM_HOME_CHANNEL:-}" ]]; then
if ! curl --fail --silent --show-error --max-time 15 \
--request POST \
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
--data-urlencode "chat_id=${TELEGRAM_HOME_CHANNEL}" \
--data-urlencode "text=${text}" >/dev/null; then
log "Telegram-Benachrichtigung konnte nicht zugestellt werden."
fi
fi
if [[ "$signal_notify" == 1 &&
-n "${SIGNAL_HTTP_URL:-}" &&
-n "${SIGNAL_ACCOUNT:-}" &&
-n "${SIGNAL_HOME_CHANNEL:-}" ]]; then
if [[ "$SIGNAL_HOME_CHANNEL" == group:* ]]; then
signal_payload="$(
jq -nc --arg account "$SIGNAL_ACCOUNT" --arg message "$text" \
--arg group_id "${SIGNAL_HOME_CHANNEL#group:}" \
'{jsonrpc:"2.0",method:"send",params:{account:$account,message:$message,groupId:$group_id},id:"hermes-maintenance"}'
)" || signal_payload=""
else
signal_payload="$(
jq -nc --arg account "$SIGNAL_ACCOUNT" --arg message "$text" \
--arg recipient "$SIGNAL_HOME_CHANNEL" \
'{jsonrpc:"2.0",method:"send",params:{account:$account,message:$message,recipient:[$recipient]},id:"hermes-maintenance"}'
)" || signal_payload=""
fi
if [[ -z "$signal_payload" ]] ||
! curl --fail --silent --show-error --max-time 30 \
--header 'Content-Type: application/json' \
--data-binary "$signal_payload" \
"${SIGNAL_HTTP_URL%/}/api/v1/rpc" |
jq -e '((.error // null) == null) and (.result != null)' >/dev/null; then
log "Signal-Benachrichtigung konnte nicht zugestellt werden."
fi
fi
exit 0
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -Euo pipefail
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-maintenance"
NOTIFIER="$HOME/.local/libexec/hermes-stack-notify"
mkdir -p "$STATE_DIR"
# shellcheck disable=SC1090
source "$CONFIG_FILE"
set +u
set -a
# shellcheck disable=SC1090
source "$HERMES_ENV" || true
set +a
set -u
state_file="$STATE_DIR/signal-health-state"
signal_url="${SIGNAL_HTTP_URL:-}"
[[ -n "$signal_url" ]] || exit 0
check_signal() {
local status
status="$(curl --silent --output /dev/null --max-time 5 \
--write-out '%{http_code}' "${signal_url%/}/api/v1/check" || true)"
[[ "$status" =~ ^2[0-9][0-9]$ ]]
}
if check_signal; then
current=healthy
else
systemctl --user restart "$SIGNAL_SERVICE" || true
sleep 5
check_signal && current=healthy || current=unhealthy
fi
previous="$(cat "$state_file" 2>/dev/null || true)"
if [[ "$current" == unhealthy && "$previous" != unhealthy ]]; then
"$NOTIFIER" "Hermes-Stack: signal-cli ist nicht erreichbar; Neustart wurde versucht." || true
elif [[ "$current" == healthy && "$previous" == unhealthy ]]; then
"$NOTIFIER" "Hermes-Stack: signal-cli ist wieder erreichbar." || true
fi
printf '%s\n' "$current" >"$state_file"
[[ "$current" == healthy ]]
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/signal-cli.env"
# shellcheck disable=SC1090
source "$CONFIG_FILE"
[[ -x "$SIGNAL_CLI_BIN" ]] || {
printf 'signal-cli nicht ausführbar: %s\n' "$SIGNAL_CLI_BIN" >&2
exit 1
}
[[ -n "$SIGNAL_ACCOUNT" ]] || {
printf 'SIGNAL_ACCOUNT ist nicht konfiguriert.\n' >&2
exit 1
}
args=(--account "$SIGNAL_ACCOUNT")
if [[ -n "${SIGNAL_CONFIG_DIR:-}" ]]; then
mkdir -p "$SIGNAL_CONFIG_DIR"
args=(--config "$SIGNAL_CONFIG_DIR" "${args[@]}")
fi
exec "$SIGNAL_CLI_BIN" "${args[@]}" daemon \
--http "${SIGNAL_HTTP_BIND:-127.0.0.1:8090}"
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
SIGNAL_CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/signal-cli.env"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-maintenance"
NOTIFIER="$HOME/.local/libexec/hermes-stack-notify"
# shellcheck disable=SC1090
source "$CONFIG_FILE"
# shellcheck disable=SC1090
source "$SIGNAL_CONFIG_FILE"
version="${1:-}"
if [[ -z "$version" ]]; then
latest_url="$(curl -LsS --max-time 30 -o /dev/null -w '%{url_effective}' \
https://github.com/AsamK/signal-cli/releases/latest)"
version="${latest_url##*/v}"
fi
[[ "$version" =~ ^[0-9]+(\.[0-9]+)+$ ]] || {
printf 'Ungültige signal-cli-Version: %s\n' "$version" >&2
exit 1
}
old_version="unbekannt"
if [[ -x "$SIGNAL_CLI_BIN" ]]; then
old_version="$("$SIGNAL_CLI_BIN" --version 2>/dev/null |
grep -Eo '[0-9]+(\.[0-9]+)+' | head -n1 || true)"
fi
[[ -n "$old_version" ]] || old_version="unbekannt"
tmp_dir="$(mktemp -d)"
trap 'rm -rf -- "$tmp_dir"' EXIT
archive="$tmp_dir/signal-cli.tar.gz"
url="https://github.com/AsamK/signal-cli/releases/download/v${version}/signal-cli-${version}.tar.gz"
curl --fail --location --show-error "$url" -o "$archive"
tar -xzf "$archive" -C "$tmp_dir"
binary="$(find "$tmp_dir" -type f -path '*/bin/signal-cli' -print -quit)"
[[ -n "$binary" ]] || {
printf 'signal-cli-Binary wurde im Archiv nicht gefunden.\n' >&2
exit 1
}
source_root="$(dirname "$(dirname "$binary")")"
timestamp="$(date +%Y%m%d-%H%M%S)"
backup_dir="$STATE_DIR/backups/signal-cli/$timestamp"
mkdir -p "$backup_dir"
old_target=""
if [[ -e "$SIGNAL_CLI_BIN" || -L "$SIGNAL_CLI_BIN" ]]; then
old_target="$(readlink -f "$SIGNAL_CLI_BIN" 2>/dev/null || true)"
if [[ -n "$old_target" ]]; then
old_root="$(dirname "$(dirname "$old_target")")"
[[ -d "$old_root" ]] && cp -a "$old_root" "$backup_dir/program"
fi
fi
if [[ -d "${SIGNAL_CONFIG_DIR:-}" ]]; then
tar --zstd -cf "$backup_dir/signal-data.tar.zst" \
-C "$(dirname "$SIGNAL_CONFIG_DIR")" "$(basename "$SIGNAL_CONFIG_DIR")"
fi
was_active=0
systemctl --user is-active --quiet "$SIGNAL_SERVICE" && was_active=1
((was_active == 0)) || systemctl --user stop "$SIGNAL_SERVICE"
target="$HOME/.local/opt/signal-cli-$version"
mkdir -p "$target" "$(dirname "$SIGNAL_CLI_BIN")"
cp -a "$source_root/." "$target/"
ln -sfn "$target/bin/signal-cli" "$SIGNAL_CLI_BIN"
if ((was_active)); then
systemctl --user restart "$SIGNAL_SERVICE"
sleep 5
set +u
set -a
# shellcheck disable=SC1090
source "$HERMES_ENV" || true
set +a
set -u
signal_http_url="${SIGNAL_HTTP_URL:-}"
status=""
if [[ -n "$signal_http_url" ]]; then
status="$(curl --silent --output /dev/null --max-time 10 \
--write-out '%{http_code}' "${signal_http_url%/}/api/v1/check" || true)"
fi
if [[ ! "$status" =~ ^2[0-9][0-9]$ ]]; then
systemctl --user stop "$SIGNAL_SERVICE" || true
if [[ -n "$old_target" ]]; then
ln -sfn "$old_target" "$SIGNAL_CLI_BIN"
systemctl --user restart "$SIGNAL_SERVICE" || true
fi
"$NOTIFIER" "signal-cli-Update auf $version fehlgeschlagen; Binary wurde auf $old_version zurückgestellt. Backup: $backup_dir" || true
exit 1
fi
fi
mapfile -t old_backups < <(
find "$STATE_DIR/backups/signal-cli" -mindepth 1 -maxdepth 1 -type d \
-printf '%T@ %p\n' | sort -nr | tail -n +4 | cut -d' ' -f2-
)
((${#old_backups[@]} == 0)) || rm -rf -- "${old_backups[@]}"
"$NOTIFIER" "signal-cli wurde erfolgreich aktualisiert: $old_version -> $version. Daten- und Binary-Backup: $backup_dir" || true
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -Euo pipefail
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-maintenance"
NOTIFIER="$HOME/.local/libexec/hermes-stack-notify"
mkdir -p "$STATE_DIR"
command -v signal-cli >/dev/null 2>&1 || exit 0
installed="$(signal-cli --version 2>/dev/null | grep -Eo '[0-9]+(\.[0-9]+)+' | head -n1)"
latest_url="$(curl -LsS --max-time 30 -o /dev/null -w '%{url_effective}' \
https://github.com/AsamK/signal-cli/releases/latest || true)"
latest="${latest_url##*/v}"
[[ "$installed" =~ ^[0-9]+(\.[0-9]+)+$ ]] || exit 1
[[ "$latest" =~ ^[0-9]+(\.[0-9]+)+$ ]] || exit 1
if [[ "$(printf '%s\n%s\n' "$installed" "$latest" | sort -V | tail -n1)" == "$latest" &&
"$installed" != "$latest" ]]; then
"$NOTIFIER" "signal-cli-Update verfügbar: $installed -> $latest. Manuell ausführen: ~/.local/libexec/hermes-stack-signal-update $latest" || true
fi