Fix crash when viewing snapshots (and other list views)

Root cause: under `set -euo pipefail`, the view functions built their body
with `body="$(zfs ... | awk ...)"`. If the pipeline exited non-zero — either
because `zfs list -t snapshot` returned non-zero (e.g. the no-snapshots case)
or because the large report was handed to `whiptail --msgbox` as one argv
string and newt choked — `set -e` aborted the entire script, dropping the
whole TUI back to the shell. The snapshot view is the most exposed since it is
both the largest body and the most likely to hit a non-zero zfs result.

Fix: route every list view through a hardened show() that runs the report
producer with `set -e` disabled (so an inner non-zero can never kill the TUI)
and renders via `whiptail --textbox <tmpfile>` (reads from a file instead of
taking the report as an argv string, and scrolls large content cleanly). Each
view_* is now a thin wrapper over a _report_* producer that prints to stdout.

Verified with a stubbed harness: failing pipeline, 5000-row list, and a
non-zero whiptail all now survive; the previous pattern crashed (exit 1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
yiekheng 2026-05-30 13:34:54 +08:00
parent 936db8b5ff
commit c1bc79efd8

View File

@ -60,10 +60,23 @@ fi
msg() { whiptail --title "$APP_TITLE" --msgbox "$1" "${2:-12}" "${3:-72}"; } msg() { whiptail --title "$APP_TITLE" --msgbox "$1" "${2:-12}" "${3:-72}"; }
yesno(){ whiptail --title "$APP_TITLE" --yesno "$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() { show() {
local title="$1" body="$2" local title="$1"; shift
whiptail --title "$title" --scrolltext --msgbox "${body:-(no output)}" 24 90 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. # Run a command, capturing output, and show the result in a scrollable box.
@ -144,14 +157,15 @@ latest_common_snapshot() {
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Snapshots menu # Snapshots menu
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
view_snapshots() { # Report producers print to stdout; show() captures them with `set -e` off.
local body _report_snapshots() {
body="$(list_snapshots | awk -F'\t' \ 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"} 'BEGIN{printf "%-48s %8s %8s %s\n","SNAPSHOT","USED","REFER","CREATED"}
{printf "%-48s %8s %8s %s\n",$1,$2,$3,$4}')" {printf "%-48s %8s %8s %s\n",$1,$2,$3,$4}'
[[ -z "$body" ]] && body="(no snapshots found)"
show "ZFS Snapshots" "$body"
} }
view_snapshots() { show "ZFS Snapshots" _report_snapshots; }
create_snapshot() { create_snapshot() {
local ds name local ds name
@ -305,14 +319,14 @@ pick_cron() {
fi fi
} }
view_schedules() { _report_schedules() {
local body local rows; rows="$(parse_snapshot_schedules)"
body="$(parse_snapshot_schedules | awk -F'\t' \ [[ -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"} 'BEGIN{printf "%-40s %-16s %s\n","DATASET","CRON","KEEP"}
{printf "%-40s %-16s %s\n",$1,$2,$3}')" {printf "%-40s %-16s %s\n",$1,$2,$3}'
[[ -z "$body" ]] && body="(no snapshot schedules configured)"
show "Snapshot Schedules" "$body"
} }
view_schedules() { show "Snapshot Schedules" _report_schedules; }
add_schedule() { add_schedule() {
local ds cron keep snaps scrubs local ds cron keep snaps scrubs
@ -378,14 +392,14 @@ menu_schedules() {
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Datasets menu # Datasets menu
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
view_datasets() { _report_datasets() {
local body local rows; rows="$(list_datasets)"
body="$(list_datasets | awk -F'\t' \ [[ -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"} 'BEGIN{printf "%-40s %8s %8s %s\n","NAME","USED","AVAIL","MOUNTPOINT"}
{printf "%-40s %8s %8s %s\n",$1,$2,$3,$4}')" {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"
} }
view_datasets() { show "ZFS Datasets" _report_datasets; }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# LXC mountpoints menu (Proxmox pct) # LXC mountpoints menu (Proxmox pct)
@ -408,18 +422,20 @@ lxc_mountpoints() {
}' }'
} }
view_mounts() { _report_mounts() {
local body="" ctid line local ctid slot host target found=0
while IFS= read -r ctid; do while IFS= read -r ctid; do
[[ -z "$ctid" ]] && continue [[ -z "$ctid" ]] && continue
while IFS=$'\t' read -r slot host target; do while IFS=$'\t' read -r slot host target; do
[[ -z "$slot" ]] && continue [[ -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 < <(lxc_mountpoints "$ctid")
done < <(list_lxc_ids) done < <(list_lxc_ids)
[[ -z "$body" ]] && body="(no LXC mountpoints found)" [[ "$found" -eq 0 ]] && echo "(no LXC mountpoints found)"
show "LXC Mountpoints" "$body" return 0
} }
view_mounts() { show "LXC Mountpoints" _report_mounts; }
add_mount() { add_mount() {
local ctid host target used slot=0 local ctid host target used slot=0
@ -509,21 +525,23 @@ EOF
chmod 755 "$REPLICATION_SCRIPT" chmod 755 "$REPLICATION_SCRIPT"
} }
view_replication() { _report_replication() {
local body="" src tgt common latest status local src tgt common latest status found=0
while IFS=$'\t' read -r src tgt; do while IFS=$'\t' read -r src tgt; do
[[ -z "$src" ]] && continue [[ -z "$src" ]] && continue
found=1
latest="$(latest_snapshot "$src")"; latest="${latest:--}" latest="$(latest_snapshot "$src")"; latest="${latest:--}"
common="$(latest_common_snapshot "$src" "$tgt")"; common="${common:--}" common="$(latest_common_snapshot "$src" "$tgt")"; common="${common:--}"
if ! dataset_exists "$tgt"; then status="target missing (full send needed)" if ! dataset_exists "$tgt"; then status="target missing (full send needed)"
elif [[ "$common" == "$latest" && "$common" != "-" ]]; then status="up-to-date" elif [[ "$common" == "$latest" && "$common" != "-" ]]; then status="up-to-date"
elif [[ "$common" == "-" ]]; then status="no common snap (mismatch)" elif [[ "$common" == "-" ]]; then status="no common snap (mismatch)"
else status="incremental pending"; fi 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) done < <(load_replication)
[[ -z "$body" ]] && body="(no replication targets configured)" [[ "$found" -eq 0 ]] && echo "(no replication targets configured)"
show "Replication Targets" "$body" return 0
} }
view_replication() { show "Replication Targets" _report_replication; }
add_replication() { add_replication() {
local src tgt cfg local src tgt cfg
@ -588,17 +606,20 @@ menu_replication() {
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Scrub menu # Scrub menu
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
view_scrub() { _report_scrub() {
local body="" pool sched line local pool sched found=0
while IFS= read -r pool; do while IFS= read -r pool; do
[[ -z "$pool" ]] && continue [[ -z "$pool" ]] && continue
found=1
sched="$(parse_scrub_schedules | awk -F'\t' -v p="$pool" '$1==p {print $2; exit}')" sched="$(parse_scrub_schedules | awk -F'\t' -v p="$pool" '$1==p {print $2; exit}')"
body+="$(printf '===== %s (schedule: %s) =====' "$pool" "${sched:-none}")"$'\n' printf '===== %s (schedule: %s) =====\n' "$pool" "${sched:-none}"
body+="$(zpool status "$pool" 2>&1 | grep -E '^[[:space:]]*(state|scan|errors):' )"$'\n\n' zpool status "$pool" 2>&1 | grep -E '^[[:space:]]*(state|scan|errors):' || true
echo
done < <(list_pools) done < <(list_pools)
[[ -z "$body" ]] && body="(no pools found)" [[ "$found" -eq 0 ]] && echo "(no pools found)"
show "Pool Scrub Status" "$body" return 0
} }
view_scrub() { show "Pool Scrub Status" _report_scrub; }
scrub_start() { scrub_start() {
local pool local pool