uninstall: read the storage registry before deleting it

The report added last commit never fired. It reads
/usr/local/lib/libreportal/storage.roots to list which locations still hold app
data — and that directory is removed earlier in the same function, so it found
an empty registry and printed nothing.

Which is precisely the silence it was written to prevent, and worse than not
having it: indistinguishable from "there was nothing left". Caught on a clean
teardown that left an app on each of two test disks and said so about neither.

Snapshot the registry before the removal and report from that.

scripts/dev/lp-uninstall-report-test runs the real capture and report fragments
from init.sh with the deletion between them, so what it guards is the ORDER
rather than the wording. Verified by moving the capture back after the delete.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-28 12:43:04 +01:00
parent 0aaae01833
commit 9d8b92367b
2 changed files with 98 additions and 23 deletions

48
init.sh
View File

@ -2262,6 +2262,22 @@ runFullUninstall()
fi
fi
# The storage registry lives inside the helpers directory about to be
# removed, so read it NOW — the summary at the end of this function reports
# which locations still hold app data, and reading it afterwards found
# nothing and said nothing, which is the exact silence it exists to prevent.
local _lp_left_report=""
if [[ -r /usr/local/lib/libreportal/storage.roots ]]; then
local _rid _rpath _rrest _rn
while IFS=$'\t' read -r _rid _rpath _rrest || [[ -n "$_rid" ]]; do
[[ -z "$_rpath" || "$_rid" == \#* ]] && continue
[[ -d "$_rpath" ]] || continue
_rn=$(find "$_rpath" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
(( _rn > 0 )) || continue
_lp_left_report+=" $(printf '%-40s %s app director%s' "$_rpath" "$_rn" "$( (( _rn == 1 )) && echo y || echo ies )")"$'\n'
done < /usr/local/lib/libreportal/storage.roots
fi
# 3. Remove the out-of-/docker footprint (see docs/architecture/system-footprint.md).
rm -f /usr/local/bin/libreportal /usr/local/bin/libreportal-uninstall /usr/local/bin/libreportal-relocate
rm -rf /usr/local/lib/libreportal
@ -2323,29 +2339,15 @@ runFullUninstall()
# — but doing one silently and the other silently is the one option nobody
# can predict. So say exactly what is still there.
#
# It also stops being obvious: the directories are owned by a uid that no
# longer maps to a user once the container account is gone, and each location
# keeps its .libreportal-storage marker, which a later install will recognise
# and adopt.
local _reg="/usr/local/lib/libreportal/storage.roots"
[[ -r "$_reg" ]] || _reg="${lp_lib_dir:-/usr/local/lib/libreportal}/storage.roots"
if [[ -r "$_reg" ]]; then
local _id _path _rest _left=0
while IFS=$'\t' read -r _id _path _rest || [[ -n "$_id" ]]; do
[[ -z "$_path" || "$_id" == \#* ]] && continue
[[ -d "$_path" ]] || continue
local _n; _n=$(find "$_path" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
(( _n > 0 )) || continue
(( _left == 0 )) && { echo ""; isNotice "App data left on these storage locations — nothing here was deleted:"; }
printf ' %-40s %s app director%s\n' "$_path" "$_n" "$( (( _n == 1 )) && echo y || echo ies )"
_left=1
done < "$_reg"
if (( _left )); then
echo ""
printf ' %s\n' "Remove them by hand if you meant to, or leave them — a reinstall can adopt"
printf ' %s\n' "the location again and the apps restore back onto it."
echo ""
fi
# Read from the snapshot taken before the registry was deleted, above.
if [[ -n "$_lp_left_report" ]]; then
echo ""
isNotice "App data left on these storage locations — nothing here was deleted:"
printf '%s' "$_lp_left_report"
echo ""
printf ' %s\n' "Remove them by hand if you meant to, or leave them — a reinstall can adopt"
printf ' %s\n' "the location again and the apps restore back onto it."
echo ""
fi
}

View File

@ -0,0 +1,73 @@
#!/bin/bash
# Does the teardown still say what it left behind, after deleting the thing it
# reads that from?
#
# scripts/dev/lp-uninstall-report-test
#
# A full uninstall removes the primary containers root and everything in it, but
# app data on an ADDITIONAL storage location survives. Leaving it is right —
# deleting someone's data off a separate disk unasked is worse — but saying
# nothing about it is not, especially as those directories end up owned by a uid
# that stops mapping to a user once the container account is gone.
#
# The report reads the storage registry, which lives INSIDE the helpers
# directory the same function deletes. Written after that deletion it found an
# empty registry and printed nothing: the exact silence it exists to prevent,
# and indistinguishable from "there was nothing left". So what this guards is
# not the wording but the ORDER — the capture has to happen before the delete.
#
# Runs the real fragments from init.sh with the deletion between them.
REPO="$(cd "$(dirname "$0")/../.." && pwd)"
BASE="$(mktemp -d "${TMPDIR:-/tmp}/lp-unreport-XXXXXX")"
trap 'rm -rf "$BASE"' EXIT
fail=0
chk(){ if [[ "$2" == "$3" ]]; then echo " ok $1"; else echo " FAIL $1: got '$2' want '$3'"; fail=1; fi; }
CAPTURE=$(awk '/^\t# The storage registry lives inside the helpers directory/,/^\tfi$/' "$REPO/init.sh")
REPORT=$(awk '/^\t# App data on an ADDITIONAL storage location is not removed/,/^\tfi$/' "$REPO/init.sh")
[[ -n "$CAPTURE" ]] || { echo " FAIL could not extract the capture block from init.sh"; exit 1; }
[[ -n "$REPORT" ]] || { echo " FAIL could not extract the report block from init.sh"; exit 1; }
# A registry where the helpers live, and data on one of the two locations.
mkdir -p "$BASE/lib" "$BASE/loc1/appA" "$BASE/loc1/appB" "$BASE/loc2" "$BASE/gone"
printf '1\t%s\t1\tuuid\n2\t%s\t2\tuuid\n3\t%s\t3\tuuid\n' \
"$BASE/loc1" "$BASE/loc2" "$BASE/nonexistent" > "$BASE/lib/storage.roots"
run() {
local tmp; tmp=$(mktemp)
{
echo 'isNotice(){ echo "NOTICE: $*"; }'
# The real paths are absolute; point them at the fixture.
printf '%s\n' "$CAPTURE" | sed "s#/usr/local/lib/libreportal#$BASE/lib#g"
# The deletion that sits between them in the real function.
echo "rm -rf \"$BASE/lib\""
printf '%s\n' "$REPORT"
} > "$tmp"
bash "$tmp" 2>&1
rm -f "$tmp"
}
out=$(run)
echo "--- it reports what survived, after the registry is gone ---"
chk "registry really was deleted" "$([[ -e "$BASE/lib" ]] && echo yes || echo no)" "no"
chk "says something" "$(grep -c 'App data left' <<< "$out")" "1"
chk "names the location" "$(grep -c "$BASE/loc1" <<< "$out")" "1"
chk "counts its apps" "$(grep -c '2 app directories' <<< "$out")" "1"
echo "--- and stays quiet about locations with nothing on them ---"
chk "empty location omitted" "$(grep -c "$BASE/loc2" <<< "$out")" "0"
chk "missing path omitted" "$(grep -c 'nonexistent' <<< "$out")" "0"
echo "--- nothing left anywhere: no report at all ---"
rm -rf "$BASE/loc1/appA" "$BASE/loc1/appB"
mkdir -p "$BASE/lib"
printf '1\t%s\t1\tuuid\n' "$BASE/loc1" > "$BASE/lib/storage.roots"
out=$(run)
chk "silent when there is nothing to say" "$(grep -c 'App data left' <<< "$out")" "0"
echo ""
if (( fail )); then echo "FAILED"; exit 1; fi
echo "All uninstall-report checks passed."