import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; /** * Accès au serveur X depuis un agent lancé hors session graphique. * * Un service systemd n'hérite ni de `DISPLAY` ni du cookie d'autorisation, et le * chemin de ce cookie dépend du gestionnaire de session : `~/.Xauthority` n'est * plus la norme depuis longtemps. GDM le place sous `/run/user/`, et une * session Wayland le nomme avec un suffixe aléatoire. * * Résoudre à l'exécution plutôt qu'à l'installation évite de figer dans l'unité * systemd un chemin qui sera faux au prochain gestionnaire de session — ou dès * la première session Wayland. */ /** Premier candidat existant, ou null si aucun cookie n'est lisible. */ export function resolveXauthority(): string | null { for (const candidate of xauthorityCandidates()) { try { if (fs.statSync(candidate).isFile()) return candidate; } catch { /* candidat suivant */ } } return null; } function xauthorityCandidates(): string[] { const candidates: string[] = []; // Une valeur explicite prime, mais seulement si le fichier existe : l'unité // systemd en déclare une par défaut, qui peut très bien ne mener nulle part. if (process.env.XAUTHORITY) candidates.push(process.env.XAUTHORITY); const runtime = runtimeDir(); if (runtime) { candidates.push(path.join(runtime, 'gdm', 'Xauthority')); // Xwayland sous GNOME : `.mutter-Xwaylandauth.XXXXXX`, suffixe aléatoire. try { for (const entry of fs.readdirSync(runtime)) { if (entry.startsWith('.mutter-Xwaylandauth')) candidates.push(path.join(runtime, entry)); } } catch { /* répertoire illisible : on passe */ } } candidates.push(path.join(os.homedir(), '.Xauthority')); return candidates; } function runtimeDir(): string | null { if (process.env.XDG_RUNTIME_DIR) return process.env.XDG_RUNTIME_DIR; const uid = typeof process.getuid === 'function' ? process.getuid() : null; return uid === null ? null : `/run/user/${uid}`; } /** * Numéro d'écran à utiliser. On préfère la valeur héritée, puis la socket X * réellement présente — supposer `:0` échoue silencieusement sur une machine où * la session tourne sur un autre écran. */ export function resolveDisplay(): string { // Une chaîne vide n'est pas « non défini » pour `??` : on la traite comme telle. const declared = process.env.DISPLAY?.trim(); if (declared) return declared; try { const sockets = fs .readdirSync('/tmp/.X11-unix') .filter((entry) => /^X\d+$/.test(entry)) .map((entry) => Number(entry.slice(1))) .sort((a, b) => a - b); if (sockets.length > 0) return `:${sockets[0]}`; } catch { /* pas de socket listée : on retombe sur la valeur usuelle */ } return ':0'; } /** * Socket Wayland de la session, si elle existe. * * Sans cette variable, une application lancée par l'agent retombe sur XWayland * alors que la session est native — c'est-à-dire sur une copie d'image * supplémentaire, payée à chaque trame pendant tout l'enregistrement. */ export function resolveWaylandDisplay(): string | null { const declared = process.env.WAYLAND_DISPLAY?.trim(); if (declared) return declared; const runtime = runtimeDir(); if (!runtime) return null; try { const sockets = fs .readdirSync(runtime) .filter((entry) => /^wayland-\d+$/.test(entry)) .sort(); for (const socket of sockets) { if (fs.statSync(path.join(runtime, socket)).isSocket()) return socket; } } catch { /* pas de session Wayland accessible */ } return null; } /** * Bus de session de l'utilisateur. * * Firefox s'en sert pour confier une URL à l'instance déjà lancée. Sans lui, le * nouveau processus ne la trouve pas, bute sur le verrou de profil et affiche * « Firefox est déjà ouvert » au lieu d'ouvrir la page — c'est le mécanisme * retenu sous Wayland, où le protocole X de remoting n'existe pas. */ export function resolveSessionBus(): string | null { if (process.env.DBUS_SESSION_BUS_ADDRESS) return process.env.DBUS_SESSION_BUS_ADDRESS; const runtime = runtimeDir(); if (!runtime) return null; const socket = path.join(runtime, 'bus'); try { if (fs.statSync(socket).isSocket()) return `unix:path=${socket}`; } catch { /* pas de bus de session accessible */ } return null; } /** * Environnement d'exécution des outils de session (xdotool, navigateur). * * Un service systemd « system » n'hérite de rien de la session graphique : ni * écran, ni cookie d'autorisation, ni bus. Les trois se résolvent ici, à chaque * appel, pour survivre à un changement de session sans réinstallation. */ export function sessionEnv(): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = { ...process.env, DISPLAY: resolveDisplay() }; const auth = resolveXauthority(); if (auth) env.XAUTHORITY = auth; const runtime = runtimeDir(); if (runtime) env.XDG_RUNTIME_DIR = runtime; const bus = resolveSessionBus(); if (bus) env.DBUS_SESSION_BUS_ADDRESS = bus; const wayland = resolveWaylandDisplay(); if (wayland) env.WAYLAND_DISPLAY = wayland; return env; }