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 { join } from 'node:path';
|
||||||
import type { VncStartParams, VncSessionInfo } from '../protocol';
|
import type { VncStartParams, VncSessionInfo } from '../protocol';
|
||||||
import { getOwnerHomeDir } from '@@/data-path';
|
import { getOwnerHomeDir } from '@@/data-path';
|
||||||
|
|
||||||
// Mirrors the physical display instead of spawning a virtual desktop per user, so the
|
// 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
|
// browser shows the same session as the screen. There is one seat, hence one shared
|
||||||
// shared mirror — the first caller's VNC password is the one it answers with.
|
// mirror — the first caller's VNC password is the one it answers with.
|
||||||
const MIRROR_DISPLAY = ':0';
|
|
||||||
const MIRROR_DISPLAY_NUM = 0;
|
|
||||||
const MIRROR_PORT = 5900;
|
const MIRROR_PORT = 5900;
|
||||||
const READY_TIMEOUT_MS = 5000;
|
const READY_TIMEOUT_MS = 5000;
|
||||||
// Above this framebuffer width the stream is halved; at or below it, pixels are served 1:1.
|
// Above this framebuffer width the stream is halved; at or below it, pixels are served 1:1.
|
||||||
const SCALE_ABOVE_WIDTH = 2560;
|
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
|
// The display number is NOT fixed. Under LightDM the user session got :0, so this was hardcoded to it.
|
||||||
// cookie lives depends on the display manager: GDM (Ubuntu GNOME on Xorg) keeps it in the per-session
|
// GDM gives :0 to its own greeter and starts the user's Xorg with -displayfd, letting the kernel pick —
|
||||||
// dir /run/user/<uid>/gdm/Xauthority; LightDM (or a manual startx) uses the classic ~/.Xauthority. Try
|
// in practice :1. Hardcoding :0 made the mirror fail with "Can't open display" on every GNOME machine.
|
||||||
// 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.
|
// 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 {
|
function resolveXauthority(): string | null {
|
||||||
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
||||||
const candidates = [
|
const candidates = [
|
||||||
@@ -90,12 +120,12 @@ export async function ensureVncPassword(homeDir: string): Promise<{ password: st
|
|||||||
return { password, passwdFile };
|
return { password, passwdFile };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Width of :0's framebuffer, or null when it cannot be read (xrandr missing, X not up). Callers
|
// Width of the mirrored display's framebuffer, or null when it cannot be read (xrandr missing, X not
|
||||||
// treat null as "don't scale" — serving too many pixels beats serving an unreadable thumbnail.
|
// up). Callers treat null as "don't scale" — serving too many pixels beats an unreadable thumbnail.
|
||||||
function getFramebufferWidth(xauthority: string): number | null {
|
function getFramebufferWidth(xauthority: string, display: string): number | null {
|
||||||
const proc = Bun.spawnSync({
|
const proc = Bun.spawnSync({
|
||||||
cmd: ['xrandr', '--current'],
|
cmd: ['xrandr', '--current'],
|
||||||
env: { ...process.env, DISPLAY: MIRROR_DISPLAY, XAUTHORITY: xauthority },
|
env: { ...process.env, DISPLAY: display, XAUTHORITY: xauthority },
|
||||||
stdout: 'pipe',
|
stdout: 'pipe',
|
||||||
stderr: 'ignore',
|
stderr: 'ignore',
|
||||||
});
|
});
|
||||||
@@ -137,26 +167,29 @@ export async function startSession(params: VncStartParams): Promise<{ port: numb
|
|||||||
const homeDir = getOwnerHomeDir(params.email);
|
const homeDir = getOwnerHomeDir(params.email);
|
||||||
const { passwdFile } = await ensureVncPassword(homeDir);
|
const { passwdFile } = await ensureVncPassword(homeDir);
|
||||||
|
|
||||||
|
const displayNum = resolveDisplayNum();
|
||||||
|
const display = `:${displayNum}`;
|
||||||
|
|
||||||
const xauthority = resolveXauthority();
|
const xauthority = resolveXauthority();
|
||||||
if (!xauthority) {
|
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
|
// 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
|
// 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
|
// tiny (800x480 here), which halves to an unreadable 400x240. Scale only when there is genuinely
|
||||||
// too much to send.
|
// too much to send.
|
||||||
const width = getFramebufferWidth(xauthority);
|
const width = getFramebufferWidth(xauthority, display);
|
||||||
const scale = width !== null && width > SCALE_ABOVE_WIDTH ? ['-scale', '0.5'] : [];
|
const scale = width !== null && width > SCALE_ABOVE_WIDTH ? ['-scale', '0.5'] : [];
|
||||||
if (width !== null) {
|
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({
|
const proc = Bun.spawn({
|
||||||
cmd: [
|
cmd: [
|
||||||
'x11vnc',
|
'x11vnc',
|
||||||
'-display',
|
'-display',
|
||||||
MIRROR_DISPLAY,
|
display,
|
||||||
'-auth',
|
'-auth',
|
||||||
xauthority,
|
xauthority,
|
||||||
'-rfbport',
|
'-rfbport',
|
||||||
@@ -202,13 +235,13 @@ export async function startSession(params: VncStartParams): Promise<{ port: numb
|
|||||||
mirror = {
|
mirror = {
|
||||||
email: params.email,
|
email: params.email,
|
||||||
username: params.username ?? '',
|
username: params.username ?? '',
|
||||||
display: MIRROR_DISPLAY_NUM,
|
display: displayNum,
|
||||||
port: MIRROR_PORT,
|
port: MIRROR_PORT,
|
||||||
pid: proc.pid,
|
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 {
|
export function stopSession(_email: string): void {
|
||||||
@@ -220,7 +253,7 @@ export function stopSession(_email: string): void {
|
|||||||
// already dead
|
// 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;
|
mirror = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user