import { existsSync, mkdirSync, readdirSync, statSync } from 'node:fs'; import { join } from 'node:path'; import type { VncStartParams, VncSessionInfo } from './protocol'; import { getOwnerHomeDir } from '@@/data-path'; // Mirrors the physical display instead of spawning a virtual desktop per user, so the // browser shows the same session as the screen. There is one seat, hence one shared // mirror — the first caller's VNC password is the one it answers with. const MIRROR_PORT = 5900; const READY_TIMEOUT_MS = 5000; // Above this framebuffer width the stream is halved; at or below it, pixels are served 1:1. const SCALE_ABOVE_WIDTH = 2560; // The display number is NOT fixed. Under LightDM the user session got :0, so this was hardcoded to it. // GDM gives :0 to its own greeter and starts the user's Xorg with -displayfd, letting the kernel pick — // in practice :1. Hardcoding :0 made the mirror fail with "Can't open display" on every GNOME machine. // // Find it by socket ownership instead: /tmp/.X11-unix/X is owned by whoever runs that X server, so // the one owned by us is the owner's session and any other is the greeter's. Fall back to the lowest // socket (covers a root-run Xorg, as LightDM did) and finally to :0 so behaviour never gets worse than // the constant it replaced. function resolveDisplayNum(): number { const uid = typeof process.getuid === 'function' ? process.getuid() : null; let sockets: number[] = []; try { sockets = readdirSync('/tmp/.X11-unix') .filter((n) => /^X\d+$/.test(n)) .map((n) => Number(n.slice(1))) .sort((a, b) => a - b); } catch { return 0; } if (uid != null) { const own = sockets.find((n) => { try { return statSync(`/tmp/.X11-unix/X${n}`).uid === uid; } catch { return false; } }); if (own !== undefined) return own; } return sockets[0] ?? 0; } // x11vnc reads the display's cookie from the logged-in user's X authority, so no root is needed. Where // that cookie lives depends on the display manager: GDM (Ubuntu GNOME on Xorg) keeps it in the // per-session dir /run/user//gdm/Xauthority; LightDM (or a manual startx) uses the classic // ~/.Xauthority. Try the GDM path first, then fall back. While the greeter owns the seat the user's // cookie doesn't exist yet and mirroring fails until someone is logged in. function resolveXauthority(): string | null { const uid = typeof process.getuid === 'function' ? process.getuid() : null; const candidates = [ uid != null ? `/run/user/${uid}/gdm/Xauthority` : null, join(process.env.HOME ?? '', '.Xauthority'), ].filter((p): p is string => Boolean(p)); return candidates.find((p) => existsSync(p)) ?? null; } type MirrorSession = { email: string; username: string; display: number; port: number; pid: number; }; let mirror: MirrorSession | null = null; function isProcessAlive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch { return false; } } // Provision the pair of files a VNC session needs: `password` in plaintext, which the API hands to // the browser, and `passwd` in rfbauth format, which x11vnc authenticates against. Idempotent — // returns the existing password when both are already present. // // x11vnc writes the rfbauth file itself via -storepasswd. This used to shell out to tigervnc's // `vncpasswd -f`, which is not installed by default (and is not in tigervnc-common, despite what // setup-desktop.sh claimed), so the rfbauth file silently never appeared — the failure was swallowed // because only a zero exit code triggered the write. export async function ensureVncPassword(homeDir: string): Promise<{ password: string; passwdFile: string }> { const vncDir = join(homeDir, '.vnc'); const passwdFile = join(vncDir, 'passwd'); const plainFile = join(vncDir, 'password'); if (existsSync(passwdFile) && existsSync(plainFile)) { const password = (await Bun.file(plainFile).text()).trim(); if (password) return { password, passwdFile }; } mkdirSync(vncDir, { recursive: true }); // 8 printable ASCII characters. VNC truncates to 8, so there is no point generating more. const password = Array.from(crypto.getRandomValues(new Uint8Array(8))) .map((b) => String.fromCharCode(33 + (b % 94))) .join(''); await Bun.write(plainFile, password); const proc = Bun.spawnSync({ cmd: ['x11vnc', '-storepasswd', password, passwdFile], stdout: 'ignore', stderr: 'pipe', }); if (proc.exitCode !== 0 || !existsSync(passwdFile)) { const stderr = proc.stderr.toString().trim(); throw new Error(`Could not write the VNC password file: ${stderr || 'x11vnc -storepasswd failed'}`); } Bun.spawnSync({ cmd: ['chmod', '600', passwdFile], stdout: 'ignore', stderr: 'ignore' }); Bun.spawnSync({ cmd: ['chmod', '600', plainFile], stdout: 'ignore', stderr: 'ignore' }); console.log(`[vnc] provisioned VNC password at ${vncDir}`); return { password, passwdFile }; } type Geometry = { w: number; h: number; x: number; y: number }; type DisplayGeometry = { framebufferWidth: number | null; primary: Geometry | null; connected: number }; // X composes every attached output into ONE framebuffer, so with two monitors side by side the // framebuffer is their combined width and mirroring it whole sends both screens squashed together. // Read the layout so the caller can clip to a single output. // // Returns nulls when xrandr is missing or X is not up; callers treat that as "don't scale, don't clip", // because serving too many pixels beats serving nothing. function readDisplayGeometry(xauthority: string, display: string): DisplayGeometry { const proc = Bun.spawnSync({ cmd: ['xrandr', '--current'], env: { ...process.env, DISPLAY: display, XAUTHORITY: xauthority }, stdout: 'pipe', stderr: 'ignore', }); if (proc.exitCode !== 0) return { framebufferWidth: null, primary: null, connected: 0 }; const out = proc.stdout.toString(); const fb = out.match(/current\s+(\d+)\s*x\s*(\d+)/); // Count ENABLED outputs, not merely connected ones: a screen that is plugged in but switched off // still reports "connected" and contributes nothing to the framebuffer. Only an enabled output // carries a WxH+X+Y geometry, so requiring that is what distinguishes the two. const connected = (out.match(/^\S+ connected(?: primary)? \d+x\d+\+\d+\+\d+/gm) ?? []).length; // "HDMI-A-0 connected primary 3840x2160+0+0 (normal left ..." — the geometry only appears on an // output that is actually enabled, so a connected-but-off output correctly yields no match. const p = out.match(/^\S+ connected primary (\d+)x(\d+)\+(\d+)\+(\d+)/m); const primary = p ? { w: Number(p[1]), h: Number(p[2]), x: Number(p[3]), y: Number(p[4]) } : null; return { framebufferWidth: fb ? Number(fb[1]) : null, primary, connected }; } async function isPortOpen(port: number): Promise { try { const socket = await Bun.connect({ hostname: '127.0.0.1', port, socket: { data() {}, error() {} }, }); socket.end(); return true; } catch { return false; } } // A listener on the port is NOT proof our server started. The running mirror lives in module state, so // a sidecar restart forgets it while the process keeps running orphaned — and then this check sees the // ORPHAN listening and reports success, leaving the platform convinced it started something the browser // is not looking at. That is not hypothetical: on 2026-08-01 an Xvnc left over from the virtual-desktop // experiment held 5900, and the mirror reported healthy while the browser was served a stale XFCE // session. So reclaim the port first, and treat our own process dying as failure. async function reclaimPort(port: number): Promise { if (!(await isPortOpen(port))) return; console.log(`[vnc] port ${port} already held — killing the orphan before starting`); Bun.spawnSync({ cmd: ['fuser', '-k', '-TERM', `${port}/tcp`], stdout: 'ignore', stderr: 'ignore' }); for (let i = 0; i < 20; i++) { await Bun.sleep(100); if (!(await isPortOpen(port))) return; } Bun.spawnSync({ cmd: ['fuser', '-k', '-KILL', `${port}/tcp`], stdout: 'ignore', stderr: 'ignore' }); await Bun.sleep(300); } // x11vnc stays in the foreground, so readiness is the listening port rather than exit code — but only // once we know the process we spawned is the one alive to own it. async function waitForPort(port: number, proc: { exitCode: number | null }): Promise { const deadline = Date.now() + READY_TIMEOUT_MS; while (Date.now() < deadline) { if (proc.exitCode !== null) return false; if (await isPortOpen(port)) return true; await Bun.sleep(100); } return false; } export async function startSession(params: VncStartParams): Promise<{ port: number; display: number }> { if (mirror && isProcessAlive(mirror.pid)) { return { port: mirror.port, display: mirror.display }; } mirror = null; const homeDir = getOwnerHomeDir(params.email); const { passwdFile } = await ensureVncPassword(homeDir); // An orphan from a previous sidecar life would still hold the port and be mistaken for a healthy start. await reclaimPort(MIRROR_PORT); const displayNum = resolveDisplayNum(); const display = `:${displayNum}`; const xauthority = resolveXauthority(); if (!xauthority) { throw new Error(`No X authority found (GDM or ~/.Xauthority) — nobody is logged in on ${display}`); } // With more than one output attached the framebuffer spans them all, and mirroring it whole shows // every monitor side by side, squashed. Clip to the primary so the remote view is one screen at the // right proportions. Only when a primary is actually marked and there is more than one output — // otherwise the framebuffer already IS the single screen and clipping would just add a failure mode. const geo = readDisplayGeometry(xauthority, display); const clipTo = geo.connected > 1 && geo.primary ? geo.primary : null; const clip = clipTo ? ['-clip', `${clipTo.w}x${clipTo.h}+${clipTo.x}+${clipTo.y}`] : []; if (clipTo) { console.log(`[vnc] ${geo.connected} outputs attached — clipping to the primary ${clip[1]}`); } // Halving a 4K screen keeps the stream sane over the tailnet, but the same 0.5 applied to a small // screen is just lost detail — and with no monitor plugged in, X falls back to something tiny // (800x480 here), which halves to an unreadable 400x240. Scale only when there is genuinely too much // to send, and judge that on what is actually being served: the clip if there is one, else the // whole framebuffer. const width = clipTo ? clipTo.w : geo.framebufferWidth; const scale = width !== null && width > SCALE_ABOVE_WIDTH ? ['-scale', '0.5'] : []; if (width !== null) { console.log(`[vnc] serving ${width}px wide — ${scale.length ? 'scaling to 50%' : '1:1'}`); } const proc = Bun.spawn({ cmd: [ 'x11vnc', '-display', display, '-auth', xauthority, '-rfbport', String(MIRROR_PORT), '-rfbauth', passwdFile, '-localhost', '-forever', '-shared', ...clip, ...scale, // No -noxdamage. It was set when this was written, with no recorded reason, and it is expensive: // without the DAMAGE extension x11vnc cannot be told which rectangles changed, so it polls the // WHOLE framebuffer continuously to find out. That makes cost scale with screen area rather than // with what actually moved — on a 4K mirror it is the single biggest source of latency, and it is // why a smaller machine felt instant by comparison. This X server reports DAMAGE, MIT-SHM and // XFIXES, so the optimisation is genuinely available. // // If stale patches ever appear on screen (some drivers under-report damage), putting -noxdamage // back is the fix — at the cost of the polling above. '-quiet', ], stdout: 'ignore', stderr: 'pipe', }); // Drain stderr so the pipe cannot fill and stall x11vnc; keep the tail for errors let stderrTail = ''; (async () => { const reader = (proc.stderr as ReadableStream).getReader(); const decoder = new TextDecoder(); try { while (true) { const { done, value } = await reader.read(); if (done) break; stderrTail = (stderrTail + decoder.decode(value, { stream: true })).slice(-2000); } } catch { // process ended } })(); if (!(await waitForPort(MIRROR_PORT, proc))) { try { proc.kill(); } catch { // already dead } throw new Error(`x11vnc failed to listen on ${MIRROR_PORT}: ${stderrTail.trim() || 'timed out'}`); } mirror = { email: params.email, username: params.username ?? '', display: displayNum, port: MIRROR_PORT, pid: proc.pid, }; console.log(`[vnc] mirroring ${display} on port ${MIRROR_PORT} (pid ${proc.pid})`); return { port: MIRROR_PORT, display: displayNum }; } export function stopSession(_email: string): void { if (!mirror) return; try { process.kill(mirror.pid); } catch { // already dead } console.log(`[vnc] stopped mirror of :${mirror.display} (pid ${mirror.pid})`); mirror = null; } export function getSession(_email: string): VncSessionInfo | null { if (!mirror) return null; const alive = mirror.pid > 0 && isProcessAlive(mirror.pid); if (!alive) { mirror = null; return null; } return { email: mirror.email, display: mirror.display, port: mirror.port, pid: mirror.pid, alive, }; } export function stopAll(): void { stopSession(''); }