paths: recover the roots from the baked unit when nothing exported them

The three roots reach running code three ways — the CLI wrapper exports them,
the task-processor unit carries them as Environment=, and anything those start
inherits them. An @reboot crontab entry is started by none of the three: it runs
a script by absolute path, so paths.sh fell through to

    : "${LP_CONTAINERS_DIR:=/libreportal-containers}"

That entry is crontab_boot_app_reconcile.sh, which brings every installed app up
at boot. On a relocated install it therefore reconciled against the DEFAULT
root, and that does not fail — docker creates the bind-mount directories it does
not find, so every app comes back empty while the real data sits untouched on
the other disk. Nothing logs an error; the only symptom is opening an app and
finding it blank (storage-locations §10.1 calls this the top data-integrity
risk).

Verified on a case-2 install (apps on /mnt/lptest2): a bare environment resolved
containers_dir to /libreportal-containers/ where the real root was
/mnt/lptest2/libreportal-containers.

Recover them from the systemd unit, which is the authoritative baked record —
init.sh already reads it back the same way, libreportal-relocate rewrites it,
and it is root-owned, so this is not the manager reading a config it can edit.
An explicit environment still wins; with no unit the defaults are unchanged.

scripts/dev/lp-paths-roots-test pins all four cases; verified it fails when the
recovery is removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-28 06:31:02 +01:00
parent fc92d556f1
commit 864059ab83
2 changed files with 95 additions and 0 deletions

66
scripts/dev/lp-paths-roots-test Executable file
View File

@ -0,0 +1,66 @@
#!/bin/bash
# Where do the three roots come from when nothing exported them?
#
# scripts/dev/lp-paths-roots-test
#
# The roots reach running code three ways: the CLI wrapper exports them, the
# task-processor unit carries them as Environment=, and anything those start
# inherits them. An @reboot crontab entry is started by none of the three — it
# runs a script by absolute path — so paths.sh fell through to the
# /libreportal-* defaults.
#
# That job is crontab_boot_app_reconcile.sh, which brings every installed app up
# at boot. Pointed at the wrong root it does not fail: docker creates the
# bind-mount directories it does not find, so every app comes back EMPTY while
# the real data sits untouched on the other disk. Nothing reports an error, and
# the damage is only visible by opening an app (storage-locations §10.1).
#
# So the case that matters is the second one: no environment, relocated install.
REPO="$(cd "$(dirname "$0")/../.." && pwd)"
BASE="$(mktemp -d "${TMPDIR:-/tmp}/lp-paths-test-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; }
# A stand-in for the baked task-processor unit.
cat > "$BASE/unit" <<EOF
[Service]
Environment=LP_SYSTEM_DIR=/mnt/d1/libreportal-system
Environment=LP_CONTAINERS_DIR=/mnt/d2/libreportal-containers
Environment=LP_BACKUPS_DIR=/mnt/d2/libreportal-backups
EOF
# Each case runs in its own bash so no root leaks between them.
ask() { # ask <var> [env assignments...]
local var="$1"; shift
env -i PATH=/usr/bin:/bin "$@" bash -c \
'source "$0" >/dev/null 2>&1; eval echo "\$$1"' "$REPO/scripts/source/paths.sh" "$var"
}
echo "--- no environment, unit present (the @reboot cron case) ---"
chk "system" "$(ask LP_SYSTEM_DIR LP_UNIT_FILE="$BASE/unit")" "/mnt/d1/libreportal-system"
chk "containers" "$(ask LP_CONTAINERS_DIR LP_UNIT_FILE="$BASE/unit")" "/mnt/d2/libreportal-containers"
chk "backups" "$(ask LP_BACKUPS_DIR LP_UNIT_FILE="$BASE/unit")" "/mnt/d2/libreportal-backups"
chk "derived containers_dir" \
"$(ask containers_dir LP_UNIT_FILE="$BASE/unit")" "/mnt/d2/libreportal-containers/"
chk "derived configs_dir" \
"$(ask configs_dir LP_UNIT_FILE="$BASE/unit")" "/mnt/d1/libreportal-system/configs/"
echo "--- an explicit environment still wins over the unit ---"
chk "containers" \
"$(ask LP_CONTAINERS_DIR LP_UNIT_FILE="$BASE/unit" LP_CONTAINERS_DIR=/explicit)" "/explicit"
echo "--- no unit: unchanged default behaviour ---"
chk "system" "$(ask LP_SYSTEM_DIR LP_UNIT_FILE=/nonexistent)" "/libreportal-system"
chk "containers" "$(ask LP_CONTAINERS_DIR LP_UNIT_FILE=/nonexistent)" "/libreportal-containers"
echo "--- a unit that names only the system root ---"
printf '[Service]\nEnvironment=LP_SYSTEM_DIR=/mnt/d1/libreportal-system\n' > "$BASE/partial"
chk "system" "$(ask LP_SYSTEM_DIR LP_UNIT_FILE="$BASE/partial")" "/mnt/d1/libreportal-system"
chk "containers -> default" "$(ask LP_CONTAINERS_DIR LP_UNIT_FILE="$BASE/partial")" "/libreportal-containers"
echo ""
if (( fail )); then echo "FAILED"; exit 1; fi
echo "All root-resolution checks passed."

