#!/bin/sh
# shellcheck disable=SC1111 # deutsche Anführungszeichen
# bashclub-be: Boot Environments (Root-Dataset unter <pool>/ROOT/) sichern
# und prüfen, ob ein Neustart sicher ist.
#
#   bashclub-be snapshot [--apt] [<name>]
#       Snapshot des laufenden Boot Environments, rekursiv:
#       <bootfs>@<name>, ohne Namen @manual-<datum>. --apt: vor einem
#       dpkg-Lauf (APT-Hook, Name apt-<datum>), nur einmal je apt-Aufruf.
#   bashclub-be list
#       Boot Environments und ihre Snapshots
#   bashclub-be prune
#       Snapshots aus dem APT-Hook über KEEP_APT hinaus löschen (älteste
#       zuerst); eigene Snapshots bleiben
#   bashclub-be check-reboot
#       Reboot-Guard: startet der neueste Kernel mit ZFS? Rückgabe 0 = ja,
#       1 = nein (Gründe auf stderr), Ergebnis auch in /run/bashclub-be/
#   bashclub-be prepare [--shutdown]
#       Probe-Boot vorbereiten, automatisch bei jedem Herunterfahren
#       (bashclub-be-prepare.service), egal ob reboot, poweroff oder
#       unattended-upgrades: Der Stand, mit dem das System gestartet ist
#       (erster Snapshot dieses Starts), wird als BE
#       <pool>/ROOT/fallback-<datum> geklont und bootfs; das laufende BE
#       startet einmal zur Probe (ZBM-Hooks aus bashclub-zfsbootmenu).
#       Bestätigt es nicht (Panic, Hänger mit Watchdog, Reset), startet beim
#       nächsten Mal das Rückfall-BE. Ohne Änderung seit dem Start oder ohne
#       Probe-fähiges ZFSBootMenu auf allen ESPs: normaler Neustart.
#   bashclub-be reboot [--no-probe]
#       Neustart nur, wenn der Reboot-Guard zustimmt; --no-probe: diesmal
#       ohne Probe-Boot. Beginnt das Herunterfahren nicht (Auftrag bei einem
#       daemon-reexec verloren), wird er wiederholt
#   bashclub-be confirm
#       beim Start (bashclub-be-confirm.service): Probe bestätigen (bootfs
#       auf das neue BE, Rückfall-BEs löschen), sobald die Prüfungen aus
#       probe-check.d bestehen (Netz: Default-Gateway erreichbar), höchstens
#       PROBE_CHECK_WAIT Sekunden lang (auto: nach Startdauer, 120–300 s);
#       sonst Neustart ins Rückfall-BE. Oder den Rückfall
#       melden (/var/lib/bashclub-be/probe-failed, Rückgabe 1); misst
#       außerdem bei jedem Start die Startdauer (WATCHDOG_SEC=auto). Ein
#       Rückfall steht bis zum Quittieren in motd und /etc/issue.d
#   bashclub-be ack
#       Rückfall quittieren: Meldung in motd und Anmeldebildschirm entfernen
#   bashclub-be status
#       laufendes BE, bootfs, Probe-Zustand, Probe-Fenster, letzter Rückfall
#   bashclub-be check-probe
#       Prüfungen aus probe-check.d einmal ausführen (wie bei der Bestätigung)
#   bashclub-be apt-check
#       APT-Hook (DPkg::Pre-Install-Pkgs): bricht ab, wenn ein Kernel ohne
#       passendes ZFS-Modul (openzfs-zfs-modules-<abi>) installiert würde
#
# Prüfungen für die Bestätigung: ausführbare Dateien in
# /usr/lib/bashclub-be/probe-check.d/ und /etc/bashclub/probe-check.d/
# (gleicher Name in /etc ersetzt den aus /usr/lib, nicht ausführbar oder
# Link auf /dev/null schaltet ab). Rückgabe 0 = bestanden, sonst letzte
# Ausgabezeile als Grund.
#
# Zurück zu einem Snapshot: im Bootmenü (ZFSBootMenu) das Boot Environment
# wählen, Strg+S zeigt seine Snapshots; daraus ein neues BE klonen oder
# zurückrollen.
# Einstellungen: /etc/bashclub/boot-env.conf
set -eu

