vnc: find the display instead of assuming :0
The mirror hardcoded :0. That held under LightDM, which gave the user session :0. GDM does not — it keeps :0 for its own greeter and starts the user's Xorg with -displayfd, letting the number be picked at runtime; on this machine the session lands on :1. So after migrating to Ubuntu Desktop the mirror failed on every attempt with "Can't open display :0", with a healthy session sitting one number over. Resolve by socket ownership: /tmp/.X11-unix/X<n> is owned by whoever runs that X server, so the socket owned by us is the owner's session and anything else is the greeter's. Falls back to the lowest socket (a root-run Xorg, as LightDM had) and finally to 0, so it is never worse than the constant it replaces. The display is now threaded through rather than read from a module constant, so getFramebufferWidth measures the display actually being mirrored and both log lines name it. Found while verifying the XFCE-to-GNOME migration on this machine — the session was up and x11 with the cookie in the GDM path, and only the display number was wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,23 +1,53 @@
|
||||
import { existsSync, mkdirSync } from 'node:fs';
|
||||
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 exactly one :0, hence one
|
||||
// shared mirror — the first caller's VNC password is the one it answers with.
|
||||
const MIRROR_DISPLAY = ':0';
|
||||
const MIRROR_DISPLAY_NUM = 0;
|
||||
// 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;
|
||||
|
||||
// x11vnc reads :0'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/<uid>/gdm/Xauthority; LightDM (or a manual startx) uses the classic ~/.Xauthority. Try
|
||||
// the GDM path first, then fall back. While the greeter owns :0 the user's cookie doesn't exist yet and
|
||||
// mirroring fails until someone is logged in.
|
||||
// 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<n> 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/<uid>/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 = [
|
||||
@@ -90,12 +120,12 @@ export async function ensureVncPassword(homeDir: string): Promise<{ password: st
|
||||
return { password, passwdFile };
|
||||
}
|
||||
|
||||
// Width of :0's framebuffer, or null when it cannot be read (xrandr missing, X not up). Callers
|
||||
// treat null as "don't scale" — serving too many pixels beats serving an unreadable thumbnail.
|
||||
function getFramebufferWidth(xauthority: string): number | null {
|
||||
// Width of the mirrored display's framebuffer, or null when it cannot be read (xrandr missing, X not
|
||||
// up). Callers treat null as "don't scale" — serving too many pixels beats an unreadable thumbnail.
|
||||
function getFramebufferWidth(xauthority: string, display: string): number | null {
|
||||
const proc = Bun.spawnSync({
|
||||
cmd: ['xrandr', '--current'],
|
||||
env: { ...process.env, DISPLAY: MIRROR_DISPLAY, XAUTHORITY: xauthority },
|
||||
env: { ...process.env, DISPLAY: display, XAUTHORITY: xauthority },
|
||||
stdout: 'pipe',
|
||||
stderr: 'ignore',
|
||||
});
|
||||
@@ -137,26 +167,29 @@ export async function startSession(params: VncStartParams): Promise<{ port: numb
|
||||
const homeDir = getOwnerHomeDir(params.email);
|
||||
const { passwdFile } = await ensureVncPassword(homeDir);
|
||||
|
||||
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 ${MIRROR_DISPLAY}`);
|
||||
throw new Error(`No X authority found (GDM or ~/.Xauthority) — nobody is logged in on ${display}`);
|
||||
}
|
||||
|
||||
// Halving a 4K framebuffer 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.
|
||||
const width = getFramebufferWidth(xauthority);
|
||||
const width = getFramebufferWidth(xauthority, display);
|
||||
const scale = width !== null && width > SCALE_ABOVE_WIDTH ? ['-scale', '0.5'] : [];
|
||||
if (width !== null) {
|
||||
console.log(`[vnc] :0 framebuffer is ${width}px wide — ${scale.length ? 'scaling to 50%' : 'serving 1:1'}`);
|
||||
console.log(`[vnc] ${display} framebuffer is ${width}px wide — ${scale.length ? 'scaling to 50%' : 'serving 1:1'}`);
|
||||
}
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [
|
||||
'x11vnc',
|
||||
'-display',
|
||||
MIRROR_DISPLAY,
|
||||
display,
|
||||
'-auth',
|
||||
xauthority,
|
||||
'-rfbport',
|
||||
@@ -202,13 +235,13 @@ export async function startSession(params: VncStartParams): Promise<{ port: numb
|
||||
mirror = {
|
||||
email: params.email,
|
||||
username: params.username ?? '',
|
||||
display: MIRROR_DISPLAY_NUM,
|
||||
display: displayNum,
|
||||
port: MIRROR_PORT,
|
||||
pid: proc.pid,
|
||||
};
|
||||
console.log(`[vnc] mirroring ${MIRROR_DISPLAY} on port ${MIRROR_PORT} (pid ${proc.pid})`);
|
||||
console.log(`[vnc] mirroring ${display} on port ${MIRROR_PORT} (pid ${proc.pid})`);
|
||||
|
||||
return { port: MIRROR_PORT, display: MIRROR_DISPLAY_NUM };
|
||||
return { port: MIRROR_PORT, display: displayNum };
|
||||
}
|
||||
|
||||
export function stopSession(_email: string): void {
|
||||
@@ -220,7 +253,7 @@ export function stopSession(_email: string): void {
|
||||
// already dead
|
||||
}
|
||||
|
||||
console.log(`[vnc] stopped mirror of ${MIRROR_DISPLAY} (pid ${mirror.pid})`);
|
||||
console.log(`[vnc] stopped mirror of :${mirror.display} (pid ${mirror.pid})`);
|
||||
mirror = null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user