View File

@ -24,6 +24,35 @@
# derivations in sync. # derivations in sync.
# --- Resolve the three roots ------------------------------------------------ # --- Resolve the three roots ------------------------------------------------
# Nothing in the environment? Recover them from the record root baked at install
# before falling back to the defaults below.
#
# The roots reach running code three ways: the CLI wrapper exports them, the
# task-processor unit carries them as Environment=, and everything started by
# those inherits them. An @reboot crontab entry is started by none of the three
# — it invokes a script by absolute path — so it fell through to the /libreportal-*
# defaults. On a relocated install that silently pointed the BOOT APP RECONCILE
# at the wrong disk, and that job brings every installed app up: docker creates
# the bind-mount directories it does not find, so the apps come back EMPTY while
# the real data sits untouched on the other disk (storage-locations §10.1).
#
# The unit is the authoritative record — init.sh reads it back the same way, and
# libreportal-relocate rewrites it — and it is root-owned, so this is not the
# manager reading a config it can edit.
if [[ -z "${LP_SYSTEM_DIR:-}" ]]; then
_lp_unit="${LP_UNIT_FILE:-/etc/systemd/system/libreportal.service}"
if [[ -r "$_lp_unit" ]]; then
_lp_v=$(grep -m1 -oE '^Environment=LP_SYSTEM_DIR=\S+' "$_lp_unit" 2>/dev/null); _lp_v="${_lp_v#*LP_SYSTEM_DIR=}"
[[ -n "$_lp_v" ]] && LP_SYSTEM_DIR="$_lp_v"
_lp_v=$(grep -m1 -oE '^Environment=LP_CONTAINERS_DIR=\S+' "$_lp_unit" 2>/dev/null); _lp_v="${_lp_v#*LP_CONTAINERS_DIR=}"
[[ -n "$_lp_v" && -z "${LP_CONTAINERS_DIR:-}" ]] && LP_CONTAINERS_DIR="$_lp_v"
_lp_v=$(grep -m1 -oE '^Environment=LP_BACKUPS_DIR=\S+' "$_lp_unit" 2>/dev/null); _lp_v="${_lp_v#*LP_BACKUPS_DIR=}"
[[ -n "$_lp_v" && -z "${LP_BACKUPS_DIR:-}" ]] && LP_BACKUPS_DIR="$_lp_v"
unset _lp_v
fi
unset _lp_unit
fi
# Transitional compat: an EXISTING install (the legacy single /docker tree, # Transitional compat: an EXISTING install (the legacy single /docker tree,
# identified by its config marker) keeps using /docker until a deliberate # identified by its config marker) keeps using /docker until a deliberate
# reinstall to the split layout — so deploying new code never strands a running # reinstall to the split layout — so deploying new code never strands a running