makepad/apps/asset-server/README.md
Admin 9d2e17e3b9 chore: README, workspace, agent docs, and the box-driving scripts
- README: a build quick-start for macOS and Windows, the honest Linux
    story, what CUDA is for and how to install the separation model.
  - AGENTS.md: the `--remote` control surface protocol, so the harness it
    documents is usable without reading platform/src/remote.rs.
  - Cargo.toml: workspace membership for the crates this series adds and
    removes.
  - tools/: the Windows box scripts (wincmd, winps and friends, winrun) and
    remote_smoke.sh — how a build gets driven on a remote machine.
  - apps/asset-server: the standalone server binary and its README.
  - Small follow-ups in libs/{windows,apple_sys,makepad_test,mbtile_reader,
    converse} and apps/route, plus .gitignore and makepad.splash.
2026-08-23 01:34:36 +02:00

156 lines
7.2 KiB
Markdown

# `makepad-asset-server` — the standalone Asset Server host
The asset store is inherently **multiplayer**. asset-ui, the VJ, the sandbox
and headless workers are all clients of one catalog, and they open and close
independently of each other. So the catalog's lifetime must not be any one
client's lifetime.
Historically the Asset UI *embedded* the server: it took `<root>/server.lock`,
served the catalog, ran the chat broker and the events hub — and every time
that window was closed or rebuilt, every other connected client lost the
store mid-session (`503 state unavailable`). This binary is the fix: the same
service, in its own process, that survives every window.
```
cargo build --release -p makepad-app-asset-server
./target/release/makepad-asset-server
```
With no flags it serves the checkout's standard store root
(`local/asset-ui/asset-server`, or `$AI_CONTENT_ASSET_ROOT`) on ephemeral
ports, announces itself on the LAN beacon, publishes
`local/ai_content_library`, and coordinates fleet jobs. `--help` lists every
flag.
---
## What this process carries
| Part | Where it lives | Notes |
| --- | --- | --- |
| Catalog + CAS, control/data planes | `AssetServer::start` | search, blobs, ranges/ETags, retire |
| Auth, tokens, grants | ” | `--root/admin-token` bootstrapped at start |
| Job queue, worker/lease protocol | ” | plus `GET /v1/job-profiles` |
| Chat broker (+ client-executed tool parking for game sessions) | ” | `--chat-fleet` / LAN fleet discovery |
| Games publish path, operations, import routes | ” | |
| Committed events hub (`/v1/events`) | ” | every client's live view |
| LAN discovery beacon | ” | `--no-beacon` to stay silent |
| Lease janitor + bounded blob GC | ” | runs whether or not anyone polls |
| **ai-content library publisher** | `makepad_asset_importer::watch` | `--library` / `--no-library` |
| **GPU-fleet job coordinator + profile announce** | `makepad_asset_importer::gen_service` | `--no-jobs` / `--no-announce` / `--fleet` |
The last two are the loops the Asset UI used to run *only while it was
hosting*. They are headless — no `Cx`, no window, no GPU surface — so they
belong beside the server, and running them here is what makes a UI-less
deployment a complete fleet citizen. Without the coordinator, jobs any client
enqueues sit at "waiting for agent" forever.
## What deliberately stays client-side
These are **content-deriving** loops that need resources only a UI process
has. They reach the catalog as ordinary clients; moving them here would mean
giving a headless daemon a window.
- **Thumbnail / preview backfill** (`apps/asset-ui`: `thumbnail_renderer.rs`,
the splat + mesh offscreen renders). Needs a real `Cx`, render passes and
GPU readback.
- **Classic-game import** (`import_classic.rs`) and **pack import** wizards —
interactive, and their icon bakes are GPU renders. The one-shot headless
equivalents already exist as `makepad-asset-importer --import-pack /
--import-games / --import-music / --import-ai-library` when a batch run is
wanted.
- **Stems / lyrics analysis bake** (`analysis.rs`) — a large local model on
the operator's machine, driven from the surface that asks for it.
- **Generation pipelines** driven from the Create surface (`pipeline.rs`) —
these are *requests*; the queue and the dispatch live server-side.
## Recommended deployment
One daemon, every app attached:
```bash
# 1. the store (leave it running; it survives every UI restart)
./target/release/makepad-asset-server > /tmp/asset-server.log 2>&1 &
# 2. every client, told never to hold the root itself
export ASSET_UI_ASSET_EMBED=never
./target/release/makepad-app-asset-ui
./target/release/makepad-vj
```
`ASSET_UI_ASSET_EMBED=never` (aliases: `no`, `off`, `0`, `false`, `attach`,
`client`) makes the Asset UI a pure client: it never takes
`<root>/server.lock`, at startup or during a succession. Without it the app
still attaches when the daemon holds the root — but whichever process starts
*first* wins the lock, so a UI launched before the daemon would put the store
back inside a window.
The VJ and the sandbox are already pure clients; they need no flag.
### How a client finds the server
Three ways, in order of precedence:
1. `ASSET_UI_ASSET_SERVER=ip:controlport:dataport` — an explicit pin.
2. `<root>/listen`, `<root>/server-id`, `<root>/admin-token` — what the
daemon writes at startup. This is the attach path, and it is why the
default bind is `0.0.0.0:0`: the *file* is the address of record.
3. The LAN discovery beacon — how a client on another machine finds it with
no configuration at all.
## What survives what
- **Closing / rebuilding / crashing any app** — the server keeps serving.
Every other client stays connected; nothing is lost.
- **Restarting the daemon** — attached clients notice the silence within
~3.5 s, say so honestly in the status chip, and rejoin the new process as
soon as it rewrites `listen`. Cursors and leases are re-established; the
events journal reports an explicit `gap` rather than a silent hole.
- **A daemon crash with `EmbedPolicy::Auto`** — an attached Asset UI succeeds
it (takes the lock, starts the loops, reconnects to itself). This is the
transition-mode safety net, not the deployment.
- **A daemon crash with `ASSET_UI_ASSET_EMBED=never`** — no app fills the
vacancy; clients wait and rejoin whoever takes the root next. Restart the
daemon.
- **Two daemons on one root** — refused immediately and by name
(`server root: locked by another server process`). One process per root is
a law: the job routing metadata assumes a single enqueuer, and two writers
over one WAL catalog would be two sources of recovery truth.
## Bouncing the daemon safely
```bash
# find it (it holds the lock and wrote the listen file)
cat local/asset-ui/asset-server/listen # ip:control:data
pkill -TERM -f 'makepad-asset-server' # clean shutdown: joins every thread
./target/release/makepad-asset-server > /tmp/asset-server.log 2>&1 &
```
SIGTERM/SIGINT shut down in order: background loops first (so nothing is
still publishing into a closing catalog), then the planes, then the state
thread. Startup logs what recovery found — `recovered N cas temps / M leases`
— which is the honest report of what the previous life left behind.
## Isolated instances
Never point a scratch instance at the live root, and never let one beacon:
```bash
./target/release/makepad-asset-server \
--root /tmp/scratch-store --work /tmp/scratch-work \
--no-beacon --no-jobs --no-library
```
`--no-beacon` keeps peers hunting for the real store from finding the scratch
one; `--no-jobs` keeps it from claiming the real fleet's queued work.
## Tests
`cargo test -p makepad-app-asset-server --release` covers the composition:
the host serves a catalog over real sockets and shuts down cleanly, a second
host on one root is refused by name, a missing library directory never costs
the catalog its server, and the defaults are the deployment defaults. The
parts themselves are covered where they live — `libs/asset/store` (HTTP,
chat, events, jobs, security, operations suites) and `libs/asset/importer`
(watch, coordinator, gen-service). The attach/succession contract is in
`apps/asset-ui/src/asset_store_state.rs`.