The owner's report: tiles deleted in the page stayed on the home screen after closing
RetroArch (from the home screen, and with the picker's Quit). The console's records showed
the removals done, but late: ShadowMountPlus refuses an uninstall while a game runs, and
the WebUI's daemon only tried again at its stop, 20 s after the title closed; a title
reopened within those 20 s left them for the next close.
The daemon now tries a tile's pending work (a removal, a new name or picture) as soon as
no program of the title is linked, and every 3 s while ShadowMountPlus still refuses (the
exiting app answers "game active" for a moment); the try at the stop stays for what is
left. ps5_forwarders::finish_pending says whether anything still waits, and runs under the
WebUI's lock (the page may be open meanwhile). README: "a few seconds after RetroArch
closes".
Evidence:
- evidence/game-forwarder-delete-on-close: on the console, a tile installed, deleted
through the WebUI while RetroArch ran ({"state":"pending"}), RetroArch closed:
/user/appmeta/PPSA99170 gone after 3.0 s; the daemon "13:18:48 not removed yet ...
game_active", "13:18:51 forwarder PPSA99170: removed"; ShadowMountPlus "uninstall
requested" at 13:18:51 (before: 20 s and more).
- tests/test_webui_daemon.py test_a_tile_removal_is_done_as_soon_as_the_title_closes:
nothing while a program is linked, the uninstall within 1.5 s of the unlink, before
the stop (2 s in the tests); test_webui_forwarders checks finish_pending's answer.
- gates: format, unit (211 tests), build, integration (211 tests), evidence (139
captures, 0 failed) - PASS.
src/ps5_forwarders.{h,cpp}: a tile is /data/homebrew/<id> (PPSA99170 to PPSA99269, an
id no folder has and ShadowMountPlus knows nowhere) with the tile app and payload of
step 1, libc.prx, a param.json for its id and name, an icon and forward.txt (the
arguments, which also mark the folder as a tile: nothing else is listed, changed or
removed). ShadowMountPlus's HTTP API (127.0.0.1:10101, its docs/api.md 1.7) puts it on
the home screen: manual/add at once, installed when no game runs.
Routes (src/webui_ps5.cpp): GET/POST /api/forwarders (the list with ShadowMountPlus's
state, the cores, each tile's state; create), POST /api/forwarders/edit?id=,
PUT/GET /api/forwarders/icon?id= (a 512x512 PNG, up to 2 MiB), POST
/api/forwarders/delete?id=. The library JSON carries each game's core, to prefill it.
ShadowMountPlus refuses to uninstall while a game runs, and RetroArch is one whenever
the WebUI is up: a removal of an installed tile takes it out of the manual list at
once (so no scan installs it again) and is finished by the WebUI's daemon once the
title has closed (daemon/webui_daemon.cpp -> finish_pending): uninstall, then delete
the folder. A new name or icon on an installed tile is re-registered there the same
way (uninstall, back in the manual list, scan), and every tile's app and payload are
brought up to the build's.
Found on the console and handled: ShadowMountPlus 1.7-beta5fix1 can drop a source from
its manual list as it installs it (its next scan reads app.db before the install shows
there); the tile stays installed, and a re-registration adds it back first. Its game
list can take longer than 5 s after RetroArch starts: 20 s now, and a list that does not
arrive says why instead of showing every tile as waiting.
Evidence:
- host: tests/test_webui_forwarders.py against a stand-in for ShadowMountPlus's API:
create (first free id past a plain folder and a title ShadowMountPlus knows), the
files and param.json, validation (name, content path, "..", absolute path, newline,
core), edit, icon (size and format refused, stored, served), removal while busy
(pending, out of the manual list) finished by finish_pending, at once when not
installed, re-registration after the source was dropped, binaries refreshed,
ShadowMountPlus absent - 3 tests OK.
- console (base PS5 13.40, ShadowMountPlus 1.7-beta5fix1), through the WebUI from the PC:
GET /api/forwarders: ShadowMountPlus answers, the test tile PPSA99170 installed;
POST create -> PPSA99171 (eboot.bin, launch.elf, libc.prx, icon0.png, param.json
UP9000-PPSA99171_00-FORWARDER0099171, forward.txt with five arguments), in the
manual list; RetroArch closed -> ShadowMountPlus "installed game PPSA99171";
the tile opened -> tile.log "110832 sent to 127.0.0.1:9021 (errno 0)", launch.log
five arguments, RetroArch "forwarder: /app0/content/snes/<game> with
/app0/cores/snes9x_libretro.so, closing after it"; rename and icon -> updating,
delete PPSA99170 -> pending (manual list empty); RetroArch closed -> daemon
"forwarder PPSA99170: removed", "forwarder PPSA99171: installed again with its new
name or icon"; /user/appmeta/PPSA99171 titleName "Forwarder renamed" and the new
icon (first pixel 20 60 d0), /user/appmeta/PPSA99170 and /data/homebrew/PPSA99170
gone, app.db 33 -> 31 -> 32 entries; the daemon also replaced PPSA99170's older
launch.elf with this build's.
- gates: format, unit (209 tests), build, integration (209 tests), evidence - PASS.
On the console the daemon stopped minutes after every start, the page then refused to
connect, and the status said RetroArch was closed while the picker ran. Its log
(webui-daemon.txt): "serving the WebUI" 6 s (9 s the time before) after its start,
then "linked: picker", "unlinked: title", "unlinked: picker", and 20 s later "no program
of the title linked: the title is closed". The WebUI's start (reading the library, once
for each of 16 saved scraping jobs) ran on the link loop: the picker's hello waited past
its 5 s, the picker gave up for good, and the daemon, linked to nothing, stopped.
- The daemon starts the WebUI on a thread of its own (4 MiB stack, joined before a
stop): hellos are answered meanwhile.
- The saved jobs are read against one scan of the library, not one each.
- src/webui_link.cpp: a hello not answered is asked again every second, never given up
while the daemon is there.
Evidence: tests/test_webui_daemon.py test_hellos_are_answered_while_the_webui_starts (a
start made 7 s slow: both hellos answered in under 1.5 s, the WebUI up later with the
right frontend). tools/verify.sh: PASS (format unit build integration evidence).
The WebUI's Games page scrapes box art, screenshots, title screens and logos from
libretro's thumbnail server into /app0/library/<system>/<kind>/<game>, one store for
every frontend: RetroArch's thumbnail lookup asks it first (patch 0114, which also
gives thumbnails_directory "default" its folder: it was empty and stopped every
lookup), EmulationStation has it as its media folder and fills its game lists from
its .meta files, the Games page shows it, a future frontend calls ps5_library_media.
- src/scraper.{h,cpp}: jobs saved and resumed without repeats, exact names, exact
titles tags aside, ranked candidates for the user, fill-missing by default; PS5 mode
on the daemon (runs with the page and the title closed) and PC mode through
webui/ps5-media-helper.py (the console identifies; the bytes pass through the PC).
- The page: the method asked first and remembered on the console, sources and every
media type described, version tags on each card. ScreenScraper, LaunchBox and
EmuMovies are listed as coming (ScreenScraper's developer credentials requested).
- The daemon's HTTP is libcurl over mbedTLS 3.6 with the shipped CA bundle (Sony's SSL
failed every handshake from the payload, 0x8095f00c); its updater uses it too.
- The shared library: one path a game read from outside the title (14 games were
listed twice), a disc's listed .bin dropped beside its .cue.
- The daemon: workers on 1 MiB stacks (a 64 KiB frame overflowed one), stale hand-over
on transfers in flight rather than idle keep-alive connections, pid in its log.
Evidence: evidence/scraper-libretro (base PS5 13.40): every system in 148 s (520
complete, 28 partly, 181 kept, 61 ambiguous, 234 unmatched); cached runs fetch
nothing; HTTPS through curl; PC mode with the helper killed: 90 images each sent once;
a job finished with the title closed; 1,024 games, 0 duplicates. Not seen live:
RetroArch and ES-DE drawing the media (owner to look), updates through the daemon.
Command: build/tmp/scraper/live.py; tools/verify.sh: all five PASS.
The WebUI only ran inside RetroArch: a frontend change (LoadExec) ended it with
every upload in flight. It is now a payload of its own (daemon/webui_daemon.cpp,
webui/ps5-retroarch-webui.elf) serving the title's folder on 6769. eboot.bin, the
picker and ES-DE each keep a link to it (src/webui_link.h, 127.0.0.1:6770) and the
first to find none starts it through the ELF loader (9021) or websrv (8080 /hbldr).
It stops 20 s after the title closes, never mid-transfer; an update's Install
closes the title and the daemon installs. With no loader, eboot.bin serves the
WebUI while RetroArch runs, as before. The page names the frontend shown.
Downloads were handed to MHD as a descriptor (sendfile), which stopped at 33,300
bytes on the console; they are read in 256 KiB blocks.
Evidence: evidence/webui-daemon (base PS5 13.40). 200 MiB upload across picker ->
RetroArch -> picker and 60 MiB across picker -> ES-DE: one token throughout, 201
Created, every 256 KiB block equal on the console; daemon stopped 20 s after the
title closed; a 1,917,648,896-byte download's sha256 equals the FTP copy.
Command: tools/run-title.sh --picker-test=600:retroarch --retroarch-frames=1800
--watch 110 with build/tmp/webui-handover.py; tools/verify.sh: all five PASS.
Host tests: tests/test_webui_link.py, tests/test_webui_daemon.py.