From bd10a9ab55526f71d418250e70b93ede165c8681 Mon Sep 17 00:00:00 2001 From: librelad Date: Wed, 19 Aug 2026 01:44:05 +0100 Subject: [PATCH] feat(update): detect a stale root footprint on git and local installs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit footprint_update_needed only ever came from comparing the installed marker against a channel manifest, so it could not fire on a git or local install — they have no channel to ask. Those are exactly the installs whose code tree is synced by hand, i.e. the ones most able to drift, and the drift was silent: the helpers in /usr/local/lib/libreportal could sit behind the code that calls them with nothing reporting it. That is how this box ended up running a crowdsec helper with no bouncer-traefik-rotate action while the tool that needs it shipped. init.sh is what bakes the marker, so the install tree's own init.sh is authoritative for every mode. lpInstallTreeFootprintVersion reads it and lpFootprintStale compares. Wired into both non-release branches of the WebUI status generator, and into the local branch of the interactive update check, which is where a local operator actually looks. Fails safe: a tree older than the marker, or a missing init.sh, reports current rather than warning — verified alongside the real stale case. Also gives webuiSystemUpdateCheck the self-reload guard webuiGenerateAppsToolsConfig already documents. The WebUI task service sources these once at startup, so without it an edited generator keeps writing the old JSON from memory. Co-Authored-By: Claude Opus 5 --- scripts/source/fetch.sh | 21 +++++++++++++ .../source/files/arrays/function_manifest.sh | 6 ++++ scripts/update/check_update.sh | 17 +++++++++++ .../generators/system/webui_system_update.sh | 30 +++++++++++++++++-- 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/scripts/source/fetch.sh b/scripts/source/fetch.sh index 4464ad4..c98bb90 100644 --- a/scripts/source/fetch.sh +++ b/scripts/source/fetch.sh @@ -68,6 +68,27 @@ lpVerifyMinisig() { # latter is greater, a plain manager-run update can't apply it (it can't rewrite # the root-owned helpers/wrapper/unit) — a root re-install is required. lpInstalledFootprintVersion() { local v; v=$(cat /usr/local/lib/libreportal/.footprint_version 2>/dev/null | tr -d ' \t\n\r'); echo "${v:-0}"; } + +# The footprint version the CODE IN THE INSTALL TREE would bake if init.sh were +# re-run. This is the only comparison available to a git or local install: those +# have no channel to ask, so lpReleaseLatestFootprint cannot answer and the +# staleness never surfaced — the root helpers silently stayed behind the code +# that expects them. Reading init.sh directly works for every mode, because +# init.sh is what bakes the marker in the first place. +lpInstallTreeFootprintVersion() { + local f="${script_dir%/}/init.sh" v="" + [[ -f "$f" ]] && v=$(grep -m1 -oE '^footprint_version=[0-9]+' "$f" 2>/dev/null | cut -d= -f2) + echo "${v:-0}" +} + +# True when what is baked into /usr/local/lib/libreportal is older than what the +# install tree carries, i.e. a root re-install (`sudo /init.sh`) is owed. +lpFootprintStale() { + local installed tree + installed=$(lpInstalledFootprintVersion) + tree=$(lpInstallTreeFootprintVersion) + [[ "${tree:-0}" -gt "${installed:-0}" ]] +} lpReleaseLatestFootprint() { local m; m="$(_lpDownload "$(lpReleaseBaseUrl)/$(lpReleaseChannel)/latest.json" - 2>/dev/null)" || return 1 local f; f=$(_lpJsonNum "$m" footprint_version); echo "${f:-0}" diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index 2c1fac1..e9b9eed 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -658,9 +658,11 @@ declare -gA LP_FN_MAP=( [lpFetchRelease]="source/fetch.sh" [lpFetchSource]="source/fetch.sh" [_lpFetchTool]="source/fetch.sh" + [lpFootprintStale]="source/fetch.sh" [lpIndexArtifactIds]="source/artifacts.sh" [lpIndexTop]="source/artifacts.sh" [lpInstalledFootprintVersion]="source/fetch.sh" + [lpInstallTreeFootprintVersion]="source/fetch.sh" [_lpJsonEsc]="source/fetch.sh" [_lpJsonNum]="source/fetch.sh" [_lpJsonStr]="source/fetch.sh" @@ -1803,9 +1805,11 @@ declare -gA LP_FN_ROOT=( [lpFetchRelease]="scripts" [lpFetchSource]="scripts" [_lpFetchTool]="scripts" + [lpFootprintStale]="scripts" [lpIndexArtifactIds]="scripts" [lpIndexTop]="scripts" [lpInstalledFootprintVersion]="scripts" + [lpInstallTreeFootprintVersion]="scripts" [_lpJsonEsc]="scripts" [_lpJsonNum]="scripts" [_lpJsonStr]="scripts" @@ -2984,9 +2988,11 @@ lpFetchIndexInto() { unset -f lpFetchIndexInto; __lpAutoload "${install_scripts_ lpFetchRelease() { unset -f lpFetchRelease; __lpAutoload "${install_scripts_dir}source/fetch.sh"; lpFetchRelease "$@"; } lpFetchSource() { unset -f lpFetchSource; __lpAutoload "${install_scripts_dir}source/fetch.sh"; lpFetchSource "$@"; } _lpFetchTool() { unset -f _lpFetchTool; __lpAutoload "${install_scripts_dir}source/fetch.sh"; _lpFetchTool "$@"; } +lpFootprintStale() { unset -f lpFootprintStale; __lpAutoload "${install_scripts_dir}source/fetch.sh"; lpFootprintStale "$@"; } lpIndexArtifactIds() { unset -f lpIndexArtifactIds; __lpAutoload "${install_scripts_dir}source/artifacts.sh"; lpIndexArtifactIds "$@"; } lpIndexTop() { unset -f lpIndexTop; __lpAutoload "${install_scripts_dir}source/artifacts.sh"; lpIndexTop "$@"; } lpInstalledFootprintVersion() { unset -f lpInstalledFootprintVersion; __lpAutoload "${install_scripts_dir}source/fetch.sh"; lpInstalledFootprintVersion "$@"; } +lpInstallTreeFootprintVersion() { unset -f lpInstallTreeFootprintVersion; __lpAutoload "${install_scripts_dir}source/fetch.sh"; lpInstallTreeFootprintVersion "$@"; } _lpJsonEsc() { unset -f _lpJsonEsc; __lpAutoload "${install_scripts_dir}source/fetch.sh"; _lpJsonEsc "$@"; } _lpJsonNum() { unset -f _lpJsonNum; __lpAutoload "${install_scripts_dir}source/fetch.sh"; _lpJsonNum "$@"; } _lpJsonStr() { unset -f _lpJsonStr; __lpAutoload "${install_scripts_dir}source/fetch.sh"; _lpJsonStr "$@"; } diff --git a/scripts/update/check_update.sh b/scripts/update/check_update.sh index 9d99a32..d61796b 100755 --- a/scripts/update/check_update.sh +++ b/scripts/update/check_update.sh @@ -22,6 +22,14 @@ webuiRunUpdate() if [[ "$install_mode" == "local" ]]; then isError "This is a local installation — updates are managed manually, nothing to pull." + # The code tree is synced by hand here, so the root-owned footprint can + # fall behind it without anything saying so — there is no channel + # manifest to trigger the usual footprint_update_needed path. + if declare -f lpFootprintStale >/dev/null 2>&1 && lpFootprintStale; then + isNotice "Root footprint is stale: installed v$(lpInstalledFootprintVersion), install tree carries v$(lpInstallTreeFootprintVersion)." + isNotice "The helpers in /usr/local/lib/libreportal are older than the code that calls them. Re-bake as root:" + isNotice " sudo ${script_dir%/}/init.sh" + fi return 1 fi if [[ "$git_updates" != "true" ]]; then @@ -139,6 +147,15 @@ checkUpdates() # Skip Git updates if installation mode is local or Git updates are disabled if [[ $CFG_INSTALL_MODE == "local" ]]; then isNotice "Local installation detected - Git updates are disabled." + # A local install syncs its code tree by hand, so the root-owned + # footprint can fall behind that code with nothing to say so — there is + # no channel manifest here to drive the usual footprint_update_needed + # path. This is the one place a local operator reliably looks. + if declare -f lpFootprintStale >/dev/null 2>&1 && lpFootprintStale; then + isNotice "Root footprint is stale: installed v$(lpInstalledFootprintVersion), install tree carries v$(lpInstallTreeFootprintVersion)." + isNotice "The helpers in /usr/local/lib/libreportal are older than the code calling them. Re-bake as root:" + isNotice " sudo ${script_dir%/}/init.sh" + fi if [[ $init_run_flag == "true" ]]; then startLoad; fi diff --git a/scripts/webui/data/generators/system/webui_system_update.sh b/scripts/webui/data/generators/system/webui_system_update.sh index 6e733e4..5ab2705 100755 --- a/scripts/webui/data/generators/system/webui_system_update.sh +++ b/scripts/webui/data/generators/system/webui_system_update.sh @@ -34,6 +34,21 @@ webuiSystemUpdate() { # Pass "force" as $1 to bypass the fetch throttle (used by the manual # "Check for updates" action: `libreportal update check`). webuiSystemUpdateCheck() { + # Same hazard webuiGenerateAppsToolsConfig documents: the WebUI task service + # is a long-lived bash process that sources this file once at startup, so an + # updated function on disk is ignored until the service restarts. That is + # not cosmetic here — it silently kept writing the OLD update_status.json, + # which is how the footprint check below appeared to do nothing after it was + # added. Re-source ourselves and dispatch to the fresh copy; the guard stops + # the recursion, and a missing file (briefly, mid-update) falls through to + # the in-memory version rather than failing. + if [[ -z "$_WEBUI_SYSUPDATE_RELOADED" && -f "${BASH_SOURCE[0]}" ]]; then + local _WEBUI_SYSUPDATE_RELOADED=1 + source "${BASH_SOURCE[0]}" + webuiSystemUpdateCheck "$@" + return $? + fi + local force_flag="$1" local repo_dir="${script_dir}" @@ -153,9 +168,14 @@ EOF # Not a git working copy, or a deliberately local install: we can't compare # against an upstream, so report "managed manually" rather than an error. if [[ ! -d "$repo_dir/.git" || "$install_mode" == "local" ]]; then + # There is no channel to compare against, but the install tree's own + # init.sh still says what the root footprint should be — so a stale + # footprint is detectable here, and used to go unreported entirely. + local _fp_local="false" + declare -f lpFootprintStale >/dev/null 2>&1 && lpFootprintStale && _fp_local="true" _webuiWriteUpdateStatus "false" "false" \ "$current_version" "$current_version" \ - "" "" "0" "0" "" "local" "" + "" "" "0" "0" "" "local" "" "$_fp_local" return 0 fi @@ -219,10 +239,16 @@ EOF local can_update="false" [[ "$install_mode" == "git" && "$git_updates" == "true" ]] && can_update="true" + # Same for a git install: no channel manifest, but init.sh in the working + # copy is authoritative about what the footprint should be. + local _fp_git="false" + declare -f lpFootprintStale >/dev/null 2>&1 && lpFootprintStale && _fp_git="true" + [[ "$_fp_git" == "true" ]] && can_update="false" + _webuiWriteUpdateStatus "$update_available" "$can_update" \ "$current_version" "$latest_version" \ "$current_commit" "$latest_commit" \ - "$behind" "$ahead" "$branch" "git" "$fetch_error" + "$behind" "$ahead" "$branch" "git" "$fetch_error" "$_fp_git" } # ---------------------------------------------------------------------------