fix(serve): give each Linux account its own Xvfb display instead of sharing :99 (#29458)

* fix(serve): give each Linux account its own Xvfb display instead of sharing :99

* fix(serve): require the spawned Xvfb to be alive before using its display
This commit is contained in:
Neil
2026-10-15 23:25:30 -07:00
committed by GitHub
parent 7e778d8129
commit c30d6381df
3 changed files with 419 additions and 143 deletions
+178 -16
View File
@@ -23,15 +23,24 @@ vi.mock('fs', () => ({
}))
vi.mock('electron', () => ({ app: appMock }))
const OWN_UID = 1000
const OTHER_UID = 1001
const ORIGINAL_PLATFORM = process.platform
const ORIGINAL_GETUID = Object.getOwnPropertyDescriptor(process, 'getuid')
const ORIGINAL_DISPLAY = process.env.DISPLAY
function setPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, 'platform', { value: platform, configurable: true })
// Why: Windows has no getuid, and a simulated Linux run still needs this account's uid.
Object.defineProperty(process, 'getuid', {
value: () => OWN_UID,
configurable: true,
writable: true
})
}
function mockLiveXDisplay(pid = 4321): void {
statSyncMock.mockReturnValue({ isSocket: () => true })
statSyncMock.mockReturnValue({ isSocket: () => true, uid: OWN_UID })
existsSyncMock.mockReturnValue(true)
readFileSyncMock.mockReturnValue(`${pid}\n`)
vi.spyOn(process, 'kill').mockImplementation(() => true)
@@ -39,7 +48,7 @@ function mockLiveXDisplay(pid = 4321): void {
function mockXvfbTakesDisplay(pid = 1234): void {
let bound = false
statSyncMock.mockImplementation(() => ({ isSocket: () => true }))
statSyncMock.mockImplementation(() => ({ isSocket: () => true, uid: OWN_UID }))
readFileSyncMock.mockImplementation(() => {
if (!bound) {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
@@ -73,6 +82,11 @@ describe('ensureVirtualDisplayForHeadlessServe', () => {
stopVirtualDisplay()
vi.restoreAllMocks()
setPlatform(ORIGINAL_PLATFORM)
if (ORIGINAL_GETUID) {
Object.defineProperty(process, 'getuid', ORIGINAL_GETUID)
} else {
Reflect.deleteProperty(process, 'getuid')
}
if (ORIGINAL_DISPLAY === undefined) {
delete process.env.DISPLAY
} else {
@@ -114,6 +128,9 @@ describe('ensureVirtualDisplayForHeadlessServe', () => {
it('reports unsupported when Xvfb cannot be launched', async () => {
setPlatform('linux')
statSyncMock.mockImplementation(() => {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
})
spawnMock.mockReturnValue({ pid: undefined, once: vi.fn(), kill: vi.fn(), killed: false })
const {
ensureVirtualDisplayForHeadlessServe,
@@ -141,7 +158,7 @@ describe('ensureVirtualDisplayForHeadlessServe', () => {
it('leaves an externally configured stale display untouched', async () => {
setPlatform('linux')
process.env.DISPLAY = ':77'
statSyncMock.mockReturnValue({ isSocket: () => true })
statSyncMock.mockReturnValue({ isSocket: () => true, uid: OWN_UID })
existsSyncMock.mockReturnValue(true)
readFileSyncMock.mockImplementation(() => {
throw new Error('display lock is outside this namespace')
@@ -159,7 +176,7 @@ describe('ensureVirtualDisplayForHeadlessServe', () => {
it('serves on an externally configured display that has no lock file', async () => {
setPlatform('linux')
process.env.DISPLAY = ':0'
statSyncMock.mockReturnValue({ isSocket: () => true })
statSyncMock.mockReturnValue({ isSocket: () => true, uid: OWN_UID })
readFileSyncMock.mockImplementation(() => {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
})
@@ -191,7 +208,7 @@ describe('ensureVirtualDisplayForHeadlessServe', () => {
it('reuses an existing virtual display only when its X server is alive', async () => {
setPlatform('linux')
statSyncMock.mockReturnValue({ isSocket: () => true }) // :99 socket present
statSyncMock.mockReturnValue({ isSocket: () => true, uid: OWN_UID }) // :99 socket present
existsSyncMock.mockReturnValue(true)
readFileSyncMock.mockReturnValue('4321\n') // lock holds a PID
const killSpy = vi.spyOn(process, 'kill').mockReturnValue(true as never) // PID alive
@@ -209,7 +226,7 @@ describe('ensureVirtualDisplayForHeadlessServe', () => {
setPlatform('linux')
existsSyncMock.mockReturnValue(true) // lock present
let bound = false
statSyncMock.mockImplementation(() => ({ isSocket: () => true }))
statSyncMock.mockImplementation(() => ({ isSocket: () => true, uid: OWN_UID }))
readFileSyncMock.mockImplementation(() => (bound ? '1234\n' : '9999\n'))
// The orphan lock names a dead PID; the freshly spawned Xvfb is alive.
const killSpy = vi.spyOn(process, 'kill').mockImplementation((pid) => {
@@ -236,12 +253,11 @@ describe('ensureVirtualDisplayForHeadlessServe', () => {
killSpy.mockRestore()
})
// A root-owned stale :99 socket (crashed system Xvfb, serve running as User=orca) cannot be
// unlinked, so our Xvfb refuses to bind and exits. Trusting the surviving socket set DISPLAY to a
// dead server and Chromium died in Ozone init with SIGSEGV.
// A stale :99 socket this account cannot unlink makes our Xvfb refuse to bind and exit. Trusting
// the surviving socket set DISPLAY to a dead server and Chromium died in Ozone init with SIGSEGV.
it('reports failure when a stale socket blocks the Xvfb rebind', async () => {
setPlatform('linux')
statSyncMock.mockReturnValue({ isSocket: () => true })
statSyncMock.mockReturnValue({ isSocket: () => true, uid: OWN_UID })
// Removal fails (foreign owner) and no lock ever appears, because Xvfb never took the display.
rmSyncMock.mockImplementation(() => {
throw Object.assign(new Error('permission denied'), { code: 'EACCES' })
@@ -271,7 +287,7 @@ describe('ensureVirtualDisplayForHeadlessServe', () => {
it('accepts the display once the spawned Xvfb owns its lock', async () => {
setPlatform('linux')
let lockWritten = false
statSyncMock.mockImplementation(() => ({ isSocket: () => lockWritten }))
statSyncMock.mockImplementation(() => ({ isSocket: () => lockWritten, uid: OWN_UID }))
rmSyncMock.mockImplementation(() => {})
readFileSyncMock.mockImplementation(() => {
if (!lockWritten) {
@@ -290,6 +306,150 @@ describe('ensureVirtualDisplayForHeadlessServe', () => {
expect(process.env.DISPLAY).toBe(':99')
})
describe('with several accounts on one machine (#26602)', () => {
type FakeDisplay = { uid: number; lockPid: number | null; socket: boolean }
// X artifacts per display number; each spawned Xvfb takes its display under this account.
function mockDisplays(displays: Record<number, FakeDisplay>, alivePids: number[]): void {
const displayOf = (path: string): FakeDisplay | undefined => {
const match = /(?:\.X(\d+)-lock|X11-unix\/X(\d+))$/.exec(path)
return match ? displays[Number(match[1] ?? match[2])] : undefined
}
const enoent = () => Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
statSyncMock.mockImplementation((path: string) => {
const display = displayOf(path)
const isLock = path.endsWith('-lock')
if (!display || (isLock ? display.lockPid === null : !display.socket)) {
throw enoent()
}
return { uid: display.uid, isSocket: () => !isLock }
})
readFileSyncMock.mockImplementation((path: string) => {
if (path === '/proc/net/unix') {
return ''
}
const display = displayOf(path)
if (!display || display.lockPid === null) {
throw enoent()
}
return `${display.lockPid}\n`
})
vi.spyOn(process, 'kill').mockImplementation((pid) => {
if (!alivePids.includes(Number(pid))) {
throw Object.assign(new Error('ESRCH'), { code: 'ESRCH' })
}
return true
})
let nextPid = 5000
spawnMock.mockImplementation((_command: string, args: string[]) => {
const pid = nextPid++
alivePids.push(pid)
const displayNumber = Number(args[0].slice(1))
if (!displays[displayNumber]) {
displays[displayNumber] = { uid: OWN_UID, lockPid: pid, socket: true }
}
return { pid, once: vi.fn(), kill: vi.fn(), killed: false }
})
}
it("starts its own display instead of attaching to another account's live :99", async () => {
setPlatform('linux')
mockDisplays({ 99: { uid: OTHER_UID, lockPid: 777, socket: true } }, [777])
const { ensureVirtualDisplayForHeadlessServe } = await import('./ensure-virtual-display')
expect(ensureVirtualDisplayForHeadlessServe({ isServeMode: true })).toBe(true)
expect(process.env.DISPLAY).toBe(':100')
expect(spawnMock).toHaveBeenCalledWith(
'Xvfb',
expect.arrayContaining([':100']),
expect.any(Object)
)
expect(rmSyncMock).not.toHaveBeenCalled()
})
it("skips another account's stale :99 that sticky /tmp keeps it from removing", async () => {
setPlatform('linux')
mockDisplays({ 99: { uid: OTHER_UID, lockPid: 777, socket: true } }, [])
const { ensureVirtualDisplayForHeadlessServe } = await import('./ensure-virtual-display')
expect(ensureVirtualDisplayForHeadlessServe({ isServeMode: true })).toBe(true)
expect(process.env.DISPLAY).toBe(':100')
expect(rmSyncMock).not.toHaveBeenCalled()
})
it('reuses its own live display past the ones other accounts hold', async () => {
setPlatform('linux')
mockDisplays(
{
99: { uid: OTHER_UID, lockPid: 777, socket: true },
100: { uid: OWN_UID, lockPid: 888, socket: true }
},
[777, 888]
)
const { ensureVirtualDisplayForHeadlessServe } = await import('./ensure-virtual-display')
expect(ensureVirtualDisplayForHeadlessServe({ isServeMode: true })).toBe(true)
expect(process.env.DISPLAY).toBe(':100')
expect(spawnMock).not.toHaveBeenCalled()
})
it("leaves its own account's display alone while that server is still starting", async () => {
setPlatform('linux')
// :99 has its live lock but no socket yet.
mockDisplays({ 99: { uid: OWN_UID, lockPid: 888, socket: false } }, [888])
const { ensureVirtualDisplayForHeadlessServe } = await import('./ensure-virtual-display')
expect(ensureVirtualDisplayForHeadlessServe({ isServeMode: true })).toBe(true)
expect(process.env.DISPLAY).toBe(':100')
expect(rmSyncMock).not.toHaveBeenCalled()
})
it('does not accept a spawned Xvfb that died after writing its lock and socket', async () => {
setPlatform('linux')
const alive: number[] = []
mockDisplays({}, alive)
const startXvfb = spawnMock.getMockImplementation()!
spawnMock.mockImplementation((command: string, args: string[]) => {
const child = startXvfb(command, args)
alive.splice(alive.indexOf(child.pid), 1)
return child
})
const { ensureVirtualDisplayForHeadlessServe } = await import('./ensure-virtual-display')
vi.useFakeTimers({ toFake: ['Date'] })
const sleep = vi
.spyOn(Atomics, 'wait')
.mockImplementation((_array, _index, _value, timeout) => {
vi.setSystemTime(Date.now() + (timeout ?? 0))
return 'timed-out'
})
try {
expect(ensureVirtualDisplayForHeadlessServe({ isServeMode: true })).toBe(false)
expect(process.env.DISPLAY).toBeUndefined()
} finally {
sleep.mockRestore()
vi.useRealTimers()
}
})
it('moves on when another server wins the free display first', async () => {
setPlatform('linux')
const displays: Record<number, FakeDisplay> = {}
mockDisplays(displays, [777])
const startXvfb = spawnMock.getMockImplementation()!
spawnMock.mockImplementationOnce((command: string, args: string[]) => {
// Another account's Xvfb binds :99 between our check and our own bind.
displays[99] = { uid: OTHER_UID, lockPid: 777, socket: true }
return startXvfb(command, args)
})
const { ensureVirtualDisplayForHeadlessServe } = await import('./ensure-virtual-display')
expect(ensureVirtualDisplayForHeadlessServe({ isServeMode: true })).toBe(true)
expect(process.env.DISPLAY).toBe(':100')
expect(spawnMock).toHaveBeenCalledTimes(2)
})
})
describe('hasUsableLinuxDisplay', () => {
it('accepts live local X11 and Wayland sockets', async () => {
setPlatform('linux')
@@ -363,7 +523,7 @@ describe('ensureVirtualDisplayForHeadlessServe', () => {
// half-finished cleanup — trusting it because DISPLAY names it would accept a dead display.
it('does not trust a lockless socket on its own managed display number', async () => {
setPlatform('linux')
statSyncMock.mockReturnValue({ isSocket: () => true })
statSyncMock.mockReturnValue({ isSocket: () => true, uid: OWN_UID })
readFileSyncMock.mockImplementation(() => {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
})
@@ -376,7 +536,7 @@ describe('ensureVirtualDisplayForHeadlessServe', () => {
it('rejects an orphaned local X11 socket whose server PID is gone', async () => {
setPlatform('linux')
statSyncMock.mockReturnValue({ isSocket: () => true })
statSyncMock.mockReturnValue({ isSocket: () => true, uid: OWN_UID })
existsSyncMock.mockReturnValue(true)
readFileSyncMock.mockReturnValue('9999\n')
vi.spyOn(process, 'kill').mockImplementation(() => {
@@ -392,7 +552,7 @@ describe('ensureVirtualDisplayForHeadlessServe', () => {
// socket is an endpoint published from elsewhere (container bind mount, WSLg) — not an orphan.
it('accepts a local X11 socket published without a lock file', async () => {
setPlatform('linux')
statSyncMock.mockReturnValue({ isSocket: () => true })
statSyncMock.mockReturnValue({ isSocket: () => true, uid: OWN_UID })
readFileSyncMock.mockImplementation(() => {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
})
@@ -400,6 +560,8 @@ describe('ensureVirtualDisplayForHeadlessServe', () => {
const { hasUsableLinuxDisplay } = await import('./ensure-virtual-display')
expect(hasUsableLinuxDisplay({ DISPLAY: ':0' })).toBe(true)
// Numbers Orca may pick for itself are still someone else's when DISPLAY names them.
expect(hasUsableLinuxDisplay({ DISPLAY: ':100' })).toBe(true)
expect(killSpy).not.toHaveBeenCalled()
})
@@ -432,7 +594,7 @@ describe('ensureVirtualDisplayForHeadlessServe', () => {
it('rejects a lock that exists but cannot be read', async () => {
setPlatform('linux')
statSyncMock.mockReturnValue({ isSocket: () => true })
statSyncMock.mockReturnValue({ isSocket: () => true, uid: OWN_UID })
readFileSyncMock.mockImplementation(() => {
throw Object.assign(new Error('permission denied'), { code: 'EACCES' })
})
@@ -443,7 +605,7 @@ describe('ensureVirtualDisplayForHeadlessServe', () => {
it('accepts a live local X11 server owned by another user', async () => {
setPlatform('linux')
statSyncMock.mockReturnValue({ isSocket: () => true })
statSyncMock.mockReturnValue({ isSocket: () => true, uid: OWN_UID })
existsSyncMock.mockReturnValue(true)
readFileSyncMock.mockReturnValue('4321\n')
vi.spyOn(process, 'kill').mockImplementation(() => {
+102 -127
View File
@@ -1,7 +1,16 @@
import { spawn, type ChildProcess } from 'node:child_process'
import { readFileSync, rmSync, statSync } from 'node:fs'
import { isAbsolute, join } from 'node:path'
import { app } from 'electron'
import {
displayOwnership,
hasAbstractXSocket,
isForeignDisplayServerAlive,
isManagedDisplayServerAlive,
isUnixSocket,
readDisplayLockPid,
removeStaleDisplayArtifacts,
xvfbSocketPath
} from './x-display-artifacts'
// Why: headless `orca serve` backs browser panes with offscreen BrowserWindows.
// On Linux, Electron has no display platform without an X server and segfaults
@@ -11,8 +20,9 @@ import { app } from 'electron'
const XVFB_STARTUP_TIMEOUT_MS = 5_000
const XVFB_POLL_INTERVAL_MS = 50
const VIRTUAL_DISPLAY_NUMBER = 99
const VIRTUAL_DISPLAY = `:${VIRTUAL_DISPLAY_NUMBER}`
// Why a range: accounts on one machine each need their own display (#26602); :99 stays the first choice.
const FIRST_VIRTUAL_DISPLAY_NUMBER = 99
const VIRTUAL_DISPLAY_NUMBER_COUNT = 32
const XVFB_INSTALL_GUIDANCE =
'install an X virtual framebuffer: `xvfb` on Debian/Ubuntu, `xorg-x11-server-Xvfb` on RPM-based systems, or `xorg-server-xvfb` on Arch.'
@@ -28,71 +38,11 @@ function configureHeadlessServeChromiumFlags(): void {
app.commandLine.appendSwitch('disable-gpu')
}
function xvfbSocketPath(displayNumber: number): string {
return `/tmp/.X11-unix/X${displayNumber}`
}
function xDisplayLockPath(displayNumber: number): string {
return `/tmp/.X${displayNumber}-lock`
}
// Why: a socket file can outlive the X server that made it. The X lock file holds the server PID;
// if that process is gone, the display is dead despite the socket. `missing` is a third outcome the
// two callers must treat differently — see each call site.
type DisplayLockProbe = 'alive' | 'dead' | 'missing'
function probeDisplayLock(displayNumber: number): DisplayLockProbe {
let pid: number
try {
pid = Number.parseInt(readFileSync(xDisplayLockPath(displayNumber), 'utf8').trim(), 10)
} catch (error) {
// An unreadable lock is a lock we cannot clear: treat it as dead, not absent.
return (error as NodeJS.ErrnoException)?.code === 'ENOENT' ? 'missing' : 'dead'
}
if (!Number.isInteger(pid) || pid <= 0) {
return 'dead'
}
try {
// signal 0 probes existence without affecting the process.
process.kill(pid, 0)
return 'alive'
} catch (error) {
// EPERM means the PID exists under another uid — a root-owned X server is still live.
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'EPERM'
? 'alive'
: 'dead'
}
}
/**
* Liveness for a display Orca did not create. An X server writes its lock beside the socket and
* both survive a crash (verified against Xvfb under SIGKILL), so a socket with no lock was never
* left by a crashed server — it is an endpoint published from elsewhere: a container bind-mounting
* only /tmp/.X11-unix, WSLg, or a foreign PID namespace. We cannot judge those, and refusing them
* blocks startup on displays that work.
*/
function isForeignDisplayServerAlive(displayNumber: number): boolean {
return probeDisplayLock(displayNumber) !== 'dead'
}
/**
* Liveness for Orca's own VIRTUAL_DISPLAY_NUMBER. Stricter on purpose: `removeStaleDisplayArtifacts`
* unlinks the lock before the socket, so a lockless socket here is Orca's own half-finished
* teardown, not a foreign endpoint. Adopting it would resurrect the orphan-socket bug and stop the
* cleanup below from self-healing.
*/
function isManagedDisplayServerAlive(displayNumber: number): boolean {
return probeDisplayLock(displayNumber) === 'alive'
}
function removeStaleDisplayArtifacts(displayNumber: number): void {
for (const path of [xDisplayLockPath(displayNumber), xvfbSocketPath(displayNumber)]) {
try {
rmSync(path, { force: true })
} catch {
// Best effort; if removal fails, Xvfb startup below will surface the error.
}
}
function isManagedDisplayNumber(displayNumber: number): boolean {
return (
displayNumber >= FIRST_VIRTUAL_DISPLAY_NUMBER &&
displayNumber < FIRST_VIRTUAL_DISPLAY_NUMBER + VIRTUAL_DISPLAY_NUMBER_COUNT
)
}
function sleepSync(ms: number): void {
@@ -105,16 +55,36 @@ function sleepSync(ms: number): void {
// crashed system Xvfb, which `User=orca` serve cannot remove) still exists after our own Xvfb
// refused to bind the display. Treating that as ready sets DISPLAY to a dead server and Chromium
// dies in Ozone init with SIGSEGV instead of reporting an unusable display.
function waitForDisplayReady(displayNumber: number, deadline: number): boolean {
const isReady = (): boolean =>
isUnixSocket(xvfbSocketPath(displayNumber)) && isManagedDisplayServerAlive(displayNumber)
// Why our PID in the lock: another account's server may win the same free number between our
// check and our Xvfb's bind, and its live lock must not read as our display being ready.
type DisplayStart = 'ready' | 'taken' | 'timeout'
function waitForDisplayReady(
displayNumber: number,
xvfbPid: number,
deadline: number
): DisplayStart {
const probe = (): DisplayStart | null => {
const lockPid = readDisplayLockPid(displayNumber)
if (lockPid === null) {
return null
}
if (lockPid !== xvfbPid) {
return isManagedDisplayServerAlive(displayNumber) ? 'taken' : null
}
// Why the liveness probe too: lock and socket both survive a SIGKILLed Xvfb.
return isUnixSocket(xvfbSocketPath(displayNumber)) && isManagedDisplayServerAlive(displayNumber)
? 'ready'
: null
}
while (Date.now() < deadline) {
if (isReady()) {
return true
const state = probe()
if (state) {
return state
}
sleepSync(XVFB_POLL_INTERVAL_MS)
}
return isReady()
return probe() ?? 'timeout'
}
// Validate display syntax and local sockets before Chromium reaches Ozone initialization.
@@ -157,34 +127,6 @@ export function missingLinuxDisplayMessage(isServeMode: boolean): string {
return isServeMode ? MISSING_SERVE_DISPLAY_MESSAGE : MISSING_LINUX_DISPLAY_MESSAGE
}
// Why: an X server may bind only the abstract namespace (`@/tmp/.X11-unix/X0`), which leaves no
// filesystem socket to stat. Abstract addresses are kernel-owned and vanish the moment the owner
// exits, so an entry here is proof of a live server — no lock file needed, and no stale entry is
// possible. Refusing these was a hard startup failure with no workaround.
function hasAbstractXSocket(displayNumber: number): boolean {
let table: unknown
try {
table = readFileSync('/proc/net/unix', 'utf8')
} catch {
return false
}
if (typeof table !== 'string') {
return false
}
const address = `@${xvfbSocketPath(displayNumber)}`
return table
.split('\n')
.some((line) => line.slice(line.lastIndexOf(' ') + 1).trimEnd() === address)
}
function isUnixSocket(path: string): boolean {
try {
return statSync(path).isSocket()
} catch {
return false
}
}
function hasUsableXDisplay(value: string | undefined): boolean {
const display = value?.trim()
if (!display) {
@@ -198,10 +140,10 @@ function hasUsableXDisplay(value: string | undefined): boolean {
}
const displayNumber = Number(localDisplay[1])
if (isUnixSocket(xvfbSocketPath(displayNumber))) {
// Why the managed number is never treated as foreign: Orca's own teardown unlinks the lock
// before the socket, so a lockless socket on VIRTUAL_DISPLAY_NUMBER is our own half-finished
// cleanup even when DISPLAY names it explicitly. Trusting it there would accept a dead display.
return displayNumber === VIRTUAL_DISPLAY_NUMBER
// Why :99 is never treated as foreign: Orca's own teardown unlinks the lock before the socket,
// so a lockless :99 socket is our own half-finished cleanup even when DISPLAY names it. Higher
// numbers stay foreign-checked: a container may publish them without a lock.
return displayNumber === FIRST_VIRTUAL_DISPLAY_NUMBER
? isManagedDisplayServerAlive(displayNumber)
: isForeignDisplayServerAlive(displayNumber)
}
@@ -254,23 +196,50 @@ export function ensureVirtualDisplayForHeadlessServe(options: { isServeMode: boo
return false
}
// Why: reuse an existing display ONLY if a live X server actually backs it.
// A crashed prior run can leave an orphan socket; trusting it by path alone
// would advertise browser support that then fails at tab creation.
if (isUnixSocket(xvfbSocketPath(VIRTUAL_DISPLAY_NUMBER))) {
if (isManagedDisplayServerAlive(VIRTUAL_DISPLAY_NUMBER)) {
process.env.DISPLAY = VIRTUAL_DISPLAY
for (
let displayNumber = FIRST_VIRTUAL_DISPLAY_NUMBER;
isManagedDisplayNumber(displayNumber);
displayNumber += 1
) {
const ownership = displayOwnership(displayNumber)
if (ownership === 'foreign') {
continue
}
if (ownership === 'mine') {
// Why: reuse this account's display ONLY if a live X server actually backs it. A crashed
// prior run can leave an orphan socket; trusting it by path alone would advertise browser
// support that then fails at tab creation.
if (isManagedDisplayServerAlive(displayNumber)) {
if (isUnixSocket(xvfbSocketPath(displayNumber))) {
process.env.DISPLAY = `:${displayNumber}`
return true
}
// Why: a live lock without its socket is a server still starting; its lock is not ours to remove.
continue
}
// Why: stale socket/lock — clean them up so Xvfb can rebind the display.
removeStaleDisplayArtifacts(displayNumber)
}
const started = startVirtualDisplay(displayNumber)
if (started === 'ready') {
return true
}
// Why: stale socket/lock — clean them up so Xvfb can rebind the display
// below instead of refusing to start on an "in use" number.
removeStaleDisplayArtifacts(VIRTUAL_DISPLAY_NUMBER)
if (started !== 'taken') {
return false
}
}
console.warn(
`[serve] Every display from :${FIRST_VIRTUAL_DISPLAY_NUMBER} to :${FIRST_VIRTUAL_DISPLAY_NUMBER + VIRTUAL_DISPLAY_NUMBER_COUNT - 1} belongs to another X server; browser panes are unavailable. Set DISPLAY to a running X server.`
)
return false
}
function startVirtualDisplay(displayNumber: number): DisplayStart | 'unavailable' {
const display = `:${displayNumber}`
try {
xvfbProcess = spawn(
'Xvfb',
[VIRTUAL_DISPLAY, '-screen', '0', '1280x1024x24', '-nolisten', 'tcp', '-terminate'],
[display, '-screen', '0', '1280x1024x24', '-nolisten', 'tcp', '-terminate'],
{
stdio: 'ignore',
// Why: foreground Ctrl-C must not kill Xvfb before Electron disconnects.
@@ -283,33 +252,39 @@ export function ensureVirtualDisplayForHeadlessServe(options: { isServeMode: boo
// PATH lookup failures emit asynchronously, but a successful spawn has a PID immediately.
if (xvfbProcess.pid === undefined) {
xvfbProcess = null
return false
return 'unavailable'
}
} catch (error) {
console.warn(
'[serve] Could not start Xvfb:',
error instanceof Error ? error.message : String(error)
)
return false
return 'unavailable'
}
const ready = waitForDisplayReady(VIRTUAL_DISPLAY_NUMBER, Date.now() + XVFB_STARTUP_TIMEOUT_MS)
if (!ready) {
console.warn(
`[serve] Xvfb did not take ownership of ${VIRTUAL_DISPLAY}; browser panes are unavailable. ` +
'A stale socket from another user can block the rebind.'
)
const started = waitForDisplayReady(
displayNumber,
xvfbProcess.pid,
Date.now() + XVFB_STARTUP_TIMEOUT_MS
)
if (started !== 'ready') {
if (started === 'timeout') {
console.warn(
`[serve] Xvfb did not take ownership of ${display}; browser panes are unavailable. ` +
'A stale socket from another user can block the rebind.'
)
}
stopVirtualDisplay()
return false
return started
}
process.env.DISPLAY = VIRTUAL_DISPLAY
process.env.DISPLAY = display
// Why: -terminate only takes effect after Xvfb accepts its first client.
process.once('exit', stopVirtualDisplay)
app.once('ready', () => process.removeListener('exit', stopVirtualDisplay))
return true
return 'ready'
}
export function stopVirtualDisplay(): void {
+139
View File
@@ -0,0 +1,139 @@
import { readFileSync, rmSync, statSync } from 'node:fs'
// X server lock and socket files under /tmp, and what they say about who owns a display.
export function xvfbSocketPath(displayNumber: number): string {
return `/tmp/.X11-unix/X${displayNumber}`
}
export function xDisplayLockPath(displayNumber: number): string {
return `/tmp/.X${displayNumber}-lock`
}
// Why: a socket file can outlive the X server that made it. The X lock file holds the server PID;
// if that process is gone, the display is dead despite the socket. `missing` is a third outcome the
// two callers must treat differently — see each call site.
export type DisplayLockProbe = 'alive' | 'dead' | 'missing'
export function probeDisplayLock(displayNumber: number): DisplayLockProbe {
let pid: number
try {
pid = Number.parseInt(readFileSync(xDisplayLockPath(displayNumber), 'utf8').trim(), 10)
} catch (error) {
// An unreadable lock is a lock we cannot clear: treat it as dead, not absent.
return (error as NodeJS.ErrnoException)?.code === 'ENOENT' ? 'missing' : 'dead'
}
if (!Number.isInteger(pid) || pid <= 0) {
return 'dead'
}
try {
// signal 0 probes existence without affecting the process.
process.kill(pid, 0)
return 'alive'
} catch (error) {
// EPERM means the PID exists under another uid — a root-owned X server is still live.
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'EPERM'
? 'alive'
: 'dead'
}
}
/**
* Liveness for a display Orca did not create. An X server writes its lock beside the socket and
* both survive a crash (verified against Xvfb under SIGKILL), so a socket with no lock was never
* left by a crashed server — it is an endpoint published from elsewhere: a container bind-mounting
* only /tmp/.X11-unix, WSLg, or a foreign PID namespace. We cannot judge those, and refusing them
* blocks startup on displays that work.
*/
export function isForeignDisplayServerAlive(displayNumber: number): boolean {
return probeDisplayLock(displayNumber) !== 'dead'
}
/**
* Liveness for Orca's own managed display numbers. Stricter on purpose: `removeStaleDisplayArtifacts`
* unlinks the lock before the socket, so a lockless socket here is Orca's own half-finished
* teardown, not a foreign endpoint. Adopting it would resurrect the orphan-socket bug and stop the
* cleanup below from self-healing.
*/
export function isManagedDisplayServerAlive(displayNumber: number): boolean {
return probeDisplayLock(displayNumber) === 'alive'
}
export type DisplayOwnership = 'free' | 'mine' | 'foreign'
/**
* Whose display a number is. Another account's X server, or its leftovers that this account
* cannot unlink from sticky /tmp, must never be adopted or cleaned up: that account can stop it
* at any moment, and its display is not ours to share.
*/
export function displayOwnership(displayNumber: number): DisplayOwnership {
const uid = process.getuid?.()
let present = false
for (const path of [xDisplayLockPath(displayNumber), xvfbSocketPath(displayNumber)]) {
let owner: number
try {
owner = statSync(path).uid
} catch (error) {
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
continue
}
return 'foreign'
}
present = true
if (uid === undefined || owner !== uid) {
return 'foreign'
}
}
if (present) {
return 'mine'
}
// A server in another mount namespace may hold only the abstract socket.
return hasAbstractXSocket(displayNumber) ? 'foreign' : 'free'
}
export function removeStaleDisplayArtifacts(displayNumber: number): void {
for (const path of [xDisplayLockPath(displayNumber), xvfbSocketPath(displayNumber)]) {
try {
rmSync(path, { force: true })
} catch {
// Best effort; if removal fails, Xvfb startup below will surface the error.
}
}
}
export function readDisplayLockPid(displayNumber: number): number | null {
try {
const pid = Number.parseInt(readFileSync(xDisplayLockPath(displayNumber), 'utf8').trim(), 10)
return Number.isInteger(pid) && pid > 0 ? pid : null
} catch {
return null
}
}
// Why: an X server may bind only the abstract namespace (`@/tmp/.X11-unix/X0`), which leaves no
// filesystem socket to stat. Abstract addresses are kernel-owned and vanish the moment the owner
// exits, so an entry here is proof of a live server — no lock file needed, and no stale entry is
// possible. Refusing these was a hard startup failure with no workaround.
export function hasAbstractXSocket(displayNumber: number): boolean {
let table: unknown
try {
table = readFileSync('/proc/net/unix', 'utf8')
} catch {
return false
}
if (typeof table !== 'string') {
return false
}
const address = `@${xvfbSocketPath(displayNumber)}`
return table
.split('\n')
.some((line) => line.slice(line.lastIndexOf(' ') + 1).trimEnd() === address)
}
export function isUnixSocket(path: string): boolean {
try {
return statSync(path).isSocket()
} catch {
return false
}
}