build the wine prefix after the display exists, not before

A cold install could never work. `winetricks -q vb6run` runs Microsoft's
redistributable, and that installer creates a window even when told to be
quiet — so with no DISPLAY wine cannot load a graphics driver, the installer
dies, and winetricks reports `returned status 43`. That message names neither
wine nor a display, and reads like a corrupt download.

ensurePrefix ran before Xvfb started, because building a prefix looks like
preparation rather than something that needs a screen.

It never showed up in testing here: this machine's prefix was seeded from the
pre-extraction one, so the build path only ever ran against a prefix that was
already complete. The first genuine cold install — alpha — failed on the first
Start. Diagnosed from `err:winediag:nodrv_CreateWindow` in the wine output,
which is the only line that says what is actually wrong.

Order is now display, prefix, game. ensurePrefix takes the display explicitly
rather than reading the ambient environment, so it cannot silently run without
one again, and a failed build now tears the display down instead of leaving an
Xvfb behind.

Verified by moving this machine's prefix aside and starting cold: prefix built
from nothing, msvbvm60.dll installed, session up with the window at 152x214,
Z: confined, socket mode 700, and RFB 003.008 through the platform relay.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-23 20:21:32 +00:00
co-authored by Claude Opus 5
parent 4fe9fa2e97
commit 41020b1b65
2 changed files with 24 additions and 9 deletions
+15 -3
View File
@@ -50,7 +50,19 @@ export type PrefixResult = { ok: boolean; detail: string };
* `winetricks -q vb6run` fetches Microsoft's own `VB6.0-KB290887-X86.exe` and verifies its checksum before * `winetricks -q vb6run` fetches Microsoft's own `VB6.0-KB290887-X86.exe` and verifies its checksum before
* unpacking, which is why this does not carry a redistributable of its own. * unpacking, which is why this does not carry a redistributable of its own.
*/ */
export async function ensurePrefix(log: (line: string) => void): Promise<PrefixResult> { export async function ensurePrefix(log: (line: string) => void, display: string): Promise<PrefixResult> {
// THE DISPLAY IS NOT OPTIONAL, and this cost a cold install to learn.
//
// `winetricks -q vb6run` runs Microsoft's redistributable, and that installer creates a window even
// when told to be quiet. With no `DISPLAY` wine cannot load a graphics driver, the installer dies, and
// winetricks reports `returned status 43` — which names neither wine nor a display and reads like a
// corrupt download.
//
// This used to run BEFORE Xvfb started, because building a prefix looks like preparation rather than
// something that needs a screen. It never showed up here: this machine's prefix was seeded from an
// older one, so the build path only ever ran on a prefix that was already complete. The first genuine
// cold install — alpha — failed immediately.
const env = { ...WINE_ENV, DISPLAY: display };
// Before the early return as well: an existing prefix may predate this, or have had `z:` restored by a // Before the early return as well: an existing prefix may predate this, or have had `z:` restored by a
// wine upgrade. It is a few `unlink` calls, so it is cheaper to re-assert than to detect. // wine upgrade. It is a few `unlink` calls, so it is cheaper to re-assert than to detect.
if (prefixReady()) { if (prefixReady()) {
@@ -69,14 +81,14 @@ export async function ensurePrefix(log: (line: string) => void): Promise<PrefixR
log('creating the 32-bit wine prefix (first run only, this takes a minute)'); log('creating the 32-bit wine prefix (first run only, this takes a minute)');
// `wineboot -i` rather than letting the first wine call create it implicitly: implicit creation happens // `wineboot -i` rather than letting the first wine call create it implicitly: implicit creation happens
// inside the game launch, races the window-detection poll, and looks like the game failing to start. // inside the game launch, races the window-detection poll, and looks like the game failing to start.
const boot = await run(['wineboot', '-i'], WINE_ENV, 300_000); const boot = await run(['wineboot', '-i'], env, 300_000);
if (!boot.ok && !existsSync(WINEPREFIX)) { if (!boot.ok && !existsSync(WINEPREFIX)) {
return { ok: false, detail: `wineboot failed: ${(boot.stderr || boot.stdout).trim().slice(-400)}` }; return { ok: false, detail: `wineboot failed: ${(boot.stderr || boot.stdout).trim().slice(-400)}` };
} }
} }
log('installing the Visual Basic 6 runtime into the prefix (downloads from Microsoft)'); log('installing the Visual Basic 6 runtime into the prefix (downloads from Microsoft)');
const tricks = await run(['winetricks', '-q', 'vb6run'], WINE_ENV, 600_000); const tricks = await run(['winetricks', '-q', 'vb6run'], env, 600_000);
// winetricks exits 0 having done nothing often enough that the exit code is not the answer. The file is. // winetricks exits 0 having done nothing often enough that the exit code is not the answer. The file is.
if (!prefixReady()) { if (!prefixReady()) {
+9 -6
View File
@@ -131,12 +131,6 @@ async function startInner(log: (line: string) => void): Promise<SessionStatus> {
return status(); return status();
} }
const built = await ensurePrefix(log);
if (!built.ok) {
lastError = built.detail;
return status();
}
// ── the display ── // ── the display ──
if (!alive(xvfb)) { if (!alive(xvfb)) {
display = pickDisplay(); display = pickDisplay();
@@ -156,6 +150,15 @@ async function startInner(log: (line: string) => void): Promise<SessionStatus> {
} }
} }
// The prefix is built HERE, after the display exists and before the game needs it. Not earlier: the VB6
// installer opens a window, so a prefix built without a display fails in a way that names neither.
const built = await ensurePrefix(log, display!);
if (!built.ok) {
lastError = built.detail;
await stop();
return status();
}
const env = { ...WINE_ENV, DISPLAY: display! }; const env = { ...WINE_ENV, DISPLAY: display! };
// ── the game ── // ── the game ──