Fix : try FS 2
This commit is contained in:
16
README.md
16
README.md
@@ -164,6 +164,7 @@ L'envoi de touche se fait au niveau du système, pas via OBS.
|
|||||||
| OS | Mécanisme | À prévoir |
|
| OS | Mécanisme | À prévoir |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Ubuntu | `xdotool windowactivate` + XTEST | `apt install xdotool`, session **X11** (pas Wayland), `DISPLAY` accessible à l'agent |
|
| Ubuntu | `xdotool windowactivate` + XTEST | `apt install xdotool`, session **X11** (pas Wayland), `DISPLAY` accessible à l'agent |
|
||||||
|
| | | Le cookie X est résolu à l'exécution : `$XAUTHORITY`, puis `/run/user/<uid>/gdm/Xauthority`, les cookies Xwayland, puis `~/.Xauthority` |
|
||||||
| Windows | `SetForegroundWindow` + `SendKeys` | L'agent doit tourner dans la session interactive — d'où la tâche planifiée plutôt qu'un service |
|
| Windows | `SetForegroundWindow` + `SendKeys` | L'agent doit tourner dans la session interactive — d'où la tâche planifiée plutôt qu'un service |
|
||||||
| macOS | AppleScript System Events | Autorisation Accessibilité (prévu pour le développement) |
|
| macOS | AppleScript System Events | Autorisation Accessibilité (prévu pour le développement) |
|
||||||
|
|
||||||
@@ -175,6 +176,21 @@ temps.
|
|||||||
Le bouton ⛶ sur la fiche de l'agent renvoie la touche à la demande, et ⟳ force une sonde
|
Le bouton ⛶ sur la fiche de l'agent renvoie la touche à la demande, et ⟳ force une sonde
|
||||||
immédiate — les deux servent à valider le titre de fenêtre sans attendre un vrai show privé.
|
immédiate — les deux servent à valider le titre de fenêtre sans attendre un vrai show privé.
|
||||||
|
|
||||||
|
Le **titre de fenêtre** à renseigner est celui de la fenêtre, pas le nom du processus. Sous
|
||||||
|
Firefox il vaut `<titre de la page> — Mozilla Firefox`, et le titre d'une page Stripchat se
|
||||||
|
termine par `| Stripchat` : `Stripchat` comme `Firefox` conviennent donc. En cas d'échec,
|
||||||
|
l'agent liste les fenêtres qu'il voit réellement, et `agent.cjs --check` fait de même sans
|
||||||
|
rien déclencher.
|
||||||
|
|
||||||
|
Trois causes distinctes produisaient autrefois le même message « aucune fenêtre ne
|
||||||
|
correspond » ; elles sont maintenant séparées :
|
||||||
|
|
||||||
|
| Symptôme | Cause |
|
||||||
|
| --- | --- |
|
||||||
|
| `Authorization required` / `Invalid MIT-MAGIC-COOKIE-1 key` | cookie X introuvable ou périmé |
|
||||||
|
| `Aucune fenêtre visible sur DISPLAY=:0` | session Wayland, ou navigateur hors session |
|
||||||
|
| Liste des fenêtres ouvertes | titre mal renseigné — recopier un fragment de la liste |
|
||||||
|
|
||||||
Si le rappel du plein écran s'avère fragile sur ta VM, l'alternative sans clavier est de
|
Si le rappel du plein écran s'avère fragile sur ta VM, l'alternative sans clavier est de
|
||||||
lancer le navigateur en mode kiosque (`chromium --kiosk`) : il n'y a alors plus de plein
|
lancer le navigateur en mode kiosque (`chromium --kiosk`) : il n'y a alors plus de plein
|
||||||
écran à restaurer.
|
écran à restaurer.
|
||||||
|
|||||||
@@ -258,8 +258,12 @@ WorkingDirectory=$INSTALL_DIR
|
|||||||
Environment=NODE_ENV=production
|
Environment=NODE_ENV=production
|
||||||
Environment=AGENT_CONFIG=$CONFIG_FILE
|
Environment=AGENT_CONFIG=$CONFIG_FILE
|
||||||
# Accès à la session graphique, nécessaire au rappel plein écran via xdotool.
|
# Accès à la session graphique, nécessaire au rappel plein écran via xdotool.
|
||||||
|
# XAUTHORITY n'est volontairement pas figé ici : son chemin dépend du
|
||||||
|
# gestionnaire de session (GDM le place sous /run/user/<uid>, Wayland lui donne
|
||||||
|
# un suffixe aléatoire) et change d'une session à l'autre. L'agent le résout à
|
||||||
|
# l'exécution. En déclarer un ici serait pire que rien : un ~/.Xauthority
|
||||||
|
# périmé, laissé par une ancienne session, serait retenu de préférence au bon.
|
||||||
Environment=DISPLAY=:0
|
Environment=DISPLAY=:0
|
||||||
Environment=XAUTHORITY=$USER_HOME/.Xauthority
|
|
||||||
ExecStart=$(command -v node) $INSTALL_DIR/agent.cjs
|
ExecStart=$(command -v node) $INSTALL_DIR/agent.cjs
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
@@ -275,12 +279,11 @@ systemctl restart "$SERVICE"
|
|||||||
# --- Vérification -----------------------------------------------------------
|
# --- Vérification -----------------------------------------------------------
|
||||||
echo
|
echo
|
||||||
# Mêmes variables que l'unité systemd, sinon le diagnostic signale un DISPLAY
|
# Mêmes variables que l'unité systemd, sinon le diagnostic signale un DISPLAY
|
||||||
# absent que le service, lui, aura bien.
|
# absent que le service, lui, aura bien. Le cookie X est résolu par l'agent.
|
||||||
CHECK_OK=true
|
CHECK_OK=true
|
||||||
sudo -u "$RUN_USER" \
|
sudo -u "$RUN_USER" \
|
||||||
AGENT_CONFIG="$CONFIG_FILE" \
|
AGENT_CONFIG="$CONFIG_FILE" \
|
||||||
DISPLAY=":0" \
|
DISPLAY=":0" \
|
||||||
XAUTHORITY="$USER_HOME/.Xauthority" \
|
|
||||||
"$(command -v node)" "$INSTALL_DIR/agent.cjs" --check || CHECK_OK=false
|
"$(command -v node)" "$INSTALL_DIR/agent.cjs" --check || CHECK_OK=false
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { execFile, spawn } from 'node:child_process';
|
import { execFile, spawn } from 'node:child_process';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import type { AgentEvent, BrowserSettings } from '@stream-control/shared';
|
import type { AgentEvent, BrowserSettings } from '@stream-control/shared';
|
||||||
|
import { x11Env } from './x11.ts';
|
||||||
|
|
||||||
const run = promisify(execFile);
|
const run = promisify(execFile);
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ export async function openUrl(
|
|||||||
const child = spawn(settings.command, args, {
|
const child = spawn(settings.command, args, {
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
env: { ...process.env, DISPLAY: process.env.DISPLAY ?? ':0' },
|
env: x11Env(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
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}`);
|
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[] = [];
|
let ids: string[] = [];
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import net from 'node:net';
|
|||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import { WebSocket } from 'ws';
|
import { WebSocket } from 'ws';
|
||||||
import { CONFIG_PATH, type AgentConfig } from './config.ts';
|
import { CONFIG_PATH, type AgentConfig } from './config.ts';
|
||||||
|
import { resolveDisplay, resolveXauthority, x11Env } from './x11.ts';
|
||||||
|
|
||||||
const run = promisify(execFile);
|
const run = promisify(execFile);
|
||||||
|
|
||||||
@@ -101,7 +102,7 @@ function probeHandshake(
|
|||||||
* renseigné. Sans lui, les trois produisent le même message.
|
* renseigné. Sans lui, les trois produisent le même message.
|
||||||
*/
|
*/
|
||||||
async function reportVisibleWindows(display: string): Promise<void> {
|
async function reportVisibleWindows(display: string): Promise<void> {
|
||||||
const env = { ...process.env, DISPLAY: display };
|
const env = { ...x11Env(), DISPLAY: display };
|
||||||
let ids: string[] = [];
|
let ids: string[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -219,16 +220,32 @@ export async function runDiagnostics(config: AgentConfig): Promise<number> {
|
|||||||
? line('ok', 'xdotool', 'installé')
|
? line('ok', 'xdotool', 'installé')
|
||||||
: line('warn', 'xdotool', 'absent — apt install xdotool'),
|
: line('warn', 'xdotool', 'absent — apt install xdotool'),
|
||||||
);
|
);
|
||||||
const display = process.env.DISPLAY;
|
const display = resolveDisplay();
|
||||||
results.push(
|
results.push(
|
||||||
display
|
line(
|
||||||
? line('ok', 'DISPLAY', display)
|
'ok',
|
||||||
: line('warn', 'DISPLAY', 'non défini — le rappel plein écran échouera'),
|
'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) {
|
if (process.env.WAYLAND_DISPLAY) {
|
||||||
line('warn', 'session', 'Wayland détecté — xdotool exige X11');
|
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') {
|
} else if (process.platform === 'win32') {
|
||||||
const hasPowershell = await commandExists('powershell.exe', ['-NoProfile', '-Command', 'exit']);
|
const hasPowershell = await commandExists('powershell.exe', ['-NoProfile', '-Command', 'exit']);
|
||||||
results.push(
|
results.push(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { execFile } from 'node:child_process';
|
import { execFile } from 'node:child_process';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
|
import { resolveXauthority, x11Env } from './x11.ts';
|
||||||
|
|
||||||
const run = promisify(execFile);
|
const run = promisify(execFile);
|
||||||
|
|
||||||
@@ -77,6 +78,7 @@ function displayFault(err: unknown): string | null {
|
|||||||
return 'xdotool est absent — installe-le : apt install xdotool';
|
return 'xdotool est absent — installe-le : apt install xdotool';
|
||||||
}
|
}
|
||||||
const stderr = String((err as { stderr?: string }).stderr ?? '').trim();
|
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
|
// « 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.
|
// sur le mauvais fichier — le cas le plus courant sous GDM.
|
||||||
if (
|
if (
|
||||||
@@ -85,9 +87,13 @@ function displayFault(err: unknown): string | null {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
`xdotool ne peut pas ouvrir l'affichage DISPLAY=${process.env.DISPLAY ?? ':0'} : ${stderr}. ` +
|
`xdotool ne peut pas ouvrir l'affichage : ${stderr}. Cookie d'autorisation ` +
|
||||||
"Vérifie XAUTHORITY dans l'unité systemd — sous GDM le fichier n'est pas " +
|
(auth
|
||||||
'~/.Xauthority mais /run/user/<uid>/gdm/Xauthority.'
|
? `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;
|
return null;
|
||||||
@@ -160,7 +166,7 @@ async function findWindow(env: NodeJS.ProcessEnv, match: string): Promise<Visibl
|
|||||||
}
|
}
|
||||||
|
|
||||||
function describeNoMatch(match: string, windows: VisibleWindow[]): string {
|
function describeNoMatch(match: string, windows: VisibleWindow[]): string {
|
||||||
const display = process.env.DISPLAY ?? ':0';
|
const display = x11Env().DISPLAY;
|
||||||
|
|
||||||
if (windows.length === 0) {
|
if (windows.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -180,7 +186,7 @@ function describeNoMatch(match: string, windows: VisibleWindow[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function sendLinux(key: string, match: string): Promise<HotkeyResult> {
|
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);
|
const target = await findWindow(env, match);
|
||||||
|
|
||||||
await run('xdotool', ['windowactivate', '--sync', target.id], { env, timeout: 5000 });
|
await run('xdotool', ['windowactivate', '--sync', target.id], { env, timeout: 5000 });
|
||||||
|
|||||||
89
packages/agent/src/x11.ts
Normal file
89
packages/agent/src/x11.ts
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user