diff --git a/zfs-snapshot.sh b/zfs-snapshot.sh index c83cada..9a0734b 100755 --- a/zfs-snapshot.sh +++ b/zfs-snapshot.sh @@ -60,10 +60,23 @@ fi msg() { whiptail --title "$APP_TITLE" --msgbox "$1" "${2:-12}" "${3:-72}"; } yesno(){ whiptail --title "$APP_TITLE" --yesno "$1" "${2:-12}" "${3:-72}"; } -# Show arbitrary text in a scrollable box. +# Display the stdout of a producer command/function in a scrollable box. +# +# The producer runs with `set -e` disabled and its exit status discarded, so a +# non-zero result from zfs / awk / pipefail can never abort the whole TUI — this +# is what made "View all snapshots" crash back to the shell. The captured report +# is shown via --textbox (which reads from a file) instead of --msgbox (which +# takes the entire report as one argv string and chokes on large snapshot lists). show() { - local title="$1" body="$2" - whiptail --title "$title" --scrolltext --msgbox "${body:-(no output)}" 24 90 + local title="$1"; shift + local out tmp + set +e + out="$("$@" 2>&1)" + set -e + tmp="$(mktemp)" + printf '%s\n' "${out:-(no output)}" >"$tmp" + whiptail --title "$title" --scrolltext --textbox "$tmp" 24 90 || true + rm -f "$tmp" } # Run a command, capturing output, and show the result in a scrollable box. @@ -144,14 +157,15 @@ latest_common_snapshot() { # --------------------------------------------------------------------------- # Snapshots menu # --------------------------------------------------------------------------- -view_snapshots() { - local body - body="$(list_snapshots | awk -F'\t' \ +# Report producers print to stdout; show() captures them with `set -e` off. +_report_snapshots() { + local rows; rows="$(list_snapshots)" + [[ -z "$rows" ]] && { echo "(no snapshots found)"; return 0; } + printf '%s\n' "$rows" | awk -F'\t' \ 'BEGIN{printf "%-48s %8s %8s %s\n","SNAPSHOT","USED","REFER","CREATED"} - {printf "%-48s %8s %8s %s\n",$1,$2,$3,$4}')" - [[ -z "$body" ]] && body="(no snapshots found)" - show "ZFS Snapshots" "$body" + {printf "%-48s %8s %8s %s\n",$1,$2,$3,$4}' } +view_snapshots() { show "ZFS Snapshots" _report_snapshots; } create_snapshot() { local ds name @@ -305,14 +319,14 @@ pick_cron() { fi } -view_schedules() { - local body - body="$(parse_snapshot_schedules | awk -F'\t' \ +_report_schedules() { + local rows; rows="$(parse_snapshot_schedules)" + [[ -z "$rows" ]] && { echo "(no snapshot schedules configured)"; return 0; } + printf '%s\n' "$rows" | awk -F'\t' \ 'BEGIN{printf "%-40s %-16s %s\n","DATASET","CRON","KEEP"} - {printf "%-40s %-16s %s\n",$1,$2,$3}')" - [[ -z "$body" ]] && body="(no snapshot schedules configured)" - show "Snapshot Schedules" "$body" + {printf "%-40s %-16s %s\n",$1,$2,$3}' } +view_schedules() { show "Snapshot Schedules" _report_schedules; } add_schedule() { local ds cron keep snaps scrubs @@ -378,14 +392,14 @@ menu_schedules() { # --------------------------------------------------------------------------- # Datasets menu # --------------------------------------------------------------------------- -view_datasets() { - local body - body="$(list_datasets | awk -F'\t' \ +_report_datasets() { + local rows; rows="$(list_datasets)" + [[ -z "$rows" ]] && { echo "(no datasets found — is 'zfs list' working as root?)"; return 0; } + printf '%s\n' "$rows" | awk -F'\t' \ 'BEGIN{printf "%-40s %8s %8s %s\n","NAME","USED","AVAIL","MOUNTPOINT"} - {printf "%-40s %8s %8s %s\n",$1,$2,$3,$4}')" - [[ -z "$body" ]] && body="(no datasets found — is 'zfs list' working as root?)" - show "ZFS Datasets" "$body" + {printf "%-40s %8s %8s %s\n",$1,$2,$3,$4}' } +view_datasets() { show "ZFS Datasets" _report_datasets; } # --------------------------------------------------------------------------- # LXC mountpoints menu (Proxmox pct) @@ -408,18 +422,20 @@ lxc_mountpoints() { }' } -view_mounts() { - local body="" ctid line +_report_mounts() { + local ctid slot host target found=0 while IFS= read -r ctid; do [[ -z "$ctid" ]] && continue while IFS=$'\t' read -r slot host target; do [[ -z "$slot" ]] && continue - body+="$(printf 'CT %-6s %-6s %-30s -> %s' "$ctid" "$slot" "$host" "$target")"$'\n' + printf 'CT %-6s %-6s %-30s -> %s\n' "$ctid" "$slot" "$host" "$target" + found=1 done < <(lxc_mountpoints "$ctid") done < <(list_lxc_ids) - [[ -z "$body" ]] && body="(no LXC mountpoints found)" - show "LXC Mountpoints" "$body" + [[ "$found" -eq 0 ]] && echo "(no LXC mountpoints found)" + return 0 } +view_mounts() { show "LXC Mountpoints" _report_mounts; } add_mount() { local ctid host target used slot=0 @@ -509,21 +525,23 @@ EOF chmod 755 "$REPLICATION_SCRIPT" } -view_replication() { - local body="" src tgt common latest status +_report_replication() { + local src tgt common latest status found=0 while IFS=$'\t' read -r src tgt; do [[ -z "$src" ]] && continue + found=1 latest="$(latest_snapshot "$src")"; latest="${latest:--}" common="$(latest_common_snapshot "$src" "$tgt")"; common="${common:--}" if ! dataset_exists "$tgt"; then status="target missing (full send needed)" elif [[ "$common" == "$latest" && "$common" != "-" ]]; then status="up-to-date" elif [[ "$common" == "-" ]]; then status="no common snap (mismatch)" else status="incremental pending"; fi - body+="$(printf '%-30s -> %-30s [%s]' "$src" "$tgt" "$status")"$'\n' + printf '%-30s -> %-30s [%s]\n' "$src" "$tgt" "$status" done < <(load_replication) - [[ -z "$body" ]] && body="(no replication targets configured)" - show "Replication Targets" "$body" + [[ "$found" -eq 0 ]] && echo "(no replication targets configured)" + return 0 } +view_replication() { show "Replication Targets" _report_replication; } add_replication() { local src tgt cfg @@ -588,17 +606,20 @@ menu_replication() { # --------------------------------------------------------------------------- # Scrub menu # --------------------------------------------------------------------------- -view_scrub() { - local body="" pool sched line +_report_scrub() { + local pool sched found=0 while IFS= read -r pool; do [[ -z "$pool" ]] && continue + found=1 sched="$(parse_scrub_schedules | awk -F'\t' -v p="$pool" '$1==p {print $2; exit}')" - body+="$(printf '===== %s (schedule: %s) =====' "$pool" "${sched:-none}")"$'\n' - body+="$(zpool status "$pool" 2>&1 | grep -E '^[[:space:]]*(state|scan|errors):' )"$'\n\n' + printf '===== %s (schedule: %s) =====\n' "$pool" "${sched:-none}" + zpool status "$pool" 2>&1 | grep -E '^[[:space:]]*(state|scan|errors):' || true + echo done < <(list_pools) - [[ -z "$body" ]] && body="(no pools found)" - show "Pool Scrub Status" "$body" + [[ "$found" -eq 0 ]] && echo "(no pools found)" + return 0 } +view_scrub() { show "Pool Scrub Status" _report_scrub; } scrub_start() { local pool