KEEP_APT=10
PROBE=yes
WATCHDOG_SEC=auto
PROBE_CHECKS=yes
PROBE_CHECK_WAIT=auto
KERNEL_GUARD=yes
CONF=${BASHCLUB_BE_CONF:-/etc/bashclub/boot-env.conf}
# shellcheck source=/dev/null
[ -r "$CONF" ] && . "$CONF"
STATE=${BASHCLUB_BE_STATE:-/run/bashclub-be}
PROP=org.bashclub:be
BOOT_PROP=org.bashclub:boot
LIB=${BASHCLUB_BE_LIB:-/var/lib/bashclub-be}
ZBM_FEATURES=/usr/lib/bashclub-zfsbootmenu/features
# Zustand des Probe-Boots auf jeder ESP, Format siehe Hooks in
# bashclub-zfsbootmenu: <neues BE>|<altes BE>|<versuch>|<watchdog-sekunden>
PROBE_FILE=bashclub/probe
ESP_TYPE=c12a7328-f81f-11d2-ba4b-00a0c93ec93b
# Hinweis auf einen Rückfall: Text aus der motd von bashclub-base (übersetzt),
# Fassung für den Anmeldebildschirm der Konsole (agetty liest /etc/issue.d)
NOTICE=/etc/update-motd.d/30-bashclub-be
ISSUE=${BASHCLUB_BE_ISSUE:-/etc/issue.d/bashclub-be.issue}
# WATCHDOG_SEC=auto: das Dreifache der längsten der letzten BOOT_TIMES
# gemessenen Startdauern, begrenzt auf WATCHDOG_MIN..WATCHDOG_MAX, ohne
# Messung WATCHDOG_DEFAULT
BOOT_TIMES=5
WATCHDOG_MIN=120
WATCHDOG_MAX=900
WATCHDOG_DEFAULT=600
# PROBE_CHECK_WAIT=auto: ebenso aus den Startdauern, aber kürzer gedeckelt
# (ein Laptop ohne Netz soll nicht lange warten); ohne Messung das Maximum
CHECK_WAIT_MIN=120
CHECK_WAIT_MAX=300
# prepare --shutdown: so viele Sekunden nach dem Zustand von systemd fragen,
# solange es keinen bekannten meldet (daemon-reexec, siehe system_state)
SHUTDOWN_WAIT=${BASHCLUB_BE_SHUTDOWN_WAIT:-30}
# Neustart: so viele Sekunden auf den Beginn des Herunterfahrens warten (logind
# wartet bis InhibitDelayMaxSec, mit unattended-upgrades 30 s), sonst erneut
# anfordern, höchstens REBOOT_TRIES-mal (siehe request_reboot)
REBOOT_WAIT=${BASHCLUB_BE_REBOOT_WAIT:-60}
REBOOT_TRIES=3
# Kernel und initramfs (Reboot-Guard); anders nur in Tests
BOOT=${BASHCLUB_BE_BOOT:-/boot}

CHECK_DIRS="/usr/lib/bashclub-be/probe-check.d /etc/bashclub/probe-check.d"
# Grund einer gescheiterten Prüfung für das Rückfall-BE: /var/log liegt
# außerhalb der Boot Environments, beide sehen dieselbe Datei
CHECK_FAILED=${BASHCLUB_BE_CHECK_FAILED:-/var/log/bashclub-be/probe-check-failed}

say() { printf '%s\n' "$*"; }
warn() { printf 'bashclub-be: %s\n' "$*" >&2; }

# Laufendes Boot Environment: das ZFS-Dataset hinter /
current() {
	[ "$(findmnt -n -o FSTYPE / 2>/dev/null)" = zfs ] || return 1
	findmnt -n -o SOURCE /
}

snapshot() {
	kind=manual
	if [ "${1:-}" = --apt ]; then kind=apt; shift; fi
	# Im Installer (chroot) und ohne ZFS-Root gibt es nichts zu sichern
	[ -z "${BASHCLUB_INSTALL:-}" ] || exit 0
	be=$(current) || { say "Root liegt nicht auf ZFS, kein Snapshot."; exit 0; }
	if [ $kind = apt ]; then
		# apt ruft dpkg je Lauf mehrmals auf (Pre-Invoke vor jedem Aufruf):
		# nur ein Snapshot je apt-Prozess (Großeltern dieses Aufrufs: sh -c ← apt)
		apt=$(ps -o ppid= -p "$PPID" 2>/dev/null | tr -d ' ')
		mkdir -p "$STATE"
		if [ -n "$apt" ] && [ -d "/proc/$apt" ] && [ "$(cat "$STATE/apt-pid" 2>/dev/null)" = "$apt" ]; then
			exit 0
		fi
		printf '%s\n' "$apt" >"$STATE/apt-pid"
	fi
	name=${1:-$kind-$(date +%Y%m%d-%H%M%S)}
	# Start, in dem der Snapshot entstand: der erste je Start ist der Stand,
	# mit dem das System gestartet ist (Rückfall beim Probe-Boot)
	zfs snapshot -r -o "$PROP=$kind" -o "$BOOT_PROP=$(cat /proc/sys/kernel/random/boot_id)" "$be@$name"
	say "Snapshot $be@$name"
	if [ $kind = apt ]; then prune; fi
}

