Grab bag Sunday - #1602
Open
CatalyticArkun wants to merge 51 commits into
Open
Conversation
Shell case globs treat the dot literally, so ra32.*/ra64.* required a 'ra32.' prefix and idlemon's bare ra32/ra64 process tokens fell through to exit 1 - in-game idle poweroff never fired for RetroArch sessions. Tracker: SPR-MED-050.
The unmount-failure branch called poweroff and fell through: poweroff returns immediately, so fsck.fat ran against the still-mounted SD and could corrupt the filesystem it was meant to repair. Exit the script once the shutdown is requested. Tracker: SPR-HIGH-015.
start_pyui_message_writer waited forever for the realtime listener file. If PyUI failed to launch, every caller hung - including save_poweroff's display path and the forced sub-1% battery shutdown, which then never powered off. Give up after 15s and let callers continue headless. Tracker: SPR-HIGH-014.
A single glitched 0%/1% sample from the battery driver immediately forced save_poweroff. Re-read after one second and only shut down when the low reading is confirmed; unreadable confirm samples also skip the forced path. Tracker: SPR-MED-051.
The -e check and the touch sat ~100ms apart (jq-heavy helpers run in between), so a lid-close and a power-press landing together started two sleep helpers: double suspend writes, two power-event readers, and both instances re-writing volume state at wake. mkdir is the guard now - create-or-fail in one syscall; the lock is a directory, removed with rmdir on every exit path. Tracker: SPR-MED-049.
Fourteen bare 'echo N' progress markers from debugging were left in the tmpfs stage2 script, including one after poweroff that can never run. Tracker: SPR-MED-015.
All eight 'pgrep | xargs kill -9' pipelines ran kill with no arguments when nothing matched, printing a usage error on every settings apply. xargs -r skips the kill entirely on empty input. Tracker: SPR-MED-016.
Three defects in one setting path: the numeric coercion rewrote "Off"
to 4 before either Off check ran, so the warning could never be
disabled; morse_code_sos wrote to ${LED_PATH}/brightness on devices
whose LED_PATH is 'not applicable'; and the recovered-battery branch
only cleared the low_battery flag when a LED exists. Off is now checked
before coercion (and breaks the inner warning loop), LED writes are
guarded, and the flag clears on recovery regardless of LED hardware.
Tracker: SPR-MED-014.
The pidfile guard was check-then-write: two shutdown triggers arriving together both passed the liveness check and ran the whole shutdown path twice, and either instance's EXIT trap removed the shared pidfile out from under the other. mkdir now takes the lock atomically, a dead owner's lock is reclaimed via the pid stored inside it, and the trap only removes a lock its own pid owns. Tracker: SPR-MED-053.
sleep_helper mutes with 'set_volume 0 false' on every sleep entry - transient by design, so the wake path can restore .vol from SYSTEM_JSON. The watchdog's inotify handler mirrored that 0 into SYSTEM_JSON before suspend, so the restore read back 0 and every sleep/wake cycle on an A133P device durably muted it. Skip mirroring while the sleep helper's lock exists; the wake-time restore already carries the right value. Tracker: SPR-HIGH-012.
Holding Vol-up while a Vol-down hold loop was still alive left both background loops stepping the volume against each other. Each key-down now cancels the opposite key's repeat loop too. Tracker: SPR-MED-048.
'kill $PID 2&> /dev/null' is a bash-ism this /bin/sh script parsed as a BACKGROUNDED 'kill $PID 2' - signalling PID 2 and racing the very loop it meant to stop, with the redirect applied to an empty command. All four repeat-loop kills are now guarded for empty PIDs, quoted, and use POSIX '2>/dev/null'. Tracker: SPR-MED-002.
mktemp defaults to /tmp (tmpfs) while SYSTEM_JSON lives on the SD card, so the mv fell back to a copy+unlink and concurrent readers - PyUI's 0.2s config watcher, jq callers - could catch a truncated file. Stage next to the target so the rename is atomic, and drop the temp if jq fails. Tracker: SPR-MED-047.
The gate tested the bound method object, which is always truthy, so X rebooted devices whose reboot_cmd() returns None - unlike the prompt text five lines up, which called it correctly and told those users no reboot option exists. Tracker: SPR-MED-060 (reboot-gate portion).
PyUiLogger.get_logger has no .error attribute, so both sites raised AttributeError inside their except blocks instead of logging: hwclock failures in device_common.sync_hw_clock and /etc/localtime updates in trim_ui_device. Tracker: SPR-LOW-006 (logger-reference portion).
Both volume and power keys arrive on /dev/input/event0 on this device, and two KeyWatcher instances each ran a polling thread over the same node - competing reads double-fired power events. Every other device pairs watchers on different nodes; here one watcher covers both roles. Tracker: SPR-HIGH-017.
A killed or timed-out ffmpeg left a partial .__safe__.ogg that the exists() short-circuit then returned forever, so one interrupted conversion permanently broke that track's BGM. Zero-byte cache entries are now deleted and reconverted, and a conversion that produced no usable output reports failure instead of a path. Tracker: SPR-MED-058 (cache-poisoning portion).
PyUiConfig, PyUiState, RomsListManager, and CollectionsManager all truncated their target file and rewrote it in place, so cutting power mid-save destroyed settings, favorites, recents, or collections. Each writer now stages to a .tmp alongside the target, fsyncs, and os.replace()s it - the same pattern UserConfig already used. Tracker: SPR-MED-045.
The initial os.stat sat outside the guarded loop, so a watcher started on a not-yet-existing config file died instantly and silently - and the stop event every device implementation stores was never consulted, so watchers could not be stopped. The initial stat is now guarded (the file appearing later counts as a change), and the loop checks and waits on the stop event, exiting promptly when it is set. Tracker: SPR-MED-046.
MiyooGameList aborted the whole parse loop when one <game> element lacked a <path> (AttributeError mid-loop dropped every later entry); each entry is now parsed under its own guard and bad ones are skipped with a log line. ActivityLog crashed outright sorting any event line missing a numeric ts; those events are filtered before the sort, which matches how _build_intervals already ignored them. Tracker: SPR-LOW-003.
perform_action ended with an unconditional 'killall sendevent' that landed milliseconds after kill_drastic backgrounded its >=0.2s MENU+L1 save-and-exit combo pipeline - the combo never reached DraStic, which neither saved nor exited on Game Switcher/Exit. The combo's PID is now recorded and waited on before the cleanup killall. Tracker: SPR-HIGH-013.
button_actions.sh is sourced by /bin/sh watchdogs where the bash-only '[[ == *glob* ]]' test is invalid; the PORTS check now uses a case pattern. Tracker: SPR-MED-017 (residual after the watchdog cleanup).
updater.py ran spruceBackup.sh without checking its result and rolled straight into the deletion block that wipes the SD card; a timeout raised out of the run() call uncaught. spruceBackup.sh compounded it by re-testing $? in an elif (testing the previous [ ] instead of 7zr) and always exiting 0. The backup status is captured once, real failures propagate as the script's exit code, and the updater aborts before touching any files when the backup did not succeed. Tracker: SPR-HIGH-003.
asound-setup.sh accepts a HOME override and wrote $BASE_HOME/.asoundrc without ensuring the directory exists; on a fresh home the redirect failed and audio routing setup was silently skipped. Tracker: SPR-MED-024.
'[!settings]*' is a character class, not an exclusion: it matched any
entry whose first character is outside {s,e,t,i,n,g}, so forget-wifi
spared unrelated entries and could miss saved networks entirely. Every
entry except the settings file is now removed explicitly.
Tracker: SPR-MED-022.
connect_services spun forever at 0.5s intervals waiting for a connection, so with WiFi enabled but unreachable the service policy never ran and the process parked indefinitely. Give up after 60s; the periodic relaunch retries on its own schedule. Tracker: SPR-MED-023.
pgrep returns one PID per line; with the stock daemon and spruce's own instance both alive, the multi-line result became a single broken /proc/<pid1>\n<pid2>/cmdline path and a malformed kill target. Each PID is now inspected on its own: wrong-config instances are killed, and a correctly-configured one is kept without a pointless restart. Tracker: SPR-MED-021.
The display helpers interpolated raw text into printf-built JSON, so a quote or backslash in a game, theme, or download name broke the message and PyUI dropped or misrendered it. A json_escape helper (backslash, quote, tab, CR, newline) now wraps every dynamic string; escaped payloads round-trip through a strict JSON parser. Tracker: SPR-MED-020.
The 0.5s poll loop forked a fresh sleep helper every pass while the lid stayed closed - each one hitting the singleton guard, but still a new process every half second for the whole closed period - and a helper returning with the lid still closed (e.g. failed timeout poweroff) was immediately relaunched with a fresh full timeout. The launch is now edge-gated on open->closed, skips when a helper is already active, and waits for the lid to reopen before re-arming. Tracker: SPR-MED-003.
send_cmd_to_ra created a fresh UDP socket per menu action and never closed it - one leaked fd per save/load/quit for the life of the UI process. The socket is now a context manager. Tracker: SPR-MED-026.
Every collection game was appended twice - once as a leftover generic GridOrListEntry and once as the RomGridOrListEntry the menu actually uses - so collections listed each game twice. Also bind the image path into the entry's lambda by default argument: the old closure captured the loop variable, giving every entry the last ROM's boxart. Tracker: SPR-MED-027.
Both in-game menu popups walked a walrus loop that exits with popup_selection = None when the view closes without a selection, then dereferenced it - crashing the menu mid-game. A None selection now behaves like pressing B (and unpauses RetroArch). The parent force-kill branch in the listener also logged child.pid after the child loop, an unbound/wrong reference when there are no children; it logs the parent pid it actually kills. Tracker: SPR-MED-025.
Italian.json ended mid-value ('"primImgWidth": "Largh<EOF>'), so the
whole file failed to parse and Italian users got zero translations. The
partial entry is dropped and the object closed; the surviving 96 keys
parse and load again. Missing keys fall back to English in Language.label.
Tracker: SPR-MED-011.
Five layout.json files under Emu/NDS/resources/bg carried trailing commas, which strict JSON parsers reject - any consumer using one failed to load these screen layouts. Tracker: SPR-MED-012.
Translations had drifted 160+ keys behind English (181 for the repaired Italian file), and Vietnamese carried seven keys English no longer has. Every parseable language file is rebuilt in English key order: existing translations kept, missing keys filled with the English string so the UI shows text instead of nothing, stale keys dropped. Tracker: SPR-LOW-008.
get_extlist lowercases entries, so pbp|PBP collapsed into a duplicate; matching is case-insensitive already. Tracker: SPR-LOW-007.
Emu/ATOMISWAVE/config.json is active but no Roms/ATOMISWAVE folder existed, so the system could never list or scan games. Matches the NAOMI layout (Imgs/.gitkeep). Tracker: SPR-LOW-007.
Two RetroArch autoconfig files and the rg35xxsp key map carried CRLF endings; on-device parsers read the stray carriage returns as part of values. Tracker: SPR-LOW-007.
App/Credits declared led.png without shipping it and commented its label out as '#label', so themeless launches drew no icon and no name; Emu/ATOMISWAVE referenced atomiswave.png/_sel.png that did not exist. Copies of the SPRUCE theme's own icons now live beside the configs like every other app/emu, and the Credits label key is real again. Tracker: SPR-MED-029.
An empty or fully-excluded game pool re-ran the selection loop without end, pegging the CPU behind the please-wait screen. Thirty failed draws now end with a clear message. Tracker: SPR-LOW-001.
eval on the composed command string executed any shell metacharacters in a ROM file name; standard_launch.sh is now invoked directly with the path as a quoted argument (the cmd file keeps its shell-line format for the resume/kill consumers). get_rand_file also returned mid-loop with IFS still set to newline, leaking it into the caller. Tracker: SPR-MED-034.
If entering TMP_DIR failed, 'rm -r ./*' ran in whatever directory the script started from. Tracker: SPR-MED-033.
PyUI launches these directly. deleteMacFiles.sh called log_message without sourcing helperFunctions.sh and assumed python3 on PATH; fetchPPSSPPauth.sh invoked the auth helper by repo-relative path, which only worked from the payload root. Helpers are sourced, the device Python resolver is used, and the path is absolute. Tracker: SPR-MED-037.
command.txt comes from the GUI's stream selection; eval executed any shell metacharacters in a host or app name. The line is now field-split with globbing off and passed as arguments. Tracker: SPR-MED-035.
Inside the double-quoted preset heredoc-style strings, the bare "" after 'alias0 = ' closed and reopened the shell string, so the written .glslp carried an unquoted empty value instead of alias0 = "". Tracker: SPR-MED-013.
The description used 'ReARMed' while the task key uses 'ReARMED', so PyUI showed no description for that task. Tracker: SPR-MED-036.
Follow-up to the empty-pool escape: use a named constant and counter so the bound is self-describing. Tracker: SPR-LOW-001.
audio_cleanup went through _send_cmd, whose _ensure_worker spawned a brand-new worker - opening the audio device - purely so the cleanup command could kill it. With no live worker the state is now reset directly. Tracker: SPR-MED-004.
Two display_message_multiline lists were missing a comma, so adjacent f-strings concatenated into 'Scale factor is 0.75Patching main assets' on one line. Tracker: SPR-MED-032.
'[ -d ] || log && return 1' groups as '([ -d ] || log) && return 1': when the backup folder existed the guard still returned 1 immediately, so theme config restore never ran at all; when it was missing, only the log-and-return path worked by accident. Now an explicit if. Tracker: SPR-MED-005.
'7zr l | grep /mnt/SDCARD/' matched the archive's own pathname in the listing header, so every queued archive passed the layout gate and was extracted at / wherever its members pointed - a malicious or malformed theme archive could write anywhere on the filesystem. The gate now reads member paths from 7zr -slt and requires every entry to sit under mnt/SDCARD/ (ancestor directory entries allowed); staged e2e tests confirm wrong-layout archives are skipped and kept in the queue while correct themes still install. Tracker: SPR-HIGH-010.
CatalyticArkun
requested review from
Sundownersport,
chrisj951 and
ryanmsartor
August 16, 2026 19:04
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
https://klipy.com/gifs/dump-truck-dirt
Results of an automated Fable audit.
Issues are separated into commits for cherry picking. I haven't had time to device test all of these, but they're all host-verified with positive and negative unit tests.