── the stage ── The framebuffer is now the picture. 800x600 whatever the difficulty, the game centred in it by the sidecar, and the panel draws it entire — scaled to fit, centred by its flex parent, never cropped. Changing difficulty moves the board instead of resizing the panel. What this replaces parked the window at the origin and had the client crop to the window's size, polled every four seconds. Two things wrong with it. The panel resized under the player mid-game; and wine puts the window at 3,29 rather than 0,0, so cropping from the origin showed a black strip on top and cut a row of cells off the bottom — the "expert gets cut off" report. Centring runs on a one-second timer because nothing announces a resize: not RFB, which keeps the framebuffer, and not X without a window manager to listen through. One chained `xdotool search … getwindowgeometry` per tick, one process, and a `windowmove` only when the numbers actually changed. The client loses the geometry poll, the offscreen canvas and the crop with it. ── the port ── x11vnc was listening on 0.0.0.0:5900 and [::]:5900 with -nopw. Every account on this machine, every device on the tailnet and everything on the LAN could open it and take the display — which is precisely the hole the 5907 version was replaced to close, reopened wider and documented as impossible. `-unixsock` ADDS a unix socket. It does not remove the TCP one. Three flags do: -rfbport 0 for IPv4, -rfbportv6 0 for IPv6, -noipv6 for outgoing. `-rfbport 0` alone leaves [::]:5900 up, which is how this nearly shipped half-fixed. Found with `ss -lntp` on the running process, and that is the only check that was right about it at any point. Running the same argv by hand showed no IPv6 listener — most likely because the PM2 session still held [::]:5900 and the hand-run copy failed to bind it silently. Comments in session.ts and index.ts that asserted there was no port are corrected rather than deleted. Verified on the live socket: the streamed framebuffer is 447k teal pixels around a 152x214 window at 324,193 — dead centre — and window grey arrives as c0 c0 c0, no TCP listener for the x11vnc pid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
120 lines
6.6 KiB
Markdown
120 lines
6.6 KiB
Markdown
# Minesweeper
|
|
|
|
Minesweeper X, running under wine on a headless X display, streamed into an Officer panel.
|
|
|
|
It is a game. It is also the smallest honest proof that Officer can put an arbitrary X11 program in a
|
|
panel — a framebuffer, a process on it, a VNC server reading it, a client in the browser — and it is the
|
|
first plugin to use the platform's per-plugin websocket relay.
|
|
|
|
## What it needs
|
|
|
|
Six host binaries, declared in `manifest.ts` and checked before install: `wine`, `Xvfb`, `x11vnc`,
|
|
`xdotool`, `xdpyinfo`, `winetricks`.
|
|
|
|
The awkward one is wine. Minesweeper X is a **32-bit** PE (machine `0x014c`, Visual Basic 6), so the host
|
|
needs i386 multiarch and a `win32` prefix. A 64-bit-only wine cannot load it, and the failure is a bare
|
|
`Bad EXE format`. On Debian or Ubuntu:
|
|
|
|
```
|
|
sudo dpkg --add-architecture i386
|
|
sudo apt update
|
|
sudo apt install wine wine32:i386 xvfb x11vnc x11-utils xdotool winetricks
|
|
```
|
|
|
|
## The game
|
|
|
|
[Minesweeper X](https://minesweepergame.com/download/minesweeper-x.zip) — the competition build, with
|
|
timing and replay extras. `game/Minesweeper X.exe` is committed; it is 68 KB and it is the thing being
|
|
run rather than state produced by running it.
|
|
|
|
| | |
|
|
| --- | --- |
|
|
| zip sha256 | `21089193924919ef876a5b464f06bed9cc149867c51e40c259288ebe623014f0` |
|
|
| exe sha256 | `e414c207adaa14999b0464c3d0a701f20ed11282d5ed0f84b27b62d25389f661` |
|
|
|
|
The download needs a browser `User-Agent`; the site answers `403` to curl's default.
|
|
|
|
## Where its state lives
|
|
|
|
`DATA_PATH/minesweeper/` — never in this directory, which `plugins:update` will `git pull` over.
|
|
|
|
- `prefix/` — the wine prefix, ~600 MB, built on first `POST /start`. Deleting it is the standard fix for
|
|
a wedged wine, and it is safe: the next start rebuilds it.
|
|
- `run/` — pid state, logs, and `vnc.sock`. **Mode 0700, and that is load-bearing.**
|
|
|
|
## Two things worth knowing before you change anything
|
|
|
|
**x11vnc has no password, and that is correct — but only because it also has no port.** It binds a unix
|
|
socket inside `run/`, mode 0700. The kernel refuses every other account before x11vnc is asked anything,
|
|
so a password would be a second lock on a door only one person can reach. The version this replaced bound
|
|
`127.0.0.1:5907` with `-nopw`, which is a different claim entirely — every local account could open it and
|
|
take the display.
|
|
|
|
**`-unixsock` does not remove the TCP listener, and the first alpha shipped believing it did.** It adds a
|
|
unix socket beside the port; x11vnc was on `0.0.0.0:5900` and `[::]:5900` with no password, reachable from
|
|
the whole tailnet and the whole LAN, under a comment saying there was no port at all. Three flags close it
|
|
and each one earns its place: `-rfbport 0` for IPv4, `-rfbportv6 0` for IPv6 — `-rfbport 0` alone leaves
|
|
`[::]:5900` up — and `-noipv6` for outgoing sockets.
|
|
|
|
The check that matters is `ss -lntp` against the **running** process. Reading the arguments cannot show a
|
|
listener that exists because a flag is missing, and running the same arguments by hand did not reproduce
|
|
it either.
|
|
|
|
**The game is centred on a fixed stage, and the whole stage is what you see.** The framebuffer is 800x600
|
|
whatever the difficulty, `sidecar/session.ts` keeps the window in the middle of it, and the panel draws the
|
|
framebuffer entire — scaled to fit, never cropped. So changing difficulty moves the board rather than
|
|
resizing the panel, and the surround is teal (`-solid`) rather than the root's black.
|
|
|
|
It used to park the window at the origin and have the client crop to the window's size, polled every four
|
|
seconds. That resized the panel under the player, and it was off by the 3x29 wine puts between the root
|
|
origin and the window — a black strip along the top and a row of cut-off cells along the bottom, worst on
|
|
expert. `GET /status` still reports the window geometry; nothing consumes it now, it is a diagnostic.
|
|
|
|
**The wine prefix is confined to `C:` on every start.** A fresh prefix maps the whole filesystem to `Z:`
|
|
and symlinks the profile into the real home, and Minesweeper X has Open and Save dialogs for its replays.
|
|
That is why this was an ungrantable `execution` capability in the platform. `sidecar/prefix.ts` closes it
|
|
instead, which is what makes the plugin's permission safe to grant. Wine restores `z:` whenever it updates
|
|
a prefix, so the confinement is re-asserted rather than done once.
|
|
|
|
## Contract
|
|
|
|
```
|
|
GET /api/minesweeper/status { installed, running, window, error? }
|
|
POST /api/minesweeper/start idempotent; builds the prefix on a cold install (minutes)
|
|
POST /api/minesweeper/stop
|
|
/api/plugin-ws/minesweeper the display, as RFB
|
|
```
|
|
|
|
## Where this came from
|
|
|
|
Built first as a standalone directory, `officer-dev/wine-minesweeper`, and folded in here when it became a
|
|
plugin. Two things from that work are worth keeping, because both are judgement calls somebody will
|
|
otherwise make again from scratch.
|
|
|
|
**Wine came from Ubuntu noble, not the WineHQ repository.** The distribution package is older — 9.0 — and
|
|
that is fine for a 2017 VB6 application. The alternative was adding a third-party apt source, with its key
|
|
and its update cadence, to a machine that runs a bitcoin wallet and a mail store, for a game of Minesweeper.
|
|
If a future version genuinely needs newer wine, that trade is worth revisiting; until then it is not.
|
|
|
|
**Enabling i386 pulled four krb5 libraries forward** within noble-updates. That was checked before
|
|
installing rather than after: nothing moved near libc, systemd, openssl or postgres. Worth re-checking on
|
|
any host where `dpkg --add-architecture i386` is new, since it is the one step here that changes how the
|
|
whole machine resolves packages and is never undone by uninstalling the plugin.
|
|
|
|
### The original verification baseline
|
|
|
|
From the standalone build, on Ubuntu noble with wine 9.0. Useful as a comparison when something feels wrong
|
|
rather than as a test to run:
|
|
|
|
- The game launches in about a second and draws the classic beginner board at **152x214**.
|
|
- A synthetic `xdotool` click opens a region and starts the timer — so **input reaches the game**, which is
|
|
the part that matters for a playable stream and the part a screenshot cannot tell you.
|
|
- x11vnc completes an **RFB 003.008** handshake and reports an 800x600 32bpp framebuffer.
|
|
- Cold start, display through to game window, in under two seconds. That excludes building the prefix,
|
|
which happens once and takes minutes.
|
|
|
|
The shell prototype that ran all this (`bin/session.sh`) is not kept. Everything it knew — waiting for a
|
|
usable display rather than a live pid, parking the window at the origin, and clearing the whole wine
|
|
process family on stop — is in `sidecar/session.ts`, with the reasoning attached to the code that depends
|
|
on it.
|