Full audit of every route and response shape in server.c against
docs/API.md, cross-checked with the original flatz implementation
(downloaded and diffed format string by format string). The code is
byte-compatible with the original; the docs were not:
- get_task_progress: lengths/counters/percent are unquoted JSON
numbers (0x1A2B3C00, 120, 100), not strings
- is_exists: 'size' is an unquoted hex number
- error_code in failure bodies is an unquoted hex number
- document the 'GET /' alias of /api/v2/system
- clarify the is_exists / progress notes accordingly
No code changes — the implementation already matches the original
wire format exactly.
The v2 job-op endpoints were documented (README, docs/API.md) as POST
but route() only dispatched them under GET/HEAD, so POST fell through
to 404. Clients had to work around it by falling back to GET.
- dispatch POST /api/v2/jobs/<id>/<op> to handle_v2_job_op()
- keep GET on the same paths routed (backward compatibility with
clients written against <= 0.0.2) and document that in API.md
No handler changes: pause/resume/stop semantics are unchanged.
* fix(installer,http): accept long signed urls, init temp paths before early failure
- All url holders (registry, proxy pieces, prefetch) now store up to
2048 chars (TASK_URL_MAX/PROXY_URL_MAX); silently truncated presigned
S3/Azure urls (>511 chars) previously broke proxied installs.
- install_pieces validates url length up front with a clear error and
initializes the temp manifest/icon paths so the early-failure paths
(registry full, network down) cannot unlink() uninitialized stack
garbage.
* fix(taskman): keep paused jobs paused, detect queue exit while paused
- The REGISTERED->RUNNING promotion no longer includes PAUSED: a paused
task has transferred_total > 0, so the old condition flipped every
pause back to RUNNING within one 1s poll (dashboard/API showed a
paused download as running).
- A task that leaves the BGFT queue while PAUSED or REGISTERED (deleted
from the console's download list, dropped by BGFT) is now detected and
moved to a terminal state; previously such slots were stuck forever
and could fill the 32-slot registry.
* fix(proxy,server): single-ownership stream lifecycle, stop streaming to dead clients
The proxy registry kept a pointer to the stream being served
(job->stream_pf) but the serving connection owned and freed it via
proxy_stream_close, leaving a dangling reference behind:
- piece change: proxy_stream_open retired (abort + prefetch_free) the
already-freed previous stream -> use-after-free on every multi-piece
proxied download;
- job teardown: the terminal event fired proxy_unregister, which freed
the dangling stream a second time -> double free after every
successful proxied install.
New ownership model: the serving connection remains the sole owner.
proxy_stream_close(job_id, stream) clears the registry's reference
under the registry lock before freeing; retiring a piece change and
unregistering a job only abort the stream (instant, and no longer
joins worker threads with the taskman poll lock held) - its owner
winds down and frees it. Dead-code proxy_get_piece and the unused
stream_open flag are gone.
conn_send now reports failure and the static/proxy serving loops stop
pulling data for a gone client, so an aborted/stopped download no
longer keeps transferring the whole piece from the origin.
* fix(server): null-check install url list allocations
A failed calloc in the legacy and v2 install handlers dereferenced
NULL while collecting package urls.
* feat(icon): add pkg icon loader with vendored stb_image
* feat(installer,taskman): track app version, content type and piece count
* feat(ui): pkg icons, version and expandable details on transfer cards
* feat(api,docs): expose pkg metadata in v2 jobs
* fix: net stack teardown order and bounded SFO key reads
- netmon: sceNetPoolDestroy was called with the mem id after it was
already reset to -1, so the real pool leaked at shutdown.
- sfo: key comparison read up to 64 bytes past the key table on
malformed SFO data; bound it to the actual key length.
* fix(taskman): reclaim terminal job slots, release already-installed jobs
Terminal-state jobs kept their slots forever, so the registry filled up
after 32 installs and every new install failed until the app restarted.
When full, the oldest terminal slot is now reclaimed (the dashboard
keeps showing the most recent jobs).
ALREADY_INSTALLED jobs never fired the terminal event, so their proxy
slot and temp manifest/icon leaked per attempt; they are now cleaned up
and released when the BGFT task reports already-installed (task id -1).
Also documents that the event hook runs with the registry lock held.
* fix(server,installer): legacy error_code response for BGFT failures
The original responds {"status":"fail","error_code":...} (HTTP 200)
when BGFT task registration fails; the rewrite returned HTTP 500 with
an error message instead. The BGFT error code is now carried through
install_result_t and the legacy byte-compatible response is restored.
Also: bound the shutdown wait for in-flight connections (a send stuck
on a dead link could stall the netmon thread forever and block
network-up recovery) and drop a needless 16-byte copy in the
/api/v2/jobs/<id> route that truncated long ids.
* fix(ui): run the re-transfer pipeline off the UI thread
action_retry ran the full install prepare (pkg header, entry table,
SFO/icon fetches plus one probe per piece) on the UI thread, freezing
the dashboard and controller input for the duration; it now runs in a
detached worker thread with the URL copied out of the job snapshot.
* perf(http,prefetch): keep-alive session reuse, race-free teardown
Every 2 MiB prefetch chunk previously created and destroyed its own
template + connection, paying a full TCP/TLS handshake and slow start
per chunk (thousands per large package). Workers now hold a reusable
keep-alive session for the piece (one handshake), with a single retry
on a stale connection.
http_client_term() could destroy the HTTP context while prefetch
workers were mid-request (network drop mid-transfer = crash risk);
teardown is now deferred by an in-flight counter until the last
request/session finishes. One-shot requests keep the previous
behavior (fresh template per request).
Also hoists the duplicated worker_ctx typedef to file scope.