8 Commits

Author SHA1 Message Date
librelad
e73f47ad46 generator: warn when the manifest indexes files git does not track
Regenerating the function manifest indexes what is on disk, which is
correct. The hazard is committing the result: an entry for a file git does
not have installs an autoload stub on every other clone, and the first call
to it unsets the stub, fails to source a file that is not there, and dies
with "command not found".

Easy to cause without noticing, and easy to cause repeatedly when more than
one person is working in the same tree — somebody else's in-progress file
is sitting under scripts/ whenever you happen to regenerate. It has already
happened twice today: once picking up a vendored dev helper, once picking
up an uncommitted validator.

Warn rather than skip. The scan is right to index them, and mid-work is a
normal state for a tree to be in; what is not fine is committing it. The
warning names the files, so the choice is obvious either way — commit them
alongside, or drop their entries first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 00:18:32 +01:00
librelad
e85fba3b09 auto: session-start commit — 4 file(s) at 2026-07-13 15:49:56 2026-07-13 15:49:56 +01:00
librelad
ba4dbc6e02 fix(tasks): stop the lazy-autoload stub from looping forever + add processor failsafes
Root cause of the "task loops eternally" bug: the lazy-autoload stub was

    fn() { source "$file"; fn "$@"; }

If the source fails — the real case: an app-install/deploy rsync briefly
removes-then-replaces a generator file while a setup task calls it — `fn` is
never redefined, so `fn "$@"` re-invokes the *stub*, which sources the (still
missing) file, which re-invokes the stub… A single setup finalize recursed
12,050 levels, flooding the task log and taking 82s before it happened to
recover when the file reappeared. A permanently-missing file would never
recover.

Fix (root cause): drop the stub before sourcing —

    fn() { unset -f fn; source "$file"; fn "$@"; }

so a failed source degrades to one "command not found" (rc 127) instead of
unbounded recursion. Regenerated function_manifest.sh (975 stubs, reformat
only — no function-set change).

Failsafes on the task processor (defence in depth, per request):
- FUNCNEST cap (TASK_FUNCNEST_MAX, default 1000) inside the task's eval
  subshell — any runaway recursion now aborts in milliseconds instead of
  spamming the log until the stack/disk gives out.
- Wall-clock cap (TASK_MAX_RUNTIME_SECS, default 7200s, 0=off) — the heartbeat
  watcher TERM→KILLs a task's process group once exceeded and marks it failed
  (distinct from a user cancel via a .timeout marker). Generous so real long
  installs/backups/migrations finish.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-07-06 20:56:07 +01:00
librelad
061fa5e391 fix(lazy-load): count both braces per line so embedded awk doesn't strand the depth tracker
`webuiSystemUpdate` was calling `webuiSystemMetrics` and getting
"command not found": the lazy-load manifest was missing it (and
`webuiSystemApps`), even though both are defined in
webui_system_metrics.sh.

Cause: the manifest scanner's awk-based depth tracker is line-based.
The first function in that file, `_metricsReadCpu`, contains an
embedded awk script:

    _metricsReadCpu() {
        awk '/^cpu /{        <-- the open brace runs to end-of-line, depth += 1
            ...
        }' /proc/stat        <-- the close brace is mid-line, NOT counted
    }                        <-- depth-- but we started one too high

The increment rule fired for the `/^cpu /{` line (open brace at EOL),
the matching close on the `}' /proc/stat` line was not counted because
the existing depth>0 branch only decremented when stripped was EXACTLY
`}`. Result: depth ended at 1 instead of 0 after _metricsReadCpu, and
every subsequent funcname() header was treated as "still inside a
function" and silently dropped. Same pattern across the ~6 files the
lazy-loader memory called out as "false-positive eager".

Fix: in the depth>0 branch, count open AND close braces per line
symmetrically and apply the net delta. Drops the redundant "exactly
`}`" special-case rule, clamps depth at 0 as a sanity backstop. False
positives from braces inside string literals could under/over-count
locally, but the clamp + the next legitimate `funcname()` header at
depth-0 re-anchors the tracker.

