Fix : try FS 2
All checks were successful
release / build (push) Successful in 22s
release / verify-windows (push) Successful in 1m15s

This commit is contained in:
jeanotx32
2026-08-11 23:47:53 +02:00
parent f1a2066d56
commit 936c598013
6 changed files with 148 additions and 16 deletions

View File

@@ -1,6 +1,7 @@
import { execFile, spawn } from 'node:child_process';
import { promisify } from 'node:util';
import type { AgentEvent, BrowserSettings } from '@stream-control/shared';
import { x11Env } from './x11.ts';
const run = promisify(execFile);
@@ -47,7 +48,7 @@ export async function openUrl(
const child = spawn(settings.command, args, {
detached: true,
stdio: 'ignore',
env: { ...process.env, DISPLAY: process.env.DISPLAY ?? ':0' },
env: x11Env(),
});
return new Promise((resolve, reject) => {
@@ -81,7 +82,7 @@ export async function closeWindow(
throw new Error(`Fermeture de fenêtre non gérée sur ${process.platform}`);
}
const env = { ...process.env, DISPLAY: process.env.DISPLAY ?? ':0' };
const env = x11Env();
let ids: string[] = [];
try {

View File

@@ -3,6 +3,7 @@ import net from 'node:net';
import { promisify } from 'node:util';
import { WebSocket } from 'ws';
import { CONFIG_PATH, type AgentConfig } from './config.ts';
import { resolveDisplay, resolveXauthority, x11Env } from './x11.ts';
const run = promisify(execFile);
@@ -101,7 +102,7 @@ function probeHandshake(
* renseigné. Sans lui, les trois produisent le même message.
*/
async function reportVisibleWindows(display: string): Promise<void> {
const env = { ...process.env, DISPLAY: display };
const env = { ...x11Env(), DISPLAY: display };
let ids: string[] = [];
try {
@@ -219,16 +220,32 @@ export async function runDiagnostics(config: AgentConfig): Promise<number> {
? line('ok', 'xdotool', 'installé')
: line('warn', 'xdotool', 'absent — apt install xdotool'),
);
const display = process.env.DISPLAY;
const display = resolveDisplay();
results.push(
display
? line('ok', 'DISPLAY', display)
: line('warn', 'DISPLAY', 'non défini — le rappel plein écran échouera'),
line(
'ok',
'DISPLAY',
process.env.DISPLAY?.trim() ? display : `${display} (déduit, DISPLAY non transmis)`,
),
);
// Le cookie est la cause n°1 d'un rappel plein écran en échec : un service
// systemd n'hérite pas de celui de la session graphique.
const xauthority = resolveXauthority();
results.push(
xauthority
? line('ok', 'XAUTHORITY', xauthority)
: line(
'warn',
'XAUTHORITY',
"aucun cookie trouvé — xdotool sera refusé par le serveur X",
),
);
if (process.env.WAYLAND_DISPLAY) {
line('warn', 'session', 'Wayland détecté — xdotool exige X11');
}
if (hasXdotool && display) await reportVisibleWindows(display);
if (hasXdotool) await reportVisibleWindows(display);
} else if (process.platform === 'win32') {
const hasPowershell = await commandExists('powershell.exe', ['-NoProfile', '-Command', 'exit']);
results.push(

View File

@@ -1,5 +1,6 @@
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { resolveXauthority, x11Env } from './x11.ts';
const run = promisify(execFile);
@@ -77,6 +78,7 @@ function displayFault(err: unknown): string | null {
return 'xdotool est absent — installe-le : apt install xdotool';
}
const stderr = String((err as { stderr?: string }).stderr ?? '').trim();
const auth = resolveXauthority();
// « Invalid MIT-MAGIC-COOKIE-1 key » est le symptôme d'un XAUTHORITY qui pointe
// sur le mauvais fichier — le cas le plus courant sous GDM.
if (
@@ -85,9 +87,13 @@ function displayFault(err: unknown): string | null {
)
) {
return (
`xdotool ne peut pas ouvrir l'affichage DISPLAY=${process.env.DISPLAY ?? ':0'} : ${stderr}. ` +
"Vérifie XAUTHORITY dans l'unité systemd — sous GDM le fichier n'est pas " +
'~/.Xauthority mais /run/user/<uid>/gdm/Xauthority.'
`xdotool ne peut pas ouvrir l'affichage : ${stderr}. Cookie d'autorisation ` +
(auth
? `utilisé : ${auth} — il existe mais ne vaut pas pour cette session. ` +
'Ouvre puis referme une session graphique, ou redémarre le service.'
: "introuvable : aucun des emplacements connus n'existe " +
'(XAUTHORITY, /run/user/<uid>/gdm/Xauthority, ~/.Xauthority). ' +
"Une session graphique est-elle ouverte sur cette machine ?")
);
}
return null;
@@ -160,7 +166,7 @@ async function findWindow(env: NodeJS.ProcessEnv, match: string): Promise<Visibl
}
function describeNoMatch(match: string, windows: VisibleWindow[]): string {
const display = process.env.DISPLAY ?? ':0';
const display = x11Env().DISPLAY;
if (windows.length === 0) {
return (
@@ -180,7 +186,7 @@ function describeNoMatch(match: string, windows: VisibleWindow[]): string {
}
async function sendLinux(key: string, match: string): Promise<HotkeyResult> {
const env = { ...process.env, DISPLAY: process.env.DISPLAY ?? ':0' };
const env = x11Env();
const target = await findWindow(env, match);
await run('xdotool', ['windowactivate', '--sync', target.id], { env, timeout: 5000 });

89
packages/agent/src/x11.ts Normal file
View File

@@ -0,0 +1,89 @@
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';
}
/** Environnement d'exécution des outils graphiques (xdotool, navigateur). */
export function x11Env(): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = { ...process.env, DISPLAY: resolveDisplay() };
const auth = resolveXauthority();
if (auth) env.XAUTHORITY = auth;
return env;
}