diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..42d66a0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,61 @@ +# 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 # 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** (`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. +- `GET /` renders an HTML page listing every `*.zip` found live on the SMB share (via `smbclient -N ... -c 'ls Games\dosbox\*.zip'`, parsed with a regex — not hardcoded, so new zips dropped on the share show up automatically) alongside each game's install status, sourced by checking for a non-empty `${GAMES_HOME}/` directory. +- `POST /install` (form-submitted, `name=`) validates the name against the live SMB listing, then kicks off `install_game()` in a background daemon thread: `smbget -au` (guest auth) into a `/tmp/setup-` scratch dir, then `unzip -o -d ${GAMES_HOME}`. Per-game state (`downloading`/`extracting`/`done`/`error: ...`) lives in an in-process dict guarded by a lock; the page does a `` every 3s while anything is in flight, no client-side JS. +- `smbget -a` (guest) cannot be combined with `-o` (output-file) — they conflict on the underlying `-U` option — so the download step `cd`s into the scratch dir and lets `smbget` save under its default filename instead. +- Games are recognized purely by directory name under `${GAMES_HOME}`; there's no separate manifest. A zip is expected to contain a single top-level folder matching its own basename (this holds for both `stuntcar.zip`→`stuntcar/` and `t7g.zip`→`t7g/`). + +**Playing a game**: `game.sh ` is `docker exec -it dosbox-novnc start_game.sh ` — it runs *inside* the already-running container (started via `run.sh`), attaching to the same `$DISPLAY` that Xvnc/fluxbox/noVNC are already serving. `scripts/start_game.sh` just runs `dosbox ${GAMES_HOME}//run.bat -conf ${GAMES_HOME}//dosbox-0.74-3.conf` — no signal handling of its own, since it's not PID 1 and Docker doesn't manage its lifecycle directly. + +**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.