From 9d8b92367bcab3b25aeafa7e4cdbb867d6c3e8cd Mon Sep 17 00:00:00 2001 From: librelad Date: Fri, 28 Aug 2026 12:43:04 +0100 Subject: [PATCH] uninstall: read the storage registry before deleting it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- init.sh | 48 +++++++++--------- scripts/dev/lp-uninstall-report-test | 73 ++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 23 deletions(-) create mode 100755 scripts/dev/lp-uninstall-report-test diff --git a/init.sh b/init.sh index 61a3e95..8fe65c9 100755 --- a/init.sh +++ b/init.sh @@ -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 } diff --git a/scripts/dev/lp-uninstall-report-test b/scripts/dev/lp-uninstall-report-test new file mode 100755 index 0000000..228ffad --- /dev/null +++ b/scripts/dev/lp-uninstall-report-test @@ -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."