LibrePortal/scripts/webui/webui_regen.sh
librelad f82237da36 fix(instance): make an instance's own functions reachable
Four defects that all reduce to "a function an instance defines is invisible
to the code that dispatches it". Reported as `mattermost_teest has no upgrade
verifier`, for an app whose verifier was on disk the whole time.

* generate_function_manifest.sh shipped 0664. lpRegenArrays invokes it as an
  executable, so it died rc=126 on every call and `|| true` swallowed it — the
  manifest was never rebuilt on any live system, only laid down at deploy.
  Its sibling generate_arrays.sh is 0775, which is why the files_*.sh arrays
  looked current while the manifest was byte-identical to the shipped copy.

* lpRegenArrays now runs both generators through bash rather than depending on
  the exec bit, reports a manifest failure instead of hiding it, and treats a
  new containers/<app> dir as stale — the one event on a live box that adds
  functions was the one the scripts/-only mtime check could not see.

* updaterHasVerifier consults the disk before answering no. The CLI runs
  LP_LAZY=1, where the container scan is skipped and every function must come
  from the build-time manifest, so an app created after the build reads as
  having no verifier. GATE 1 then refuses an upgrade that is fully verifiable,
  and updaterUpgradeAuto's `|| continue` drops the app in silence for good.
  Self-healing regardless of manifest staleness, which matters because a
  self-update restores the shipped manifest and drops instance entries again.

* _instanceRewriteTools gains three renames. authAdapter_<type>_<method>() was
  caught by neither the prefix rule (no word boundary before _<type>) nor the
  suffix rule (needs () right after the type), so the clone defined the base
  app's adapter name while pointing at its own container — every instance user
  tool answered "does not implement", and which definition survived came down
  to find(1) order. Bare-app arguments to authAdapterCall/authPersistCfg went
  unrewritten too, so an instance's password reset wrote the credential into
  the base app's config. And dockerAppRunTool wants app<Ucfirst><Pascal>, which
  no rule produced, so every tool on every instance was unreachable. The infix
  rename runs before the suffix rename: the reverse order appends the id half
  twice (appSetupComposeTags_nextcloud_work_work).

Verified on a live install: the upgrade ladder now plans mattermost_teest
11.9 -> 11.10, and `regen arrays --force` indexes the instance hooks.

Also carries in-flight instance-removal regen work from a concurrent session
on the same worktree (_lpRegenOrphanedApp, instanceRemove's WebUI refresh).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 02:51:18 +01:00

131 lines
6.1 KiB
Bash

#!/bin/bash
# Unified regeneration front door.
#
# One entry point that rebuilds the file-derived artifacts whose SOURCES changed,
# so callers (the CLI, the task-processor poll, a deploy) don't have to know which
# generator owns what. Runs as the manager and writes only into the data dirs — no
# privilege needed.
#
# lpRegen [scope] [--force]
# scope: all (default) | webui | arrays
#
# Self-heal: a stage runs only when a source file is newer than its artifact (or
# the artifact is missing), unless --force. The check is a cheap `find -newer`
# mtime compare — there is no watcher/daemon. The natural triggers (install,
# config change, deploy, the periodic task-processor poll) call this; it no-ops
# when nothing is stale, so it is safe to call often.
#
# webui — apps.json / apps-tools.json etc. from containers/<app>/{*.config,tools/*.tools.json}
# arrays — the static files_*.sh source arrays from scripts/** (a dev/build concern;
# a normal install never adds core scripts, so this is a no-op there)
# Is $artifact stale relative to the given find expression? Returns 0 (stale) when
# the artifact is missing or any matched source file is newer than it.
_lpRegenStale() {
local artifact="$1"; shift
[[ -f "$artifact" ]] || return 0
find "$@" -newer "$artifact" -print -quit 2>/dev/null | grep -q .
}
# Does apps.json still list an app whose containers/<app> dir is gone?
#
# Deletions are invisible to the mtime check above: removing an app dir leaves
# no source file newer than the artifact, so a ghost entry (an uninstalled
# instance, a hand-deleted app folder) would sit in apps.json forever — still
# drawn as a pill in the app-detail Instances bar and still counted on the Apps
# grid. Compare the two sets instead of their timestamps. Returns 0 when a ghost
# is present. No jq → skip the check rather than guess.
_lpRegenOrphanedApp() {
local apps_json="$1" slug
[[ -f "$apps_json" ]] || return 1
command -v jq >/dev/null 2>&1 || return 1
while IFS= read -r slug; do
[[ -n "$slug" ]] || continue
[[ -d "${install_containers_dir%/}/$slug" ]] || return 0
done < <(jq -r '.apps[]? | (.command // "") | split(" ") | last' "$apps_json" 2>/dev/null)
return 1
}
lpRegenWebui() {
local force="$1"
local gen="${containers_dir}libreportal/frontend/data/apps/generated"
local apps_json="$gen/apps.json"
local tools_json="$gen/apps-tools.json"
if [[ "$force" == "force" ]] \
|| _lpRegenStale "$apps_json" "$install_containers_dir" -maxdepth 2 -name '*.config' \
|| _lpRegenStale "$tools_json" "$install_containers_dir" -maxdepth 3 -path '*/tools/*.tools.json' \
|| _lpRegenOrphanedApp "$apps_json"; then
# Sources changed (e.g. an app folder was dropped in or removed) — do the
# full, debounced refresh so the app appears/disappears everywhere. Force
# past the updater's own debounce: there is established real work.
WEBUI_UPDATER_FORCE=1 webuiLibrePortalUpdate
return $?
fi
return 0
}
lpRegenArrays() {
local force="$1"
local arrays_dir="${install_scripts_dir}source/files/arrays"
local gen_script="${install_scripts_dir}source/files/generate_arrays.sh"
local manifest_script="${install_scripts_dir}source/files/generate_function_manifest.sh"
local newest_array
newest_array="$(ls -t "$arrays_dir"/files_*.sh 2>/dev/null | head -1)"
# Staleness covers BOTH roots the manifest indexes. generate_function_manifest
# scans scripts/ AND containers/, so watching only scripts/ meant the one event
# that actually adds functions on a live box — a new containers/<app> dir, i.e.
# every instance clone — never registered as stale. Nothing but an explicit
# --force would rebuild, so an instance's hooks stayed invisible to lazy mode.
if [[ "$force" == "force" ]] || [[ -z "$newest_array" ]] \
|| find "$install_scripts_dir" -name '*.sh' -newer "$newest_array" -print -quit 2>/dev/null | grep -q . \
|| find "$install_containers_dir" -maxdepth 3 -name '*.sh' -newer "$newest_array" -print -quit 2>/dev/null | grep -q .; then
local rc=0
# Both generators run THROUGH bash rather than being executed, so a missing
# exec bit cannot silently disable regeneration. Not hypothetical:
# generate_function_manifest.sh shipped 0644, so every invocation died with
# rc=126 and the `|| true` below swallowed it. The manifest was therefore
# never rebuilt on any live system — it stayed byte-identical to the copy
# laid down at deploy — and lazy mode (the CLI default) could not see a
# single function belonging to an app created after that deploy.
if [[ -f "$gen_script" ]]; then
bash "$gen_script" run || rc=$?
else
rc=1
fi
# Function manifest tracks the same source set — keep them in sync.
# A failure still doesn't abort (eager mode is unaffected by a stale
# manifest) but it is no longer silent: lazy mode is what the CLI runs, so
# "the manifest is stale" is the difference between an app's hooks existing
# and not existing, which surfaces as absurd errors elsewhere ("app has no
# upgrade verifier" for an app whose verifier is sitting on disk).
if [[ -f "$manifest_script" ]] && ! bash "$manifest_script" run >/dev/null 2>&1; then
isNotice "Function manifest regeneration failed — lazy mode may not see recently added functions. Retry with: libreportal regen arrays --force"
fi
return $rc
fi
return 0
}
lpRegen() {
local scope="all" force=""
local a
for a in "$@"; do
case "$a" in
--force|-f|force) force="force" ;;
all|webui|arrays) scope="$a" ;;
*) isNotice "Unknown regen argument: $a (use: all|webui|arrays [--force])" ;;
esac
done
local rc=0
case "$scope" in
arrays) lpRegenArrays "$force" || rc=$? ;;
webui) lpRegenWebui "$force" || rc=$? ;;
all) lpRegenArrays "$force" || rc=$?; lpRegenWebui "$force" || rc=$? ;;
esac
return $rc
}