Internals: patch & headless
How a 2002 Windows game runs headless under Wine with no xvfb, why the modern runtime DLLs are needed, and how crashes are captured.
Running a GUI game with no display
Tribes 2’s Tribes2.exe is a GUI subsystem PE. Even a dedicated server normally expects a
window/display. Two things make it run truly headless:
- PE subsystem patch (GUI → console). A build-time Python patcher
(
content/tribes_dual_patcher.py) flips the PE header’s subsystem from GUI (2) to console/CUI (3) forTribes2.exe. This makes the process a console app so it has real standard streams. It’s a one-field PE edit (verified by re-reading the header), with a backup. - A pseudo-terminal (PTY). The dedicated-server code path polls console input
(
GetNumberOfConsoleInputEvents/ReadConsoleInput). With no real console that path reads uninitialized state and faults at mission start. Instead of removing it, the supervisor gives the game a real TTY: it launches the game through a tiny embedded Python PTY bridge, so console input/output behave correctly.
The result: no xvfb, no virtual X server, no telnet — the game just runs on a pseudo-terminal.
The PTY bridge
GameSupervisor writes a small Python bridge to a temp path and launches:
python3 <bridge> [taskset -c <affinity>] wine <Tribes2.exe> <params...>
The bridge openpty()s, forks, makes the child a session leader with the slave as its
controlling terminal, and dup2s the slave to stdin/stdout/stderr before exec’ing the game.
The parent proxies bytes between the PTY master and its own stdio (stripping terminal escape
sequences). The supervisor then:
- reads the console feed from the bridge’s stdout → publishes to
ConsoleHub→ SSE clients; - writes commands (including
quit();) to the bridge’s stdin → the game’s console input.
This is also why the root terminal works (a separate bridge runs bash instead of the game,
without escape-stripping). See Services/TerminalSession.cs.
Old + new Windows runtimes side by side
The game itself is a 2002 MSVC6-era binary; the QoL patch needs newer 32-bit Windows APIs. Both must be satisfied in the Wine prefix:
- Old VC++6 runtime — comes from the game’s own bundled
MSVCRT.dllplus aTribes2.exe.localredirection (so Wine loads the local copy). - New VC++ 2022 runtime — real Microsoft DLLs (
msvcp140,vcruntime140,concrt140, …) fetched at build time from Microsoft’s official redistributable (VCREDIST_URL,vc_redist.x86.exe). It’s a WiX “Burn” bundle, socabextractpulls its payload cabs (the DLLs are stored as*.dll_x86); the needed set is renamed to.dlland dropped into the Winesystem32, withWINEDLLOVERRIDESset to prefer the native copies. Nothing is committed to the repo. See Building & deploying → licensing.
No winetricks and no Ruby are involved — the 2025 QoL feature set is native code in
IFC22.dll (≈2 MB), overlaid from the patch.
Wine version pin
Wine is pinned to 10.0.0.0 (WINE_VERSION). Wine 11 regresses the Tribes 2 mission-start
path. The pin selects matching winehq-stable / wine-stable* packages from the WineHQ repo for
the image’s distro/codename.
Build pipeline (Dockerfile stages)
- spa-build (
node) — builds the React SPA →/wwwroot. - app-build (
dotnet/sdk) — restores + publishes the panel (framework-dependent) and copies in the SPA. - rewise-build (
debian) — compiles a tiny static REWise binary (pinned commit) used to extractGameDatafrom the GSI installer. - runtime (
dotnet/aspnet, Ubuntu) — i386 multiarch + WineHQ + Wine;winebootheadless; fetch + unpack the official VC++ runtime DLLs; download the public GSI installer and extract the game from it with REWise (no install run); overlay the QoL patch; run the PE patcher; copy in the published panel. Entrypoint = the panel undertini.
Crash tracking
When the game exits unexpectedly (i.e. not from an operator action — graceful quit, force-restart, stop, or panel shutdown all mark the exit as expected), the supervisor records a crash report:
- server start + crash timestamps and the process exit code;
- if the console tail contains an access violation, the parsed module,
0xfault address, and faulting instruction; - the console tail plus
CRASHLOG.TXTfrom the game dir, for context.
These appear on the read-only Crashes page (Admin+) and are meant to be handed upstream so
the image can be patched. By default the game then auto-restarts (RESTART_ON_CRASH).