prune() {
	be=$(current) || exit 0
	# Snapshots des Hooks, älteste zuerst; die neuesten KEEP_APT bleiben
	zfs list -H -t snapshot -o name,"$PROP" -s createtxg -d 1 "$be" | awk -v p="$be@" '$2 == "apt" && index($1, p) == 1 {print $1}' >"${TMPDIR:-/tmp}/bashclub-be.$$"
	total=$(wc -l <"${TMPDIR:-/tmp}/bashclub-be.$$")
	head -n "$((total > KEEP_APT ? total - KEEP_APT : 0))" "${TMPDIR:-/tmp}/bashclub-be.$$" | while read -r snap; do
		# Ein Snapshot, aus dem ein BE geklont wurde, lässt sich nicht löschen
		if zfs destroy -r "$snap" 2>/dev/null; then say "gelöscht: $snap"; else warn "$snap bleibt (in Gebrauch)"; fi
	done
	rm -f "${TMPDIR:-/tmp}/bashclub-be.$$"
}

list() {
	pool=$(zpool list -H -o name,bootfs | awk '$2 != "-" {print $1; exit}')
	[ -n "$pool" ] || { say "Kein Boot-Pool (Pool mit bootfs) gefunden."; exit 0; }
	bootfs=$(zpool get -H -o value bootfs "$pool")
	running=$(current || true)
	zfs list -H -o name -d 1 -t filesystem "$pool/ROOT" | sed 1d | while read -r be; do
		mark=""
		[ "$be" = "$bootfs" ] && mark=" (bootfs)"
		[ "$be" = "$running" ] && mark="$mark (läuft)"
		say "$be$mark"
		zfs list -H -t snapshot -o name,creation,"$PROP" -s createtxg -d 1 "$be" | awk -F '\t' '{sub(/^[^@]*/, "", $1); printf "  %-28s %s  %s\n", $1, $2, ($3 == "-" ? "" : $3)}'
	done
}

