/** * Running a command and waiting for a condition — the two things every step below needs. * * The waiting matters more than it looks. Every readiness check here polls for an OBSERVABLE FACT rather * than trusting a live pid, because each of these processes can be alive and not yet usable: Xvfb forks * before its socket answers, wine returns while the window is still being created, and x11vnc exits after * printing an error if the display went away. A pid check passes for all three and the next step then fails * in a way that reads as the game being broken. */ export type RunResult = { ok: boolean; stdout: string; stderr: string; code: number }; export async function run(cmd: string[], env: Record = {}, timeoutMs = 30_000): Promise { const proc = Bun.spawn(cmd, { env: { ...process.env, ...env }, stdout: 'pipe', stderr: 'pipe', stdin: 'ignore', }); const killer = setTimeout(() => proc.kill('SIGKILL'), timeoutMs); try { const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]); const code = await proc.exited; return { ok: code === 0, stdout, stderr, code }; } finally { clearTimeout(killer); } } /** Poll `check` until it is true. Resolves false on timeout rather than throwing — every caller reports. */ export async function waitFor(check: () => boolean | Promise, timeoutMs: number, everyMs = 250) { const deadline = Date.now() + timeoutMs; for (;;) { if (await check()) return true; if (Date.now() >= deadline) return false; await Bun.sleep(everyMs); } }