* docs: 6.0.0 changelog, README, FAQ and contributor docs Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * Release prep: version 6.0.0 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(ci): keep the leaked test pools reachable for LSan; skip the tiny-file floor under coverage - ava1-ctest diag/mgmt_fs: the &'static Pool each test leaks for AvaTransport::with_pool is now held in a static list, so LeakSanitizer sees it (and the sessions on its threads) as reachable instead of reporting ~140 KB of indirect leaks the frame-name suppression could never match. Verified under clang ASan+LSan in Linux Docker: diag 13/13, mgmt_fs 40/40. - download.rs tiny-file throughput floor is skipped when cargo-llvm-cov runs (CARGO_LLVM_COV set): instrumentation slows every call several times over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): data_rust servers use the clients' fast timing Five folder-host tests gave the client fast timing (dead after 500 ms) but left the server on the default (ping every 2 s). On a slow runner a half-second gap made the client drop healthy lanes and the job stalled until the 36 s progress limit (both CI runs on #374/#377). The ordered-download tests already set both ends; these now do too. The app uses one timing on both ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): the C lane socket-buffer test accepts Linux's wmem_max cap Same cap as the Rust lane test: Linux CI gives the send side 416 KiB, so the 1 MiB floor failed under ASan in CI. 128 KiB is still far above a default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(ci): gcc unused-result in the FTP selftest, serial ctest in make test, sender state on failure Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(ava1): flush a partial bundle after any reader message, not only a record The last small-file record could arrive while a large file's chunk or root was still queued behind it: the flush saw a non-empty channel, the next message was not a record, and the records sat unsent until the receiver cut the job as stalled (CI-only flake; 7/36 under local parallel load, 0/48 after). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(validate): run the C interop tests serially in make ci too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(ava1): a resumed file goes in one-group pieces so a flapping link still makes progress With kills every 2 s, the 4 MiB tail piece was cut before it was durable on every attempt and the upload retried the same 28 MiB mark until it timed out (1 in 3 locally, CI coverage). Resumed files now send 1 MiB pieces: 5/5 in ~6 s. Also: make docker-engine uses the repo root as context (as CI); governor classification asserts are skipped under the sanitizers, where the C is the bottleneck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): single-instance gate tests hold the C-server lock; relay test cap leaves margin Coverage runs the C tests in parallel; the two gate tests stop the one global C server. The relay order test's 4 MiB/s cap finished in 5.9 s on CI against its >6 s precondition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): periodic-kill bound widened under coverage; tiny-file floor relaxed on CI runners Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): lane-kill test kills until a resend; periodic-kill liveness race skipped under coverage Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): a hung relay fails its test at the bound instead of hanging the CI job The dest-ends relay test ran the relay under spawn_blocking: on a hang the timeout panicked but the runtime then waited forever for the blocking thread (27 min on CI). It now runs on a plain thread. The engine test steps get a 25-minute bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): dest-ends relay test in its own binary on small files; relay logs its silent retries The test hung a CI job twice inside the shared relay binary (behind the 384 MiB relays' lock) and never reproduced locally or in Linux Docker. It now runs alone, on 4x32 MiB (still twice the relay buffer), ~3 s. The relay's open-dropped and retry paths now say so on stderr instead of looping silently. Governor classification asserts also skip under coverage, where the instrumented sender is the bottleneck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): the partly-durable relay test caps its first attempt so it can be interrupted On a fast runner the uncapped relay finished all 160 MiB before the first durable report was observed. The first attempt now reads A through a 32 MiB/s proxy (~5 s); the resume runs uncapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * feat: one-time 6.0 upgrade clean-up that keeps every setting Host (engine, once per data dir): pre-5.x files no 5.41/6.0 code reads (api_token, ps5upload.ini, ps5upload_profiles.ini/.json, app-config.json) move to ~/.ps5upload/legacy-5x/. App (desktop, once): the 1.x/2.x config.json/profiles.json move to legacy-5x/; the cached ezremote-dpi.elf nothing loads is deleted. Client: the retired upload-streams key is dropped. Console (once per console, on its first 6.0 helper answer): the FTX2 /data/ps5upload/tx and /spool folders are deleted; a failure retries later. The health check no longer lists those folders (its fix would have recreated them). Nothing current is moved or reset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): the download periodic-kill race is skipped under coverage, like its upload twin Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): the tiny-zip floor only catches a stall on CI runners Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(test): 'other calls still answer during launches' is a state check, not a wall-clock bound Both mgmt_t6 tests asserted a sub-300 ms answer while a slow Sony call ran; on a loaded runner that separates nothing. They now assert the answer arrived while the slow call was still in flight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff * fix(ci): reword the 6.0 migration comments for check-no-ftx2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XR8fQMhp7Rdgq2e4U9oDff --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
6.1 KiB
Engine
The Rust workspace: everything between the desktop app and the PS5.
The engine is a local HTTP service. The Tauri app doesn't talk to the
console directly — it calls the engine, which speaks AVA1 to the payload.
That split is deliberate: the same API is reachable from a browser, a
script, or CI, so nothing the app can do is locked inside the app. See
the self-hosted-engine entries in ../FAQ.md.
client (Tauri/React) ──HTTP──▶ ps5upload-engine :19113
│ AVA1 (Noise XX, sealed frames)
▼
payload :9120
Crates
| Crate | What it is |
|---|---|
ava1 |
The AVA1 protocol: frames, generated message codecs, Noise handshake, sessions, lanes, pairing. Spec: protocol/ava1/SPEC.md. |
ava1-gen |
Generates the Rust and C codecs from protocol/ava1/schema/ava1.toml. Run it after editing the schema. |
ava1-ctest |
Builds the payload's AVA1 C on the host so cargo test checks C against Rust. |
ava1-chaos |
A misbehaving TCP proxy (latency, bandwidth caps, blackholes, kills) for resilience tests. |
ps5upload-core |
The bulk of the logic — socket helpers, the management seam, archive inspection and plan previews, filesystem and app RPCs, volume parsing, package install, saves, cheats, hardware, SMB, BPS patching. |
ps5upload-engine |
The Axum HTTP service (~100 routes), job tracking, SSE progress events, and the desktop + mobile entry points. |
ps5upload-pkg |
.pkg parsing — headers, entries, split-file sets. |
ps5upload-lab |
CLI for driving the console by hand over AVA1: management calls, uploads, benchmarks (bench), pairing and chaos tools. Useful when you want one call, not a workflow. |
ps5upload-tests |
Integration tests that run against in-process and loopback AVA1 peers, with no console. |
Working on it
cargo test --workspace # no PS5 required — loopback peers
cargo test -p ava1-ctest -- --test-threads=1 # payload AVA1 C vs Rust, on the host
cargo fmt --all -- --check # CI fails on this; client/src-tauri has its own
cargo build --release -p ps5upload-engine
Run it standalone and point it at a console:
PS5_ADDR=<ip> cargo run -p ps5upload-engine
curl "http://127.0.0.1:19113/api/ps5/status?addr=<ip>"
PS5UPLOAD_ALLOW_IP lets another machine reach it. The API is
unauthenticated and can read, write and delete files on the console —
keep it on a trusted LAN.
Testing
engine/crates/ps5upload-tests/tests/ runs against a loopback AVA1 console
(the Rust job host plus a scripted management node): single-file, folder,
small-file, resume-after-drop, retry classification, exclude rules,
.zip/.7z/.rar streaming, hardware commands, and volume parsing. Unit
tests live beside their modules.
Anything touching real transfer behaviour still needs hardware — see
../TESTING.md.
Things that will bite you
- The payload emits JSON; serde parses it. Field names must be
snake_caseor serde silently leaves the field at its default — a zero or an empty string, not an error. One malformed byte rejects the whole response, so anything interpolated into a payload-side JSON string has to be escaped first. ok: falseinside a 200 is a refusal, not a success. Several payload commands answer that way.client/src/api/ps5.tshas anassertOk()for action endpoints; status endpoints keepok:falseas data because there it means "unsupported on this console".- One port, one session. Everything goes to 9120 as AVA1: management calls on the session's control connection, bulk data on its lanes.
- A stream install has the PS5 fetch the package from the engine. The
engine tells the console an address to connect to; behind Docker's bridge
network, a VPN or a virtual adapter that address is one the PS5 cannot
reach.
PS5UPLOAD_PKG_HOST_IPpins the LAN IP the console is given (publish port 19113 when the engine is in a container;compose.yamluses host networking instead). Every stream-unreachable failure — the pre-install reach check, a Sony refusal before any byte was fetched, and an accepted install the console never fetched from — ends ininstall/mod.rsstream_unreachable_hint_for, which names this variable.
Where the web UI's file browser opens (PS5UPLOAD_BROWSE_ROOTS)
The browser-based UI picks local files and folders from the engine's filesystem. By default
that picker opens at the engine's home directory, which in the Docker images is /data, the
engine's own state volume, not the folder where you mounted your packages. Set
PS5UPLOAD_BROWSE_ROOTS to a comma-separated list of directories (for example /pkgs, or
/pkgs,/games) and the picker starts there instead. compose.yaml sets it to /pkgs, next to
the volume mount it describes.
- Unset or blank keeps the old behaviour (the home directory).
- An entry that is not a directory the engine can see is skipped and named in the engine log (a warning), so a typo or a volume that was never mounted is visible. If no entry is usable the picker falls back to the home directory.
- It only chooses where the picker opens. It is not a sandbox: the engine can still be asked for any path it can read.
HOMEis deliberately left alone, because FPKG conversions write under$HOME/Downloads/fpkgsand a package mount is usually read-only.
Synology and other NAS boxes (saved servers: "Permission denied")
The Docker images keep their state (saved servers, install history, artwork cache) in /data
and run as UID:GID 65532 unless you override user:. If you bind-mount a host folder that
someone else owns, writing fails and the engine says so, naming the folder, the UID:GID it runs
as, and the owner it found (#361). Fix it one of two ways:
- set
user:in the compose file to the folder's owner (ls -lnon the host shows the numbers), for exampleuser: "1234:77777"; or chown -R 65532:65532 <host folder>for the mount.
USER_ID / GROUP_ID environment variables are not read by these images; use user:.
The account must also be allowed to write that folder in the NAS's own permission settings.