diff --git a/Dockerfile b/Dockerfile index 2ebb264..eb2ae77 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,10 +7,11 @@ ENV GAMES_HOME=/opt/games ENV SCRIPTS_HOME=/opt/scripts ENV PATH=${PATH}:${SCRIPTS_HOME} -# unzip/smbclient are needed at runtime now too: the games are no longer baked into the -# image, fetch_games.sh pulls them into the ${GAMES_HOME} volume on first container start. +# unzip/smbclient/python3 are needed at runtime: the games are no longer baked into the +# image, setup_server.py serves an HTML install UI (backed by smbget+unzip) that the user +# drives on ${SETUP_PORT} instead of anything being auto-downloaded at container start. RUN apt-get update && \ - apt-get -y install --no-install-recommends dosbox alsa-utils unzip smbclient && \ + apt-get -y install --no-install-recommends dosbox alsa-utils unzip smbclient python3 && \ rm -rf /var/lib/apt/lists/* RUN openssl req -x509 -nodes -newkey rsa:2048 -keyout /tmp/novnc.key -out /tmp/novnc.pem -days 3650 -subj "/C=US/ST=NY/L=NY/O=NY/OU=NY/CN=NY emailAddress=email@example.com" @@ -23,6 +24,7 @@ ADD scripts ${SCRIPTS_HOME} WORKDIR / EXPOSE 60${DISPLAY_NUM} +EXPOSE 70${DISPLAY_NUM} EXPOSE 5900 ENTRYPOINT ["/opt/scripts/server_start.sh", "/opt/scripts/server.sh"] diff --git a/TODO.md b/TODO.md index 3b6ff0f..594b2fa 100644 --- a/TODO.md +++ b/TODO.md @@ -5,8 +5,11 @@ layer (so the downloaded zips and the packages only needed to extract them no longer persist in the final image). 2.31GB -> 1.6GB. 2. Stopped baking the games into the image at all (927MB of that was just t7g's two CD - ISOs). `scripts/fetch_games.sh` now downloads them from the SMB share into - `${GAMES_HOME}` (`/opt/games`) on first container start instead, skipping the fetch if - already present. `run.sh` mounts a named volume (`dosbox-games`) so the games persist + ISOs). `run.sh` mounts a named volume (`dosbox-games`) so installed games persist across `--rm` restarts. 1.6GB -> ~690MB. Container now needs network access to - `vlda-01` at runtime (not just build time) for the first start. + `vlda-01` at runtime (not just build time) to install games. + 3. Dropped the auto-download at container startup entirely. `scripts/setup_server.py` is + a small stdlib-only HTTP server (started alongside Xvnc/fluxbox/websockify) that serves + an HTML page on `${SETUP_PORT}` (`70${DISPLAY_NUM}`, published as `7099` by `run.sh`) + listing every `*.zip` on the SMB share with an Installed/Install status per game; the + user clicks "Install" to fetch+extract a specific game into `${GAMES_HOME}` on demand. diff --git a/run.sh b/run.sh index 7b91fba..60615b2 100755 --- a/run.sh +++ b/run.sh @@ -1,4 +1,4 @@ #/bin/bash -docker run --name dosbox-novnc -d --rm --net bridge -e SCREEN_W=1536 -e SCREEN_H=960 --device /dev/snd -v dosbox-games:/opt/games jayfield/dosbox-novnc +docker run --name dosbox-novnc -d --rm --net bridge -e SCREEN_W=1536 -e SCREEN_H=960 --device /dev/snd -v dosbox-games:/opt/games -p 7099:7099 jayfield/dosbox-novnc diff --git a/scripts/fetch_games.sh b/scripts/fetch_games.sh deleted file mode 100755 index ca44bc7..0000000 --- a/scripts/fetch_games.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -e - -SMB_BASE="smb://vlda-01/software/Games/dosbox" -GAME_ZIPS="stuntcar.zip t7g.zip" - -if [ -n "$(ls -A "${GAMES_HOME}" 2>/dev/null)" ]; then - echo "Games already present in ${GAMES_HOME}, skipping fetch" - exit 0 -fi - -echo "Fetching games into ${GAMES_HOME}..." -TMPDIR=$(mktemp -d) -trap 'rm -rf "${TMPDIR}"' EXIT - -cd "${TMPDIR}" -for zip in ${GAME_ZIPS}; do - smbget -au "${SMB_BASE}/${zip}" -done -unzip -o -d "${GAMES_HOME}" "*.zip" diff --git a/scripts/server.sh b/scripts/server.sh index 177e920..3199540 100755 --- a/scripts/server.sh +++ b/scripts/server.sh @@ -3,8 +3,9 @@ set -x RFB_PORT=59${DISPLAY_NUM} NOVNC_PORT=80${DISPLAY_NUM} +SETUP_PORT=70${DISPLAY_NUM} -${SCRIPTS_HOME}/fetch_games.sh +SETUP_PORT=${SETUP_PORT} ${SCRIPTS_HOME}/setup_server.py & echo Display at ${DISPLAY} with ${SCREEN_W}x${SCREEN_H}x24 Xvnc ${DISPLAY} -geometry ${SCREEN_W}x${SCREEN_H} -depth 24 +xinerama -securitytypes none >/var/log/xvfb.log & diff --git a/scripts/setup_server.py b/scripts/setup_server.py new file mode 100755 index 0000000..7b10587 --- /dev/null +++ b/scripts/setup_server.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""HTML setup routine: lets the user browse games available on the SMB +share and install them into GAMES_HOME on demand, instead of the image +auto-downloading everything at container startup.""" + +import html +import os +import re +import subprocess +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import parse_qs, urlparse + +SMB_SERVER = "vlda-01" +SMB_SHARE = "software" +SMB_DIR = r"Games\dosbox" +GAMES_HOME = os.environ["GAMES_HOME"] +SETUP_PORT = int(os.environ.get("SETUP_PORT", "8080")) + +state_lock = threading.Lock() +install_state = {} # name -> "downloading" | "extracting" | "done" | "error: ..." + + +def list_remote_games(): + result = subprocess.run( + ["smbclient", "-N", f"//{SMB_SERVER}/{SMB_SHARE}", "-c", f"ls {SMB_DIR}\\*.zip"], + capture_output=True, text=True, timeout=15, + ) + games = [] + for line in result.stdout.splitlines(): + m = re.match(r"\s*(\S+)\.zip\s+A\s+(\d+)", line) + if m: + games.append((m.group(1), int(m.group(2)))) + return sorted(games) + + +def list_installed_games(): + if not os.path.isdir(GAMES_HOME): + return set() + return { + name for name in os.listdir(GAMES_HOME) + if os.path.isdir(os.path.join(GAMES_HOME, name)) and os.listdir(os.path.join(GAMES_HOME, name)) + } + + +def install_game(name): + with state_lock: + if install_state.get(name) in ("downloading", "extracting"): + return + install_state[name] = "downloading" + + tmpdir = f"/tmp/setup-{name}" + try: + os.makedirs(tmpdir, exist_ok=True) + zip_path = os.path.join(tmpdir, f"{name}.zip") + # smbget's -a (guest) can't be combined with -o (output file), so let it save + # under the default name in tmpdir instead, same as the original build-time fetch. + subprocess.run( + ["smbget", "-au", f"smb://{SMB_SERVER}/{SMB_SHARE}/Games/dosbox/{name}.zip"], + check=True, timeout=1800, cwd=tmpdir, + ) + with state_lock: + install_state[name] = "extracting" + os.makedirs(GAMES_HOME, exist_ok=True) + subprocess.run(["unzip", "-o", "-d", GAMES_HOME, zip_path], check=True, timeout=600) + with state_lock: + install_state[name] = "done" + except Exception as exc: + with state_lock: + install_state[name] = f"error: {exc}" + finally: + subprocess.run(["rm", "-rf", tmpdir], check=False) + + +def render_page(): + remote = list_remote_games() + installed = list_installed_games() + with state_lock: + pending = dict(install_state) + + any_pending = any(v in ("downloading", "extracting") for v in pending.values()) + refresh_tag = '' if any_pending else "" + + rows = [] + for name, size in remote: + status = pending.get(name) + if name in installed and status not in ("downloading", "extracting"): + action = 'Installed' + elif status in ("downloading", "extracting"): + action = f'{status}...' + elif status and status.startswith("error"): + action = ( + f'{html.escape(status)}' + f'
' + ) + else: + action = ( + f'' + ) + rows.append( + f"| Game | Size | Status / Action |
|---|---|---|
| No games found on the share | ||