# Reboot-Guard: Gründe sammeln, die gegen einen Neustart sprechen
check_reboot() {
	reasons=""
	block() { reasons="$reasons$1
"; }
	# Jeder Kernel in /boot, nicht nur der neueste: ZFSBootMenu startet den
	# höchsten nach eigener Sortierung (sort -V, -rt-amd64 vor -amd64), von
	# Hand ist jeder wählbar
	kernels=$(linux-version list 2>/dev/null || true)
	if [ -z "$kernels" ]; then
		block "kein Kernel installiert"
	fi
	for kernel in $kernels; do
		[ -f "$BOOT/vmlinuz-$kernel" ] || block "Kernel $kernel: $BOOT/vmlinuz-$kernel fehlt"
		modinfo -k "$kernel" -F filename zfs >/dev/null 2>&1 || block "Kernel $kernel: kein ZFS-Modul (openzfs-zfs-modules-$kernel; fremder Kernel? entfernen)"
		if [ ! -f "$BOOT/initrd.img-$kernel" ]; then
			block "Kernel $kernel: initramfs fehlt"
		elif ! lsinitramfs "$BOOT/initrd.img-$kernel" 2>/dev/null | grep -q '/zfs\.ko'; then
			block "Kernel $kernel: ZFS fehlt im initramfs (update-initramfs -u -k $kernel)"
		fi
	done
	# shellcheck disable=SC2086 # Liste, durch Leerzeichen getrennt
	kernel=$(printf '%s\n' $kernels | sort -V | tail -n 1)
	# Ohne ZFSBootMenu auf einer ESP startet nichts mehr
	if command -v bashclub-zbm-update >/dev/null 2>&1; then
		bashclub-zbm-update --list 2>/dev/null | grep -q -e ': aktuell' -e ': veraltet' || block "keine ESP mit ZFSBootMenu gefunden (bashclub-zbm-update --list)"
	fi
	mkdir -p "$STATE"
	if [ -n "$reasons" ]; then
		printf '%s' "$reasons" >"$STATE/reboot-blocked"
		printf '%s' "$reasons" | sed 's/^/bashclub-be: Neustart nicht sicher: /' >&2
		return 1
	fi
	rm -f "$STATE/reboot-blocked"
	say "Neustart sicher: Kernel $kernel mit ZFS-Modul im initramfs, ZFSBootMenu vorhanden."
}

boot_pool() { zpool list -H -o name,bootfs 2>/dev/null | awk '$2 != "-" {print $1; exit}'; }

# ESPs auf den Platten des Boot-Pools (wie bashclub-zbm-update)
esps() {
	zpool list -vHPL "$(boot_pool)" | awk '$1 ~ /^\/dev\// {print $1}' | while read -r dev; do
		disk=$(lsblk -dno PKNAME "$dev" 2>/dev/null) || continue
		[ -n "$disk" ] || continue
		lsblk -lnpo NAME,PARTTYPE "/dev/$disk" | awk -v t=$ESP_TYPE 'tolower($2) == t {print $1}'
	done | sort -u
}

# Zustand des Probe-Boots auf den ESPs: write <zeile> (alle) | read (erste
# gefundene) | clear (alle)
on_esps() {
	mnt=$(mktemp -d /run/bashclub-be-esp.XXXXXX)
	ok=0
	for esp in $(esps); do
		mount -t vfat "$esp" "$mnt" 2>/dev/null || { warn "$esp lässt sich nicht einhängen"; continue; }
		case $1 in
		write)
			mkdir -p "$mnt/${PROBE_FILE%/*}"
			if printf '%s\n' "$2" >"$mnt/$PROBE_FILE.new" && mv -f "$mnt/$PROBE_FILE.new" "$mnt/$PROBE_FILE"; then ok=$((ok + 1)); fi
			;;
		read) if [ -f "$mnt/$PROBE_FILE" ]; then head -n 1 "$mnt/$PROBE_FILE"; ok=1; fi ;;
		clear) rm -f "$mnt/$PROBE_FILE" "$mnt/$PROBE_FILE.new" ;;
		esac
		sync
		umount "$mnt"
		if [ "$1" = read ] && [ $ok = 1 ]; then break; fi
	done
	rmdir "$mnt"
	[ "$1" = clear ] || [ $ok -gt 0 ]
}

# Probe nur, wenn jede ESP das ZFSBootMenu dieses Pakets trägt und es die
# Hooks hat: Startete die Firmware ein ZBM ohne Hooks, liefe ohne Not das
# Rückfall-BE
probe_ready() {
	grep -qx probe-boot "$ZBM_FEATURES" 2>/dev/null || { warn "ZFSBootMenu ohne Probe-Boot (bashclub-zfsbootmenu zu alt)"; return 1; }
	# Zeilen „<esp>: aktuell“, mit BIOS-Einrichtung „<esp>: aktuell, BIOS“
	states=$(bashclub-zbm-update --list 2>/dev/null | grep -E ': (aktuell|veraltet|fehlt)(,|$)' || true)
	if [ -z "$states" ] || printf '%s\n' "$states" | grep -qvE ': aktuell(,|$)'; then
		warn "nicht jede ESP hat das aktuelle ZFSBootMenu (bashclub-zbm-update)"
		return 1
	fi
}

