feedBack/lib
Byron Gamatos 79825af28e
fix(demo): the janitor re-entry guard actually works now (#902) (#909)
The guard in startup_events() read:

    if getenv_compat("FEEDBACK_DEMO_MODE") or getenv_compat("FEEDBACK_DEMO_MODE") == "1" \
            and not _DEMO_JANITOR_STARTED:

`and` binds tighter than `or`, so that is `A or (B and C)`. The not-already-started half
never ran when the env var was truthy — the only case that reaches it at all. A second
startup started a SECOND janitor thread, overwrote the handle, and shutdown then joined
only the last: the first leaked and kept firing registered hooks hourly, forever.

The guard now lives INSIDE start_janitor(). A caller cannot get operator precedence wrong
if there is nothing left for it to get wrong.

━━━ THREE WAYS TO WRITE THIS GUARD WRONG. I HIT ALL THREE. ━━━

1. NO GUARD — the original bug. Double-start, orphaned thread.

2. GUARD ON THE FLAG (`if _DEMO_JANITOR_STARTED: return`). Codex [P2]. stop_janitor()
   DELIBERATELY leaves that flag True when a hook outruns its join timeout, so that a later
   startup cannot spawn a janitor beside a live one. But the hook usually finishes a moment
   later: the thread exits and the flag is stale. A flag-keyed guard then refuses to start a
   replacement for the rest of the process — demo cleanup silently dead. (The original bug
   accidentally MASKED this by always starting.)

3. GUARD ON LIVENESS ALONE (`if thread.is_alive(): return`). Codex [P2], second pass. A
   timed-out stop leaves the old thread ALIVE BUT DOOMED — its stop event is set and it
   exits as soon as its current hook returns. Treating that as a running janitor skips the
   replacement, and we are back at (2) a second later.

So: a janitor counts as running only if its thread is alive AND it has not been told to stop.

━━━ AND EACH JANITOR NOW OWNS ITS STOP EVENT ━━━

start_janitor() used to `_DEMO_JANITOR_STOP.clear()` a single SHARED Event. Start a
replacement while a doomed thread is still finishing a hook and that clear RESURRECTS it: it
loops back to wait(), sees the flag cleared, and carries on. Two janitors — the exact bug we
started from. A fresh Event per janitor makes it impossible; the old thread waits on its own
event, which stays set, so it can only exit.

Env semantics UNCHANGED, verified across every value ("", "1", "0", "true", "false", "off"):
the old expression and demo_mode_enabled() agree on all of them. The only behavioural change
is the idempotency fix.

FOUR tests, and each of the three wrong guards fails a different subset:

    no guard              -> 2 fail   (double start; orphaned thread)
    guard on the flag     -> 2 fail   (never restarts after a timed-out stop)
    liveness alone        -> 1 fail   (no replacement for a doomed janitor)
    liveness + not-stopping -> all pass

pytest 2416, pyflakes 0, Codex 0.

Closes #902

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 11:30:53 +02:00
..
routers refactor(server): extract the artist routes into routers/artist.py (R3) (#870) 2026-07-11 15:49:37 +02:00
acoustid_match.py feat(enrichment): AcoustID audio-fingerprint identification (opt-in) (#759) 2026-07-05 00:17:43 +02:00
appconfig.py refactor(server): extract /api/tunings into routers/tunings.py + lib/appconfig.py (R3) (#858) 2026-07-11 01:43:19 +02:00
appstate.py refactor(server): carve the library scanner into lib/scan.py (R3b) (#901) 2026-07-12 01:26:02 +02:00
audio_effects_db.py refactor(server): extract AudioEffectsMappingDB into lib/audio_effects_db.py (R3) (#831) 2026-07-10 14:30:05 +02:00
audio.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
builtin_content.py refactor(server): carve builtin-content seeding into lib/builtin_content.py (R3b) (#900) 2026-07-12 00:50:50 +02:00
demo_mode.py fix(demo): the janitor re-entry guard actually works now (#902) (#909) 2026-07-12 11:30:53 +02:00
diagnostics_bundle.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
diagnostics_hardware.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
diagnostics_redact.py fix(tests): isolate plugin routes modules + redact .feedpak filenames (#736) 2026-07-03 13:01:07 +02:00
dlc_paths.py refactor(server): move DLC path resolution to lib/dlc_paths.py (R3 substrate) (#843) 2026-07-10 20:41:42 +02:00
drums.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
enrichment.py refactor(server): extract the song routes into routers/song.py (R3) (#864) 2026-07-11 13:14:52 +02:00
env_compat.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
gp2midi.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
gp2notation.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
gp2rs_gpx.py feat(gp_autosync): piecewise time-warp helpers + refine_sync onset pass (#787) 2026-07-05 20:08:19 +02:00
gp2rs.py fix(gp2rs): write beat times at 6-decimal precision so imported tempo matches Guitar Pro (#819) 2026-07-10 13:12:10 +02:00
gp8_audio_sync.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
gp_autosync.py fix(gp_autosync): slope-constrained DTW steps — stop path collapse on riff-based songs (#791) 2026-07-05 23:12:31 +02:00
jsonc.py feat(core): read .jsonc data files (strip C-style comments) (feedpak-spec §8) 2026-06-20 14:10:04 -07:00
library_registry.py refactor(server): extract library + collections routes + the provider registry (R3) (#866) 2026-07-11 14:56:12 +02:00
logging_setup.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
loosefolder.py Improve wording in terminology cleanup 2026-06-16 19:43:45 +01:00
lyrics_transcribe.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
mb_match.py feat(enrichment): alias-aware scoring — auto-confirm non-Latin-primary artists (#772) 2026-07-05 01:11:38 +02:00
metadata_db.py refactor(server): extract MetadataDB into lib/metadata_db.py (R3) (#830) 2026-07-10 14:18:59 +02:00
midi_import.py feat(drums): capture velocities alongside times in unmapped-percussion reporting (#808) 2026-07-07 23:58:49 +02:00
notation_lift.py fix: correctly import and notate multi-staff (piano/keys) tracks from GP8 (#692) 2026-07-02 08:40:20 +02:00
notation.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
progression.py Improve wording in terminology cleanup 2026-06-16 19:43:45 +01:00
reqfields.py refactor(server): extract the playlists routes into routers/playlists.py (R3) (#841) 2026-07-10 19:58:09 +02:00
safepath.py fix(library): serve art/load for songs mounted through a library junction (#766) 2026-07-05 00:15:50 +02:00
scan_worker.py refactor(server): extract MetadataDB into lib/metadata_db.py (R3) (#830) 2026-07-10 14:18:59 +02:00
scan.py refactor(server): carve the library scanner into lib/scan.py (R3b) (#901) 2026-07-12 01:26:02 +02:00
sloppak.py Drop dead whisperx entry from allowed lyrics_source set 2026-07-06 17:30:26 -04:00
song_score.py Clean release snapshot 2026-06-16 18:47:13 +02:00
song.py fix(gp-import): correct bass string count, lead/rhythm roles, preview note count (#601) 2026-06-26 18:58:35 +02:00
songmeta.py library: opt-in gap-fill — write confirmed missing metadata into the pack (R4a) (#724) 2026-07-02 20:52:11 +02:00
tailwind_rebuild.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
tones.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
tunings.py Serve exact MIDI notes from GET /api/tunings (tuningMidis) (#829) 2026-07-10 13:05:39 +02:00
vocal_pitch.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
wem_decode.py rename: slopsmith → feedBack, byron → got-feedBack (#537) 2026-06-23 11:03:01 +02:00
xp.py Clean release snapshot 2026-06-16 18:47:13 +02:00