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>
131 lines
6.1 KiB
Bash
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
|
|
}
|