# Probe-Boot vorbereiten: Stand dieses Starts (erster Snapshot mit der
# aktuellen boot_id) als Rückfall-BE klonen, bootfs darauf, Zustand auf die
# ESPs. Läuft beim Herunterfahren (bashclub-be-prepare.service), also bei
# jedem Neustart und Ausschalten, egal wie ausgelöst
prepare() {
	# Nur beim Herunterfahren, nicht wenn die Unit bei einem Paket-Update
	# oder von Hand gestoppt wird
	if [ "${1:-}" = --shutdown ]; then
		if ! state=$(system_state); then
			mkdir -p "$LIB"
			note "Neustart ohne Probe: Zustand von systemd nach $SHUTDOWN_WAIT s unbekannt ($state)"
			return 0
		fi
		[ "$state" = stopping ] || return 0
	fi
	be=$(current) || return 0
	pool=$(boot_pool)
	mkdir -p "$LIB"
	if [ -e "$STATE/no-probe" ]; then
		note "Neustart ohne Probe (bashclub-be reboot --no-probe)"
		return 0
	fi
	if [ "$PROBE" != yes ]; then
		note "Neustart ohne Probe (PROBE=$PROBE)"
		return 0
	fi
	if on_esps read >/dev/null 2>&1; then
		return 0 # Probe läuft schon (Neustart vor der Bestätigung)
	fi
	good=$(zfs list -H -t snapshot -o name,"$BOOT_PROP" -s createtxg -d 1 "$be" |
		awk -v b="$(cat /proc/sys/kernel/random/boot_id)" '$2 == b {print $1; exit}')
	if [ -z "$good" ]; then
		note "Keine Änderung seit dem Start: Neustart ohne Probe."
		return 0
	fi
	probe_ready || { note "Neustart ohne Probe (ZFSBootMenu ohne Probe-Boot)"; return 0; }
	check_reboot >/dev/null 2>&1 || warn "Reboot-Guard meldet Probleme (bashclub-be check-reboot); die Probe fängt sie ab"
	fallback=${be%/*}/fallback-$(date +%Y%m%d-%H%M%S)
	zfs clone -o canmount=noauto -o mountpoint=/ -o "$PROP=fallback" "$good" "$fallback"
	wdsec=$(watchdog_sec)
	if on_esps write "$be|$fallback|0|$wdsec"; then
		zpool set bootfs="$fallback" "$pool"
		note "Probe-Boot: $be startet einmal zur Probe, Rückfall auf $fallback (Stand $good), Watchdog $wdsec s."
	else
		warn "Zustand auf keiner ESP geschrieben: Neustart ohne Probe"
		zfs destroy "$fallback"
	fi
}

# Zustand von systemd (systemctl is-system-running). Führt systemd sich
# gerade neu aus (daemon-reexec, etwa needrestart aus unattended-upgrades,
# das beim Herunterfahren weiterläuft), ist es kurz nicht erreichbar und
# systemctl meldet „unknown“: dann jede Sekunde erneut, höchstens
# SHUTDOWN_WAIT-mal. Rückgabe 1, wenn kein bekannter Zustand kam
system_state() {
	tries=0
	while :; do
		s=$(systemctl is-system-running 2>/dev/null || true)
		case $s in initializing | starting | running | degraded | maintenance | stopping)
			say "$s"
			return 0
			;;
		esac
		tries=$((tries + 1))
		if [ "$tries" -ge "$SHUTDOWN_WAIT" ]; then
			say "${s:-keine Antwort}"
			return 1
		fi
		sleep 1
	done
}

# Meldung auf stdout, ins Journal und als letzte Vorbereitung (lib/last-prepare,
# landet im laufenden BE und ist nach der Probe dort lesbar)
note() {
	say "$*"
	printf '%s: %s\n' "$(date '+%Y-%m-%d %H:%M')" "$*" >"$LIB/last-prepare" 2>/dev/null || true
	logger -t bashclub-be -p notice "$*" 2>/dev/null || true
}

# Neustart über den Reboot-Guard; den Probe-Boot bereitet das Herunterfahren vor
reboot_() {
	check_reboot || exit 1
	if [ "${1:-}" = --no-probe ]; then
		mkdir -p "$STATE"
		: >"$STATE/no-probe"
	fi
	request_reboot || exit 1
}

# Neustart anfordern und prüfen, ob das Herunterfahren beginnt. logind
# bestätigt systemctl reboot sofort, wartet auf Verzögerungs-Sperren
# (InhibitDelayMaxSec) und startet erst dann reboot.target. Führt sich
# systemd in diesem Moment neu aus (daemon-reexec, etwa durch needrestart
# aus unattended-upgrades), geht der Auftrag verloren; logind meldet nur
# „Error during inhibitor-delayed operation (already returned success to
# client)“. Darum bis REBOOT_WAIT Sekunden auf „stopping“ warten, sonst
# erneut anfordern. Argumente gehen an systemctl (z. B. --no-block)
request_reboot() {
	n=0
	while [ "$n" -lt "$REBOOT_TRIES" ]; do
		n=$((n + 1))
		systemctl "$@" reboot || warn "systemctl reboot gescheitert (Versuch $n)"
		waited=0
		while [ "$waited" -lt "$REBOOT_WAIT" ]; do
			if [ "$(system_state)" = stopping ]; then return 0; fi
			sleep 2
			waited=$((waited + 2))
		done
		warn "Herunterfahren nach $REBOOT_WAIT s nicht begonnen (Versuch $n von $REBOOT_TRIES)"
		logger -t bashclub-be -p warning "Neustart angefordert, Herunterfahren nach $REBOOT_WAIT s nicht begonnen (Versuch $n von $REBOOT_TRIES)" 2>/dev/null || true
	done
	warn "Neustart nicht begonnen"
	return 1
}

# Probe-Fenster in Sekunden: fester Wert aus WATCHDOG_SEC oder (auto) aus
# den gemessenen Startdauern
watchdog_sec() {
	case $WATCHDOG_SEC in
	auto) ;;
	*[!0-9]* | '') warn "WATCHDOG_SEC=$WATCHDOG_SEC ungültig, nehme auto" ;;
	*) say "$WATCHDOG_SEC"; return ;;
	esac
	from_boot_times $WATCHDOG_MIN $WATCHDOG_MAX $WATCHDOG_DEFAULT
}

# Frist der Prüfungen vor der Bestätigung: fester Wert oder (auto) aus den
# gemessenen Startdauern
check_wait() {
	case $PROBE_CHECK_WAIT in
	auto) ;;
	*[!0-9]* | '') warn "PROBE_CHECK_WAIT=$PROBE_CHECK_WAIT ungültig, nehme auto" ;;
	*) say "$PROBE_CHECK_WAIT"; return ;;
	esac
	from_boot_times $CHECK_WAIT_MIN $CHECK_WAIT_MAX $CHECK_WAIT_MAX
}

# Dreifache längste Startdauer, begrenzt auf <min>..<max>, ohne Messung <default>
from_boot_times() {
	if [ ! -s "$LIB/boot-times" ]; then say "$3"; return; fi
	awk -v min="$1" -v max="$2" -v def="$3" '
		$1 ~ /^[0-9]+$/ && $1 > m {m = $1}
		END {
			if (m == 0) {print def; exit}
			s = 3 * m
			print (s < min ? min : (s > max ? max : s))
		}' "$LIB/boot-times"
}

# Startdauer bis hierher (multi-user.target): Uptime zählt ab dem kexec aus
# ZFSBootMenu, also ab dem Start des Probe-Fensters; die letzten BOOT_TIMES
# Werte bleiben. Nur einmal je Start (Unit neu gestartet, Aufruf von Hand)
record_boot_time() {
	[ ! -e "$STATE/boot-time" ] || return 0
	mkdir -p "$LIB" "$STATE"
	: >"$STATE/boot-time"
	up=$(cut -d. -f1 /proc/uptime)
	{ tail -n $((BOOT_TIMES - 1)) "$LIB/boot-times" 2>/dev/null || true; say "$up"; } >"$LIB/boot-times.new"
	mv -f "$LIB/boot-times.new" "$LIB/boot-times"
}

# Anmeldebildschirm passend zu probe-failed: Meldung schreiben oder entfernen
issue_sync() {
	if [ -s "$LIB/probe-failed" ]; then
		mkdir -p "${ISSUE%/*}"
		if [ -x "$NOTICE" ] && "$NOTICE" --issue >"$ISSUE.new" 2>/dev/null && [ -s "$ISSUE.new" ]; then
			mv -f "$ISSUE.new" "$ISSUE"
		else
			rm -f "$ISSUE.new"
			printf '\\e{red}\\e{bold}  ! %s\\e{reset}\n    bashclub-be status\n\n' "$(head -n 1 "$LIB/probe-failed")" >"$ISSUE"
		fi
	else
		rm -f "$ISSUE"
	fi
	# Bereits angezeigte Anmeldebildschirme neu ausgeben
	agetty --reload >/dev/null 2>&1 || true
}

ack() {
	if [ -s "$LIB/probe-failed" ]; then
		say "quittiert: $(cat "$LIB/probe-failed")"
		rm -f "$LIB/probe-failed"
	else
		say "Kein Rückfall gemeldet."
	fi
	issue_sync
}

confirm() {
	record_boot_time || warn "Startdauer nicht gespeichert"
	if ! line=$(on_esps read); then
		issue_sync
		exit 0
	fi
	IFS='|' read -r new old tries _wd <<LINE
$line
LINE
	running=$(current || true)
	mkdir -p "$LIB"
	if [ "$running" = "$new" ] && [ "$tries" = 1 ]; then
		# Gestartet: Probe-Fenster schließen (Watchdog-Treiber laden, er hält
		# den Timer an), die Prüfungen haben ihre eigene Frist
		wd=$(sed -n 's/.*bashclub\.probe_wd=\([^ ]*\).*/\1/p' /proc/cmdline)
		for m in $(printf '%s' "$wd" | tr ',' ' '); do modprobe "$m" || true; done
		if ! wait_checks; then
			# Zustand bleibt auf den ESPs (Versuch 1): ZFSBootMenu startet beim
			# nächsten Mal das alte BE, das Herunterfahren bereitet keine neue
			# Probe vor (prepare sieht den Zustand)
			msg="Prüfung nach $(check_wait) s nicht bestanden: $(printf '%s' "$failed_checks" | tr '\n' ';' | sed 's/;$//')"
			mkdir -p "${CHECK_FAILED%/*}"
			printf '%s\n' "$msg" >"$CHECK_FAILED"
			warn "$msg; Neustart ins Rückfall-BE $old"
			logger -t bashclub-be -p err "$msg; Neustart ins Rückfall-BE $old" || true
			request_reboot --no-block || true
			exit 1
		fi
		on_esps clear
		zpool set bootfs="$new" "$(boot_pool)"
		rm -f "$LIB/probe-failed" "$CHECK_FAILED"
		issue_sync
		# Läuft ein früheres Rückfall-BE (nach einem Rückfall weitergearbeitet),
		# ist es jetzt der Hauptstand: Markierung weg, damit es nie als altes
		# Rückfall-BE aufgeräumt wird
		if [ "$(zfs get -H -o value "$PROP" "$new")" = fallback ]; then
			zfs inherit "$PROP" "$new"
		fi
		# Ist es ein Klon (Rückfall-BE), übernimmt es die gemeinsamen Snapshots
		# (promote): erst dann lassen sich die Stände löschen, aus denen es
		# entstanden ist
		if [ "$(zfs get -H -o value origin "$new")" != - ]; then
			zfs promote "$new" || warn "zfs promote $new gescheitert"
		fi
		# Rückfall-BEs löschen, auch das eben angelegte: Der neue Stand ist
		# durchgestartet, zurück geht es weiter über die Snapshots (der Stand
		# vor dem Update bleibt als Snapshot im BE). Das laufende bleibt immer
		zfs list -H -o name,"$PROP" -d 1 -t filesystem "${new%/*}" |
			awk -v running="$new" '$2 == "fallback" && $1 != running {print $1}' |
			while read -r fs; do zfs destroy -r "$fs" && say "gelöscht: $fs"; done
		say "Probe-Boot bestätigt: $new ist bootfs."
		logger -t bashclub-be -p notice "Probe-Boot bestätigt: $new ist bootfs" || true
	else
		on_esps clear
		msg="$(date '+%Y-%m-%d %H:%M'): Probe von $new nicht bestätigt (Zustand $tries), läuft auf ${running:-?}; bootfs bleibt $old"
		# Grund aus der Probe, falls eine Prüfung scheiterte
		if [ -s "$CHECK_FAILED" ]; then
			msg="$msg; $(cat "$CHECK_FAILED")"
			rm -f "$CHECK_FAILED"
		fi
		printf '%s\n' "$msg" >"$LIB/probe-failed"
		issue_sync
		warn "$msg"
		logger -t bashclub-be -p err "$msg" || true
		exit 1
	fi
}

# Prüfungen einmal ausführen; Gründe der gescheiterten in failed_checks
run_checks() {
	failed_checks=""
	names=$(for d in $CHECK_DIRS; do ls "$d" 2>/dev/null; done | sort -u)
	for n in $names; do
		f=""
		for d in $CHECK_DIRS; do
			if [ -e "$d/$n" ] || [ -L "$d/$n" ]; then f=$d/$n; fi
		done
		if [ ! -f "$f" ] || [ ! -x "$f" ]; then continue; fi
		if ! out=$(timeout 60 "$f" 2>&1); then
			failed_checks="$failed_checks$n: $(printf '%s\n' "$out" | tail -n 1)
"
		fi
	done
	[ -z "$failed_checks" ]
}

# Bis zur Frist (check_wait) alle 10 s prüfen: kurz fehlendes Netz
# (DHCP, Gateway startet mit) führt nicht zum Rückfall
wait_checks() {
	failed_checks=""
	[ "$PROBE_CHECKS" = yes ] || return 0
	end=$(($(cut -d. -f1 /proc/uptime) + $(check_wait)))
	while ! run_checks; do
		[ "$(cut -d. -f1 /proc/uptime)" -lt "$end" ] || return 1
		sleep 10
	done
}

check_probe() {
	if run_checks; then
		say "Prüfungen bestanden."
	else
		printf '%s' "$failed_checks" | sed 's/^/bashclub-be: nicht bestanden: /' >&2
		return 1
	fi
}

# APT-Hook vor dpkg (DPkg::Pre-Install-Pkgs, Paketdateien auf stdin): ein
# Kernel-Image (linux-image-<abi> mit /boot/vmlinuz-<abi>) nur, wenn
# openzfs-zfs-modules-<abi> schon installiert ist oder mitkommt. Fremde
# Kernel (rt, cloud, Backports ohne unser ZFS-Modul) startet ZFSBootMenu
# sonst womöglich als neuesten, ohne ZFS bootet nichts
apt_check() {
	[ "$KERNEL_GUARD" = yes ] || { cat >/dev/null; return 0; }
	debs=$(grep '\.deb$' || true)
	names=$(printf '%s\n' "$debs" | while read -r deb; do
		if [ -n "$deb" ]; then dpkg-deb -f "$deb" Package 2>/dev/null || true; fi
	done)
	bad=""
	for deb in $debs; do
		pkg=$(dpkg-deb -f "$deb" Package 2>/dev/null) || continue
		case $pkg in linux-image-*) ;; *) continue ;; esac
		abi=${pkg#linux-image-}
		dpkg-deb -c "$deb" 2>/dev/null | grep -q "/boot/vmlinuz-$abi\$" || continue
		printf '%s\n' "$names" | grep -qx "openzfs-zfs-modules-$abi" && continue
		[ "$(dpkg-query -W -f '${db:Status-Status}' "openzfs-zfs-modules-$abi" 2>/dev/null)" = installed ] && continue
		bad="$bad $pkg"
	done
	[ -z "$bad" ] && return 0
	for pkg in $bad; do
		warn "$pkg: kein ZFS-Modul (openzfs-zfs-modules-${pkg#linux-image-}), mit diesem Kernel startet das System nicht"
	done
	warn "Installation abgebrochen. Kernel kommen mit bashclub-zfs-kernel; abschalten (auf eigene Gefahr): KERNEL_GUARD=no in $CONF"
	return 1
}

