Files
docker-dosbox-novnc/CLAUDE.md
T
jensandClaude Sonnet 5 827f03f087 Drive games from per-game JSON manifests instead of raw zip scanning
Games are no longer discovered from *.zip files on the SMB share; each
game now has its own <name>.json manifest (name/title/zip/start_cmd)
living next to its zip, discovered dynamically via smbclient. Nothing
about a game's identity or how to install/start it is hardcoded in the
image anymore. The setup page also grew Start/Stop/Uninstall buttons,
backed by real process tracking (Popen + terminate/kill).

Piloted on stuntcar only (manifest already uploaded to the share); t7g
has no manifest yet so it won't appear until one's added. The old
game.sh/start_game.sh docker-exec launch path is left as-is for now and
overlaps with the new Start button - noted in TODO.md for later cleanup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 10:27:33 +02:00

68 lines
8.1 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
A Docker image definition for headless DOSBox games exposed over noVNC (VNC-over-websockets in the browser). It builds on the public base image `ich777/novnc-baseimage`, which already provides TurboVNC, websockify, fluxbox, and noVNC — this repo adds `dosbox`/`alsa-utils` plus `unzip`/`smbclient`/`python3` for game management, a TLS cert for noVNC, and the entrypoint/lifecycle scripts. Published as `jayfield/dosbox-novnc` on Docker Hub.
Game data (DOS game installs, sourced as zips from an internal SMB share `//vlda-01/software/Games/dosbox/`) is deliberately **not** baked into the image — one of the two games (`t7g`, "The 7th Guest") is ~930MB of CD-ROM ISOs on its own, more than the rest of the image combined. Instead the image ships empty and games are installed at runtime into a volume, via an HTML setup page (see below).
## Commands
There is no build system, linter, or test suite — this is a Dockerfile plus a handful of shell scripts and a small Python HTTP server. Common workflows:
```bash
# Build the image
docker build -t jayfield/dosbox-novnc .
# Run it the way it's intended to be run (fixed name, volume, ports)
./run.sh
# Launch a specific installed game inside the running container
./game.sh <game-name> # e.g. ./game.sh stuntcar
```
There's no `create.sh` in this project (unlike the other noVNC-family projects) — `run.sh` is the only launch script, and it's `-d --rm` (long-lived-but-disposable), not a persistent named container created once via `docker create`.
There's no automated way to exercise `scripts/server.sh`, `scripts/setup_server.py`, or `scripts/start_game.sh` outside a running container — to test changes, rebuild the image, start a real container, and either watch `docker logs` or `docker exec` in.
## Architecture
**Image layering** (`Dockerfile`):
- Base image supplies TurboVNC (`Xvnc` on `$PATH`), websockify (`websockify` on `$PATH`), and noVNC's web client at `/usr/share/novnc/`.
- This layer adds `dosbox`/`alsa-utils` (the actual game runtime) and `unzip`/`smbclient`/`python3` (needed permanently at *runtime* now, not just at build time — see below), generates a self-signed TLS cert/key at build time (`/tmp/novnc.pem`, `/tmp/novnc.key`) for noVNC's HTTPS/WSS listener, and copies `scripts/` to `/opt/scripts/`.
- `${GAMES_HOME}` (`/opt/games`) is created and declared as a `VOLUME` — it's meant to be backed by a named volume at `docker run` time (`run.sh` uses `-v dosbox-games:/opt/games`) so installed games survive `--rm` container restarts instead of being re-fetched every time.
- `ENTRYPOINT` is `/opt/scripts/server_start.sh /opt/scripts/server.sh` — the signal-handling wrapper invoked with the real startup script as its argument.
**Runtime env vars** (set at `docker run` time, not baked into the image): `DISPLAY_NUM` (X display number, default 99 → `DISPLAY=:99`), `SCREEN_W`, `SCREEN_H` (framebuffer geometry).
**Port scheme**, all derived from `DISPLAY_NUM`:
- `59${DISPLAY_NUM}` — raw VNC (RFB), only bound to localhost, not published directly.
- `80${DISPLAY_NUM}` — noVNC over HTTPS/WSS. This is the port users connect to from a browser to see/play the game.
- `60${DISPLAY_NUM}``EXPOSE`d for consistency with the other noVNC-family projects, but nothing in `server.sh` actually bridges X11-over-TCP here (no `socat` call) — this project doesn't wire that up, unlike `docker-xserver-novnc`/`docker-sdr-novnc`.
- `70${DISPLAY_NUM}` — the game **setup UI** (`scripts/setup_server.py`), plain HTTP. `run.sh` publishes this as `7099:7099`.
`run.sh` hardcodes `DISPLAY_NUM=99` (implicitly, via the image default) and the `7099` port mapping; update it if `DISPLAY_NUM` ever changes. Note `run.sh` does *not* currently publish the `59xx`/`80xx` noVNC ports to the host — only `7099` (setup) is mapped, plus whatever the container's on the same Docker network can reach directly.
**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) started in the background by `server.sh` alongside Xvnc/fluxbox/websockify.
- 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 next to its zip on the SMB share (e.g. `Games\dosbox\stuntcar.json`), with `name`/`title`/`zip`/`start_cmd` fields — see the module docstring in `setup_server.py` for the exact schema. `discover_games()` lists `*.json` on the share (`smbclient -N ... -c 'ls Games\dosbox\*.json'`), `smbclient get`s each one into `/tmp/manifests/`, and parses it — so dropping a new `<name>.json` + `<name>.zip` pair on the share is enough to make a game appear, no image rebuild needed.
- `GET /` renders one row per discovered manifest with its live status (Not installed / downloading / extracting / Installed / Running / error) and the matching action buttons.
- `POST /install`: `smbget -au` (guest auth) the manifest's `zip` 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.
- `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`: `subprocess.Popen(manifest["start_cmd"], cwd=GAMES_HOME/<name>)`, tracked in an in-process dict keyed by game name (`running_procs`). This inherits `setup_server.py`'s own environment, including `$DISPLAY`, so the launched process renders onto the same Xvnc display fluxbox/noVNC are already serving — no separate X setup needed.
- `POST /stop`: `terminate()`s the tracked process, escalating to `kill()` after a 5s grace period.
- Per-game install progress (`install_state`) and the running-process table (`running_procs`) are both guarded by one `threading.RLock` (reentrant — several code paths call `is_running()` from inside a block that already holds the lock). The page does a `<meta http-equiv="refresh">` every 3s while any install is in flight; no client-side JS anywhere.
- **Only `stuntcar.json` exists on the share right now** — this whole manifest/start/stop/uninstall mechanism was rolled out as a pilot on that one (small, fast-to-test) game before being extended to `t7g`. Until `t7g.json` is added, `t7g` simply won't appear on the setup page even though its zip is still there.
**Playing a game — two overlapping paths right now**:
1. The setup page's Start/Stop buttons (above) — the current way, works for any manifest-driven game.
2. `game.sh <name>``docker exec -it dosbox-novnc start_game.sh <name>`, running *inside* the already-running container. `scripts/start_game.sh` hardcodes `DOSBOX_VERSION="0.74-3"` and the `run.bat`/`dosbox-<version>.conf` convention — this predates the manifest system and was deliberately left as-is during the pilot. The two paths don't know about each other (e.g. a game started via `game.sh` won't show as "Running" on the setup page). Worth reconciling once manifests cover every game — see `TODO.md`.
**Signal handling** (`scripts/server_start.sh` + `scripts/signals.sh`): identical pattern to the other noVNC-family projects — Docker sends `SIGTERM` to PID 1 on `docker stop`, `server_start.sh` traps it, forwards it to the child process tree, waits for the descendants to exit, and re-raises `128 + signal number` as its own exit code. Generic (`$APP` passed as an argument), not specific to `server.sh`.
## Sizing history
See `TODO.md` for the detailed before/after of the size-reduction work (2.31GB → 1.6GB → ~690MB) — the short version: merge build layers so intermediate files (downloaded zips, build-only packages) don't persist, then stop embedding the actual game data in the image at all and fetch it into a volume on demand instead.