158 lines
5.1 KiB
TypeScript
158 lines
5.1 KiB
TypeScript
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/<uid>`, 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;
|
|
}
|