Support multiple programs per manifest; fix ScummVM launch and audio unlock
Replace the single start_cmd manifest field with a "programs" list so a game can expose more than one runnable binary. Duke Nukem 3D's manifests now offer Play and Setup (DOS sound hardware config), and every ScummVM manifest offers Start and Manager (ScummVM's own graphical Launcher). Also fixes two real bugs found along the way: - All 5 ScummVM manifests were passing the game id as `-f <id>`, but -f is ScummVM's --fullscreen flag; ScummVM rejected it as a stray argument and exited before the setup page's poll interval could notice, so clicking Start silently did nothing. Switched to --auto-detect. - The page's audio-unlock listener called ctx.resume() once on first click/keydown and unconditionally removed itself with no .catch(), so a silently failed first attempt left the AudioContext stuck suspended forever with no way to retry. Now retries on every click/keydown until ctx.state actually reports "running". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
This commit is contained in:
@@ -50,11 +50,13 @@ There is **no single shared desktop/display for video** — unlike the other noV
|
||||
|
||||
**Game installation & lifecycle** (`scripts/setup_server.py`), the key thing that differs from the other noVNC-family projects:
|
||||
- A stdlib-only Python `ThreadingHTTPServer` (no third-party deps, no pip installs) — the *only* thing `server.sh` starts. No Xvnc/fluxbox/websockify run until a game is actually started.
|
||||
- Games are **not** hardcoded anywhere in the image or discovered from raw zip files. Each game is described by its own `<name>.json` manifest living in the `Games\dosbox\` catalog directory on the SMB share, with `name`/`title`/`zip_path`/`release_date`/`publisher`/`start_cmd` fields — see the module docstring in `setup_server.py` for the exact schema. `discover_games()` lists `*.json` under `Games\dosbox\` (`smbclient -N ... -c 'ls Games\dosbox\*.json'`), `smbclient get`s each one into `/tmp/manifests/`, and parses it — so dropping a new `<name>.json` on the share is enough to make a game appear, no image rebuild needed. `zip_path` is a full path relative to the share root (e.g. `Games/Monkey Island/The Secret of Monkey Island.zip`), **not** assumed to live next to its manifest — manifests always live in the `Games\dosbox\` catalog regardless of where the actual game data sits on the share (the SCUMM games' zips live in their own folders, e.g. `Games\Monkey Island\`, `Games\Indiana Jones\`). `release_date`/`publisher` are static declared metadata (historical facts, can't be derived from the share); install size deliberately is **not** a manifest field — `get_zip_sizes()` looks it up live via a batched `smbclient ls` (one `ls "<path>"` per game in a single connection, results split on each command's `N blocks ... available` trailer) so it can't go stale if a zip is replaced.
|
||||
- Games are **not** hardcoded anywhere in the image or discovered from raw zip files. Each game is described by its own `<name>.json` manifest living in the `Games\dosbox\` catalog directory on the SMB share, with `name`/`title`/`zip_path`/`release_date`/`publisher`/`programs` fields — see the module docstring in `setup_server.py` for the exact schema. `discover_games()` lists `*.json` under `Games\dosbox\` (`smbclient -N ... -c 'ls Games\dosbox\*.json'`), `smbclient get`s each one into `/tmp/manifests/`, and parses it — so dropping a new `<name>.json` on the share is enough to make a game appear, no image rebuild needed. `zip_path` is a full path relative to the share root (e.g. `Games/Monkey Island/The Secret of Monkey Island.zip`), **not** assumed to live next to its manifest — manifests always live in the `Games\dosbox\` catalog regardless of where the actual game data sits on the share (the SCUMM games' zips live in their own folders, e.g. `Games\Monkey Island\`, `Games\Indiana Jones\`). `release_date`/`publisher` are static declared metadata (historical facts, can't be derived from the share); install size deliberately is **not** a manifest field — `get_zip_sizes()` looks it up live via a batched `smbclient ls` (one `ls "<path>"` per game in a single connection, results split on each command's `N blocks ... available` trailer) so it can't go stale if a zip is replaced.
|
||||
|
||||
- `programs` is a **list**, not a single command, so a game can expose more than one runnable binary — e.g. `duke`/`dn3d` list both `play` (`duke3d.exe`) and `setup` (`setup.exe`, DOSBox/Build-engine's interactive hardware-configuration wizard, distinct from the game itself). Every manifest on the share has at least one entry (most just have a single `{"id": "start", "label": "Start", ...}`, preserving the plain "Start" button they had before this existed). Each entry gets its own button in the "Installed" row (`doAction('start', name, program_id)`); `POST /start` reads the `program` form field and `start_game()` looks up the matching entry by `id`, using its `cmd` in place of the old single `start_cmd`. Still only one program per game slot at a time — starting a second program while the first is running is refused the same way restarting an already-running game is (`is_running()` doesn't care which program is active, just whether the tracked process is alive). The active program's `label` is stashed in `running_procs[name]["program_label"]` purely for display, so the "Running" status can show e.g. "Running (Setup)" instead of just "Running" now that the game name alone doesn't imply which binary is active.
|
||||
- `GET /` renders one row per discovered manifest (title/release/publisher/size/status/actions) plus a `"{used}/{MAX_CONCURRENT_GAMES} game slots in use"` line at the top. **None of the action buttons are `<form>`s** — every one is a bare `<button onclick="doAction('<action>','<name>')">`; `doAction()` (client-side JS in the page itself) `fetch()`s the action and then calls `refresh()`, which re-`fetch()`es `/` and replaces `#content`'s `innerHTML` in place. The page itself never navigates. This matters a lot — see Audio below for why a real `<form>` submit here was an actual bug, not just a style choice.
|
||||
- `POST /install`: `smbget -au` (guest auth) the manifest's `zip_path` into a `/tmp/setup-<name>` scratch dir, then `unzip -o -d ${GAMES_HOME}`. `smbget -a` (guest) cannot be combined with `-o` (output-file) — they conflict on the underlying `-U` option — so the download `cd`s into the scratch dir and lets `smbget` save under its default filename instead. Installed-ness is just "non-empty `${GAMES_HOME}/<name>` directory exists"; a zip is expected to contain one top-level folder matching its own basename (this happens to already match ScummVM's own game-target IDs for the SCUMM titles, e.g. `monkey2.zip` → `monkey2/` — convenient, not enforced by the code).
|
||||
- `POST /uninstall`: `rm -rf ${GAMES_HOME}/<name>`. Refused while the game is running (stop it first) — there's no auto-stop-then-delete.
|
||||
- **`POST /start`: each game gets its own ephemeral X session, not a shared one.** `_free_display_num()` picks an unused display from `GAME_DISPLAY_NUMS` (`:90`-`:99`, one per `MAX_CONCURRENT_GAMES = 10` slot); if none is free, the attempt is refused and `game_errors[name]` is set to a message shown on that game's row (no slot ever gets allocated for it). Otherwise: clean any stale `/tmp/.X{N}-lock`/`/tmp/.X11-unix/X{N}` for that display (the same reason the other noVNC-family projects do this on startup — a prior Xvnc for that display might not have cleaned up after itself), then start `Xvnc :{N}`, `fluxbox`, and `websockify` (bridging `59{N}` → `80{N}`, no `-D`/daemonize flag — needs to stay a normal foreground child so its `Popen` handle actually reflects whether it's still alive, unlike the old shared-desktop `server.sh` which didn't care), wait ~1s for Xvnc to bind, then `subprocess.Popen(manifest["start_cmd"], cwd=GAMES_HOME/<name>, env={..., "DISPLAY": f":{N}"})`. All four `Popen` handles plus the display/port numbers are tracked together in `running_procs[name]`.
|
||||
- **`POST /start`: each game gets its own ephemeral X session, not a shared one.** `_free_display_num()` picks an unused display from `GAME_DISPLAY_NUMS` (`:90`-`:99`, one per `MAX_CONCURRENT_GAMES = 10` slot); if none is free, the attempt is refused and `game_errors[name]` is set to a message shown on that game's row (no slot ever gets allocated for it). Otherwise: clean any stale `/tmp/.X{N}-lock`/`/tmp/.X11-unix/X{N}` for that display (the same reason the other noVNC-family projects do this on startup — a prior Xvnc for that display might not have cleaned up after itself), then start `Xvnc :{N}`, `fluxbox`, and `websockify` (bridging `59{N}` → `80{N}`, no `-D`/daemonize flag — needs to stay a normal foreground child so its `Popen` handle actually reflects whether it's still alive, unlike the old shared-desktop `server.sh` which didn't care), wait ~1s for Xvnc to bind, then `subprocess.Popen(program["cmd"], cwd=GAMES_HOME/<name>, env={..., "DISPLAY": f":{N}"})` where `program` is the manifest's `programs` entry matching the `program_id` the client sent. All four `Popen` handles plus the display/port numbers are tracked together in `running_procs[name]`.
|
||||
- `POST /stop`: tears down all four processes for that game (game first, then websockify/fluxbox/Xvnc), each `terminate()`d then escalated to `kill()` after a 5s grace period, and cleans up the stale-lock files for that display.
|
||||
- `is_running(name)` does the same full teardown **lazily** if the game process exited on its own (quit or crash) — it won't leave an idle Xvnc/websockify pair holding a slot forever just because nobody clicked Stop. `render_page()` calls this for every tracked game on every load, so the slot count and each row's status stay accurate without polling.
|
||||
- Each running game's row shows its own "Open Screen" link (only while `Running`) — a plain top-level link straight to that game's own noVNC URL (`https://<host>:<novnc_port>/`, opens in a new tab). Deliberately **not** an iframe — see Audio below for why.
|
||||
|
||||
Reference in New Issue
Block a user