status() {
	pool=$(boot_pool)
	say "läuft: $(current || echo '-')"
	say "bootfs: $(zpool get -H -o value bootfs "$pool" 2>/dev/null || echo '-')"
	say "Probe: $(on_esps read 2>/dev/null || echo 'keine')"
	times=$(tr '\n' ' ' <"$LIB/boot-times" 2>/dev/null || true)
	say "Probe-Fenster: $(watchdog_sec) s (WATCHDOG_SEC=$WATCHDOG_SEC, Startdauern: ${times:-keine})"
	if [ -s "$LIB/last-prepare" ]; then say "Letzter Neustart: $(cat "$LIB/last-prepare")"; fi
	if [ "$PROBE_CHECKS" = yes ]; then
		say "Prüfungen vor der Bestätigung: $(for d in $CHECK_DIRS; do ls "$d" 2>/dev/null; done | sort -u | tr '\n' ' ')(Frist $(check_wait) s, PROBE_CHECK_WAIT=$PROBE_CHECK_WAIT)"
	else
		say "Prüfungen vor der Bestätigung: aus (PROBE_CHECKS=$PROBE_CHECKS)"
	fi
	if [ -s "$LIB/probe-failed" ]; then say "Rückfall: $(cat "$LIB/probe-failed") (quittieren: bashclub-be ack)"; fi
}

case "${1:-}" in
snapshot) shift; snapshot "$@" ;;
prune) prune ;;
list) list ;;
check-reboot) check_reboot ;;
reboot) shift; reboot_ "$@" ;;
prepare) shift; prepare "$@" ;;
confirm) confirm ;;
status) status ;;
ack) ack ;;
check-probe) check_probe ;;
apt-check) apt_check ;;
-h | --help) sed -n '3,/^set -eu/{/^set -eu/d;s/^# \{0,1\}//;p}' "$0" ;;
*) echo "Aufruf: bashclub-be snapshot [--apt] [<name>] | list | prune | check-reboot | reboot [--no-probe] | prepare | confirm | status | ack | check-probe | apt-check" >&2; exit 2 ;;
esac
