Files
ps5upload/engine
PhantomPtrandClaude Opus 5.5 4eaa6af456 6.1.2: game-folder uploads no longer freeze on the console (#395)
* fix(test): live console tests use a per-run job id

A console remembers a finished job under its id; the live perf test reused a fixed id for a
new tagged destination each run and was refused ("this job was opened for another
destination") from the second run on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkU45KbpqSUxuUeeq6L9rG

* fix(ava1): a peer's close reason wins over our racing write failure

A peer that refuses us (not paired, refused join) writes an Error and closes. Our next
write, often a ping, then fails with a broken pipe before the reader has read that Error,
and the session ended as "write failed: broken pipe" instead of the real reason. A failed
write now marks the writer dead at once and gives the reader 200 ms to deliver the peer's
reason; the first close wins. Seen as a sanitizer-only CI failure
(a_data_frame_before_pairing_is_answered_not_paired_and_closes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkU45KbpqSUxuUeeq6L9rG

* fix(payload): a large file with its root but missing chunks no longer pins a descriptor slot

Uploads of game folders froze on the console (35k-file tree: ~13k files in, then nothing).
A large file holds a descriptor slot until it commits; a job gets ~61 slots on the console
(fd budget ~490). Workers waiting for a slot close idle files to make room, but skipped any
file that had its root, assuming its commit was about to run. The root comes on the control
connection and can arrive before the file's last chunks are applied; those chunks sit in
the queue behind the waiting workers. Such a file could neither commit nor be closed, and
once a job's whole share was files like it every worker waited forever, holding the
sender's whole credit window. Only a file with its root AND every byte durable is kept open
now. Regression test freezes without the fix (2/2) and passes with it; the real folder now
uploads on both consoles (FW 13.60): 35,254 files in ~3.5 min each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 6.1.2: version and changelog; keep the fs_ops test building on both toolchains

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:22:41 -07:00
..
2026-09-27 04:31:13 -07:00

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_case or 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: false inside a 200 is a refusal, not a success. Several payload commands answer that way. client/src/api/ps5.ts has an assertOk() for action endpoints; status endpoints keep ok:false as 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_IP pins the LAN IP the console is given (publish port 19113 when the engine is in a container; compose.yaml uses 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 in install/mod.rs stream_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.
  • HOME is deliberately left alone, because FPKG conversions write under $HOME/Downloads/fpkgs and 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 -ln on the host shows the numbers), for example user: "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.