Result on a full regen:
  - 858 → 876 functions indexed (+18 previously-stranded)
  - webuiSystemMetrics + webuiSystemApps now correctly autoloaded
  - eager-file count: 9 → 11 (two files that genuinely have both
    function defs and top-level side effects are now correctly seen
    both as eager AND get their functions indexed — net win on
    every axis)

Verified live: the WebUI updater that was failing with "command not
found" now prints "Updated system information..." cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-27 00:10:35 +01:00
librelad
77342c8047 feat(lazy-load): extend manifest to containers/ + skip container scan (Phase 5)
Containers used to be eager-loaded via `sourceScanFiles "containers"`
even under LP_LAZY=1 — sourcing all ~160 installer functions up front.
Phase 5 brings them into the autoload-stub mechanism.

generate_function_manifest.sh now scans BOTH scripts/ AND containers/
(maxdepth 3, matching sourceScanFiles' existing prune), with a per-entry
root selector so stub emission uses the right base directory:

  scripts/peer/peer_add.sh    →  source "${install_scripts_dir}peer/peer_add.sh"
  containers/linkding/linkding.sh →  source "${install_containers_dir}linkding/linkding.sh"

New manifest exports:
  LP_FN_MAP             funcname → relpath        (existing)
  LP_FN_ROOT            funcname → scripts|containers   NEW
  LP_EAGER_FILES        "<root>:<relpath>" entries     NEW format
  ~860 autoload stubs   (was ~700; +160 from containers)

Loader changes (initilize_files.sh):
  - Parses LP_EAGER_FILES entries as `root:path`, dispatches to the
    right install_*_dir. Pre-Phase-5 entries without a colon default to
    scripts (backwards-compatible).
  - sourceScanFiles "containers" is skipped when LP_LAZY=1 AND
    LP_FN_MAP is loaded (manifest-driven autoload covers it).
    Eager mode and lazy-with-missing-manifest both still run the scan.

Measurement target: ~70 ms saved on top of Phase 4. Verified separately
in the commit message of the next deploy.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 21:30:36 +01:00
librelad
3b410fe6d9 fix(lazy-load): skip manifest itself + files_*.sh arrays from scan
The previous run had 32 eager files; 24 of them were the auto-generated
files_*.sh arrays (only useful to the eager loader) + the manifest
itself (which the lazy loader sources explicitly). Eager-sourcing them
under lazy mode was pure overhead — ~55ms on the manifest alone (it was
being parsed twice, once via the explicit lazy-loader source and once
via the LP_EAGER_FILES loop).

Down to 8 eager files (the genuinely-side-effecting ones: setup_lock.sh,
the two crontab task processors, backup_db.sh, backup_files.sh,
docker swap_docker_type.sh, migrate_url_rewrite.sh, cli_debug_commands.sh).

The files_*.sh arrays are still sourced by the eager loader's existing
path — that's unchanged. Lazy mode just doesn't need them because it
never iterates files_libreportal_app[@] / files_libreportal_cli[@].

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 20:54:38 +01:00
librelad
c68254ad70 feat(lazy-load): dual loader with LP_LAZY=1 opt-in (Phase 3)
scripts/source/loading/initilize_files.sh gains an LP_LAZY=1 branch:
  - Sources scripts/source/files/arrays/function_manifest.sh once. The
    manifest defines LP_FN_MAP, LP_EAGER_FILES, AND ~700 autoload stubs
    (precompiled by the generator — one parse cost vs evaluating 700
    snippets at startup).
  - Eager-sources every file listed in LP_EAGER_FILES (top-level side
    effects: variable assignments, source calls, bare commands). These
    can't safely be deferred — they'd skip the side effect, not just the
    function definition.
  - Skips the bulk loop that sources every files_to_source[@] entry.

Default behaviour (LP_LAZY unset or 0) is byte-identical to the previous
loader — every file gets eager-sourced up front. Long-running processes
(WebUI service, task processor) leave LP_LAZY unset because their first
call to anything wants the function already hot.

Each autoload stub looks like:
  funcname() {
    source "${install_scripts_dir}path/to/file.sh"
    funcname "$@"
  }

First call sources the real file, which redefines the function with the
real body; the stub's trailing `funcname "$@"` then calls the freshly-
defined real implementation. Sourcing the file also redefines stubs for
any sibling functions the same file declares, so they don't re-source.

Safety nets:
- Missing manifest → fall back to eager loading (`export LP_LAZY=0`).
  No regression risk if someone enables LP_LAZY=1 on a stale install
  whose regen never ran.
- LP_LOAD_TRACE=1 still works in lazy mode — it records the manifest
  parse + each eager file (tagged LAZY-manifest / LAZY-EAGER) so Phase 4
  can measure the actual saving.

No automatic flip yet — this commit only adds the path. Phase 4 will set
LP_LAZY=1 by default for the CLI entrypoint (and re-measure with the
trace tool from Phase 1).

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 20:51:24 +01:00
librelad
7a66801ead feat(lazy-load): function manifest generator + lpRegen wiring (Phase 2)
scripts/source/files/generate_function_manifest.sh — scans every .sh in
scripts/ (skip-list matches generate_arrays.sh, plus excludes peer_shell.sh
which is a standalone forced-command target) and emits
scripts/source/files/arrays/function_manifest.sh:

  declare -gA LP_FN_MAP=(
      [acquireSingletonLock]="crontab/task/crontab_task_processor.sh"
      [adoptDockerSubnet]="checks/requirements/check_docker_network.sh"
      ...                                  # 698 entries
  )
  LP_EAGER_FILES=(
      "backup/db/backup_db.sh"
      "source/files/arrays/files_app.sh"
      ...                                  # 32 entries (~7% of files)
  )

The lazy loader (Phase 3) consumes LP_FN_MAP to install autoload stubs of
the form `name() { source "$LP_FN_MAP[name]"; name "$@"; }`. First call
sources the real file, which redefines the stub with the real body;
subsequent calls hit the real one. LP_EAGER_FILES enumerates files with
top-level side effects (variable assignments, source calls, bare commands
outside any function) — those MUST always source so the side effects fire.

Heuristic correctness, in order of importance:
- Function header detection requires EMPTY parens (`name()`), not just
  `name(` — otherwise lines like `if (...)`, `for (...)`, `while (...)`
  in embedded awk/perl get misread as bash function defs.
- Handles three function styles: `name() {` (same line), `name()\n{`
  (LibrePortal convention), and one-liners `name() { body; }`.
- Tracks { } balance for inside-function depth, with the safe fallback that
  ambiguous cases get marked eager (false positive = no behaviour change;
  false negative would skip a needed source).
- Files containing embedded awk/perl with their own { } blocks (about 6 of
  them: cli_debug_commands.sh, crontab_task_processor.sh, backup_db.sh,
  backup_files.sh, etc.) get false-positive flagged eager — acceptable
  because they just stay eager-loaded, matching today's behaviour.
- Collisions report to stderr (last-write wins, same as eager-load
  semantics); no collisions found in the current tree.

Wiring:
- lpRegenArrays (`libreportal regen arrays`) now also runs the manifest
  generator when the existing arrays need regen, keeping the two in sync.
- update.sh's quick-deploy regen step does the same after copying files
  to the live install. Best-effort: failures don't abort because lazy
  loading is opt-in (LP_LAZY=1) in Phase 3 and not the default yet.

Scanned: 454 files, indexed 698 function definitions, 32 eager (9 real,
23 auto-generated arrays + the manifest itself). 0 name collisions.

No behaviour change in this commit — the manifest is just data the loader
in Phase 3 will use. The default eager loading path is untouched.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-26 20:47:54 +01:00