* feat(elfldr): vendor elfldr with a read deadline, embedded in the engine
elfldr serves one connection at a time and its reads had no deadline,
so a client that went silent without closing (a dropped link, a machine
or console asleep mid-send) left it running but deaf on :9021 — users
had to load elfldr again. Measured on the Phat (FW 5.10): the stock
build answered nothing while a silent connection stayed open; with a
15 s SO_RCVTIMEO it dropped that connection and answered the next
request at once, and a payload sent with 10 s pauses still loaded.
third_party/elfldr is upstream 02cfe91 plus ps5upload.patch (GPLv3,
credited), with the tested ELF checked in so every engine build embeds
it; `make elfldr` rebuilds it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(elfldr): the engine swaps a console's stock elfldr for the patched one
GET /api/ps5/elfldr/health tells healthy / stuck / absent from a request
elfldr answers without running anything. POST /api/ps5/elfldr/ensure
replaces the standard elfldr.elf with the embedded patched build when it
answers and isn't the one this engine installed (a reboot or autoloader
brings the stock one back), leaves any other loader on :9021 alone, and
returns once the new one answers. On the Phat: upgraded (pid 685,
answering at once), then 'current'; the helper was untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ci(publish): give each client build 75 minutes
The Intel macOS build ran past the 45-minute limit for v5.37.0 (about
33 minutes for v5.35.0 and v5.36.0), which cancelled it and skipped
create-release, so v5.37.0 was never published.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(engine): history tests get their own temp dirs
TmpDir named directories by pid and nanoseconds, but macOS time ticks in
microseconds: two tests starting together shared a directory and one
deleted it under the other (a_legacy_host_port_file_is_still_read read
nothing). A counter makes each name unique. The elfldr embed test reads
the embedded bytes directly instead of clearing an env var mid-run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(elfldr): the app keeps the patched elfldr in place and never sends into a stuck one
Before sending the helper, the app checks the console's loader: a stuck
one (took the connection, never answered) is waited out for 20 s — the
patched build clears in 15 — and one that stays stuck is reported
plainly ('load elfldr again') instead of a socket error. Once the helper
is up, the engine swaps the stock elfldr for the patched build: at once
after a fresh helper send (a reboot or wake brings the stock one back),
else at most every 10 minutes per console. Desktop, browser and Android
share the engine routes; strings in all 20 languages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(changelog): 5.37.1 — elfldr recovers by itself; 5.37.0 folded in
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(elfldr): swap it in when the helper is already up at launch
Only ensurePayloadCurrent asked for the patched elfldr, and a helper
that is already up when the app opens never goes through it — on the
Phat the stock elfldr stayed. The status poll's 'arrives at up' edge
(first sighting included) now asks too, after the same settle as the
log capture and never during a transfer.
Hardware (Phat FW 5.10, web build and Android app): stock elfldr
replaced ~10 s after opening; a silent :9021 connection held while the
helper was shut down — the helper came back through the patched
elfldr; with the stock elfldr wedged, the Android app sent nothing for
150 s (health: stuck), then redeployed and swapped elfldr once released.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(convert): the compress stage pays for reading and writing on one thread
kraken_image::compress runs the encoder on a pool, but one thread reads
the source and writes each compressed block, so the stage takes the
longer of encoding and read+write — not the slower of reading and
encoding, as the estimate assumed. With the Windows Battlefield 6 run's
speeds the model now gives 75 min for a 71-min build.
The estimate also reports the speeds it measured (rates), because the
small read/write samples can be fooled by caches: on a source read an
hour earlier the read sample hit the file cache at 8.6 GB/s and the
estimate came out at 25 min for a 54-min build. Calibrating from past
builds is the follow-up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(changelog): build-time estimate, stated as measured
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ci(docker): 25 minutes for the PR image checks
Both PR gates build the whole image (engine, and the client for the web
UI one) in about 7-10 minutes; the engine gate took 9m55s and the web UI
gate was cancelled at its 10-minute limit on PR #338.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(docker): the web UI image bundles the FAQ and Changelog
Since the FAQ and Changelog screens bundle ../FAQ.md and ../CHANGELOG.md,
the web UI image's frontend stage failed: it built the client at /src
and .dockerignore excluded every *.md. The stage now keeps the repo
layout (/repo/client) and copies the two files in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(scripts): the audit counts a script's node:test file as used
scripts:check runs every *.test.mjs by pattern, so the test beside
stale-client-lib.mjs is named nowhere and the strict audit called it an
orphan (repo scripts failed on all three OSes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A 234 GiB game estimated at 17 min took 71: the estimate timed only
compression, of blocks already in memory, one thread multiplied by the
thread count. It now times the source's sequential reads and the
output drive's writes, runs the sample on every thread at once, and
adds the package's write and verify to compression bounded by the
slower of reading and encoding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Install Package lists every drive's package folder; a drive without one
logged a gateway error. The body is unchanged, which is what the app
reads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It reported only the unlogged repeats, so one logged 502 followed by a
200 read "recovered after 0 failure(s)".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parent-watch thread's first eprintln! panicked on the closed stderr
pipe a dead shell leaves behind, so the engine never shut down and held
its port (reproduced with the v5.36.0 app: killed shell, engine alive
25 s later). Shutdown is now triggered before any message, messages use
a write that cannot panic, and the notify keeps a permit so a parent
that dies during start-up isn't missed (it fell to the 10 s hard exit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The copy itself got a 409 when its name was taken, but a set's other
volumes were written over, and cleanup would then delete them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Battlefield 6's eboot imports scePlayGoGetLanguageMask: it asks the
install which language it has, and every converted package answered
"all 31, en-US first". Convert's Options card now takes one language;
the package's playgo-scenario.json then lists it as the default and
only language (the shape Sony's own scenarios take). Carried through a
single run and the Convert queue; reset per game.
Checked on the Phat (FW 5.10): a fr-FR Minecraft build installs and
plays like the all-language control.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UnRAR finds the rest of a set beside its first volume, so copying only
game.part1.rar (or game.rar of an .r00/.r01 set) could never extract. The
server copy now brings the set's other volumes into the same folder and
cleanup removes them too - for Upload, a batch item's turn, and Convert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured 2026-09-28 on the Phat over this Mac's Wi-Fi: 7.8 MB/s for 2 GiB,
the same as curl on the same file (7.9 MB/s) — the reader adds no overhead;
the link is the limit (117 MB/s wired in the spike).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add this game / Add games from a folder… queue builds on this computer, one at
a time, each shown by the Convert screen's own stage view. After converting:
keep the package, or Stream & install / Upload & install - the package is
handed to the console queue and the next build starts while it installs
(a console dump still swaps inside its build). Optional delete after a
verified install. Reorder, remove, stop after the current build; a build
interrupted by a restart is built again, and the engine now clears an
interrupted build's stale .pkg.partial instead of refusing the rebuild.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client writes a request and its newline separately; a fake that answered
after the first segment closed over unread bytes, and the reset beat the
reply to the client (hello_over_socket flaked under load).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The viewer reads remote:// and ps5:// sources through the engine's opener, and
.pkg parsing now works over any reader, so a staged package opens straight
from the console: every Install Package row gets View details, reading the
local original when there is one. New GET /api/game/inspect/files lists a
package's entries or a tree's files (a windowed list for 200k-file dumps);
Images shows and saves every artwork; Related groups the title's other
library and queued packages base → update → DLC, each viewable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A server folder or image is built straight off the server through the
engine's server file system (8 MiB read-ahead, retries) - no copy stage;
only a server archive is still copied, then unpacked. 'On the PS5' browses
the console and reads a dump through ftpsrv (:2121) as ps5://host/path;
installed games under /user/app are refused as encrypted. source::open
resolves remote:// and ps5:// through an opener the engine registers, so
build, estimate, inspect and the viewer all read them. Compression still
copies a server image first (it reads local disk).
Verified on the Phat (5.10): inspect + build of an image on the console
over ps5://, same package size as the local build; /user/app refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An archive is unpacked into .ps5upload-extract-<job> in the output folder as
its own Convert stage (progress, Cancel), the game found inside - a folder
however wrapped, or a single .exfat/.ffpkg/.ffpfsc image - is built, and the
unpack is removed when the run ends. Space for the files plus the package is
checked first; unpack folders left by an earlier run are swept. RAR takes a
password (never stored); a wrong one on a content-encrypted RAR now says so
instead of 'File CRC error', for uploads too. zip/7z are built without their
encryption features, so a protected one is refused with that reason. A server
archive is copied, then unpacked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PfscReader is Read + Seek over a .ffpfsc's inner file, decoding 64 KiB zlib
blocks on demand with a small cache; the single-file layout parser is shared
with verify and bounds-checked for images users bring. exFAT and UFS2 sources
now open from any reader. A package built from a .ffpfsc is byte-identical
to the one built from its image.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Cache key covers a folder's PARAM files and a package's split parts, and a
hit keeps its token alive.
- Content-id slicing can't panic; text read from an image or folder is capped;
a failing OS random source is an error, never a guessable token.
- SFO counts and levels show in decimal (flags and versions stay hex).
- "Entries" for a package, "Parental level" for PS4; Copy uses the shared
clipboard helper; a reply for an earlier path or console is ignored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An accepted socket inherits the listener's O_NONBLOCK on BSD/macOS, so a read
before the client wrote returned WouldBlock and the request went unrecorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(pkg): choose which drive install packages are kept on
Uploaded packages always went to internal storage
(/user/data/ps5upload/pkg_library), which fills the console's own drive.
The Volumes screen now lets you mark a drive "Use for packages"; new
packages are staged under <drive>/ps5upload/pkg_library on it.
- Per console, remembered on this device. Internal stays the default.
- Falls back to internal storage, with a notice, when the chosen drive
isn't connected.
- The package library lists packages on every drive; rows show the drive.
- The folder chain on the drive is created one level at a time.
- The External Packages scan skips a drive's ps5upload/ folder, so staged
packages aren't listed twice.
Hardware (Phat, FW 5.10): Star Wars Racer Revenge base+patch staged on
/mnt/ext0 installed, patched 01.00->01.02 and launched in 2 of 3 trials
(80 MB/s); the first stalled after the header read and is covered by the
daemon-recycle fix that follows. Internal control: 1/1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(install): recycle the installer daemon after a stalled delivery
A console-path install that stalls leaves the daemon's loopback job
"serving" until Sony reads every byte, which never happens, so every later
install was refused as "an install is already in progress" until the
daemon restarted, although the failure told the user to retry.
Measured on the Phat (FW 5.10). A stalled daemon-served install now stops
the daemon the same way a wedged stream refusal already did, and the next
install's ensure brings up a fresh one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PS5Upload v5.36.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
release-posts.js
- Reads whole changelog groups (bold heading + wrapped bullets), not just
the first line of each bullet.
- X: link counted as 23 chars, replies packed to fit 280, markdown stripped.
- Discord: full bullets when they fit 2000 chars, short ones otherwise;
Reddit: real markdown sections instead of a code block.
- Says "pre-release" when GitHub does (gh), --prerelease/--stable to
override; --only and --out to pick a platform and write files.
- make release-post calls it directly; the .sh wrapper is gone.
Removed (outdated)
- ps5-install-trace.py, ps5-install-watch.py, fpkg-mount-test.py: drove
/api/pkg/install/start and the dpi-* routes, which no longer exist.
- gather-fakelibs.py: superseded by the app's Backport scan.
- pull_ps5_debug.py: pulled a debug dir the helper no longer writes to.
hw-test-loop.sh ran cargo from the repo root (no workspace there); it now
points at engine/Cargo.toml.
README: a Highlights section up top (unified install, Convert to FPKG,
sleep and wake), Android/web in the platforms badge, tested firmware 5.10 &
9.60.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>