fix : opening link 1
All checks were successful
release / build (push) Successful in 23s
release / verify-windows (push) Successful in 1m13s

This commit is contained in:
jeanotx32
2026-08-12 01:03:23 +02:00
parent 6ad8429cf1
commit a38baf9a88
6 changed files with 109 additions and 19 deletions

View File

@@ -351,6 +351,27 @@ L'agent évite donc les deux :
Les agents configurés avant ce correctif sont migrés au démarrage du serveur, sauf si leurs Les agents configurés avant ce correctif sont migrés au démarrage du serveur, sauf si leurs
arguments ou leur comportement d'arrêt ont été personnalisés. arguments ou leur comportement d'arrêt ont été personnalisés.
### « Firefox est déjà ouvert »
Ce dialogue signifie que le processus lancé par l'agent n'a pas trouvé l'instance déjà en
cours : il bute alors sur le verrou de profil au lieu de lui confier l'URL.
Le passage de relais se fait par le **bus de session D-Bus** — le seul mécanisme disponible
sous Wayland, le protocole X de remoting n'y existant pas. Or un service systemd « system »
n'hérite pas de ce bus, pas plus qu'il n'hérite du cookie X. L'agent résout donc lui-même,
à chaque lancement :
| Variable | Origine |
| --- | --- |
| `DISPLAY` | valeur héritée, sinon la socket X présente dans `/tmp/.X11-unix` |
| `XAUTHORITY` | `$XAUTHORITY` s'il existe, puis GDM, Xwayland, `~/.Xauthority` |
| `XDG_RUNTIME_DIR` | valeur héritée, sinon `/run/user/<uid>` |
| `DBUS_SESSION_BUS_ADDRESS` | valeur héritée, sinon la socket `$XDG_RUNTIME_DIR/bus` |
`agent.cjs --check` affiche les quatre. Si le bus ressort en avertissement, vérifie que
l'agent tourne bien sous **le même compte** que la session graphique : l'utilisateur est
choisi par `--user` à l'installation.
### Éviter l'accumulation d'onglets ### Éviter l'accumulation d'onglets
Avec les réglages d'usine de Firefox, un lien venu de l'extérieur ouvre un **nouvel Avec les réglages d'usine de Firefox, un lien venu de l'extérieur ouvre un **nouvel

View File

@@ -1,7 +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'; import { sessionEnv } from './x11.ts';
const run = promisify(execFile); const run = promisify(execFile);
@@ -9,6 +9,22 @@ export interface BrowserLog {
(level: 'info' | 'warn' | 'error', message: string, event?: AgentEvent): void; (level: 'info' | 'warn' | 'error', message: string, event?: AgentEvent): void;
} }
export interface LaunchOptions {
/**
* Forcer XWayland. Une fenêtre Wayland native est invisible à xdotool, donc
* impossible à basculer en plein écran par une touche. On ne l'impose que
* lorsque ce rappel est réellement demandé : sous Wayland, XWayland ajoute une
* copie d'image dont une capture par rognage OBS n'a que faire.
*/
forceXWayland?: boolean;
}
function launchEnv(options: LaunchOptions): NodeJS.ProcessEnv {
const env = sessionEnv();
if (options.forceXWayland) env.MOZ_ENABLE_WAYLAND = '0';
return env;
}
/** /**
* Seules les URL http(s) sont acceptées. Elles finissent en argument de * Seules les URL http(s) sont acceptées. Elles finissent en argument de
* processus — jamais dans un shell, donc pas d'injection possible — mais un * processus — jamais dans un shell, donc pas d'injection possible — mais un
@@ -39,6 +55,7 @@ export async function openUrl(
settings: BrowserSettings, settings: BrowserSettings,
url: string, url: string,
log: BrowserLog, log: BrowserLog,
options: LaunchOptions = {},
): Promise<{ command: string; url: string }> { ): Promise<{ command: string; url: string }> {
const target = assertWebUrl(url); const target = assertWebUrl(url);
const args = [...settings.args, target]; const args = [...settings.args, target];
@@ -48,12 +65,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',
// Sous une session Wayland, une fenêtre Firefox native est invisible à env: launchEnv(options),
// xdotool : ni activation, ni touche, donc pas de plein écran. XWayland la
// rend pilotable, sans rien changer à la lecture vidéo. Sans effet sur une
// session X11, et sans effet non plus si une instance Firefox tourne déjà —
// elle récupérerait l'URL avec ses propres variables d'environnement.
env: { ...x11Env(), MOZ_ENABLE_WAYLAND: '0' },
}); });
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -85,6 +97,7 @@ export async function openUrl(
export async function blankPage( export async function blankPage(
settings: BrowserSettings, settings: BrowserSettings,
log: BrowserLog, log: BrowserLog,
options: LaunchOptions = {},
): Promise<{ command: string; url: string }> { ): Promise<{ command: string; url: string }> {
const args = [...settings.args, 'about:blank']; const args = [...settings.args, 'about:blank'];
log('info', 'Lecteur déchargé (page vide) — fenêtre conservée pour OBS', 'browser.closed'); log('info', 'Lecteur déchargé (page vide) — fenêtre conservée pour OBS', 'browser.closed');
@@ -92,7 +105,7 @@ export async function blankPage(
const child = spawn(settings.command, args, { const child = spawn(settings.command, args, {
detached: true, detached: true,
stdio: 'ignore', stdio: 'ignore',
env: { ...x11Env(), MOZ_ENABLE_WAYLAND: '0' }, env: launchEnv(options),
}); });
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -118,7 +131,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 = x11Env(); const env = sessionEnv();
let ids: string[] = []; let ids: string[] = [];
try { try {

View File

@@ -4,7 +4,7 @@ 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 { looksLikeWayland } from './hotkey.ts'; import { looksLikeWayland } from './hotkey.ts';
import { resolveDisplay, resolveXauthority, x11Env } from './x11.ts'; import { resolveDisplay, resolveSessionBus, resolveXauthority, sessionEnv } from './x11.ts';
const run = promisify(execFile); const run = promisify(execFile);
@@ -103,7 +103,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 = { ...x11Env(), DISPLAY: display }; const env = { ...sessionEnv(), DISPLAY: display };
let ids: string[] = []; let ids: string[] = [];
try { try {
@@ -253,6 +253,19 @@ export async function runDiagnostics(config: AgentConfig): Promise<number> {
), ),
); );
// Sans le bus de session, Firefox ne trouve pas l'instance déjà lancée et
// affiche « Firefox est déjà ouvert » au lieu d'ouvrir la page.
const bus = resolveSessionBus();
results.push(
bus
? line('ok', 'bus session', bus)
: line(
'warn',
'bus session',
"introuvable — Firefox affichera « déjà ouvert » au lieu d'ouvrir la page",
),
);
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');
} }

View File

@@ -1,6 +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'; import { resolveXauthority, sessionEnv } from './x11.ts';
const run = promisify(execFile); const run = promisify(execFile);
@@ -182,7 +182,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 = x11Env().DISPLAY; const display = sessionEnv().DISPLAY;
if (windows.length === 0) { if (windows.length === 0) {
return ( return (
@@ -213,7 +213,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 = x11Env(); const env = sessionEnv();
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 });

View File

@@ -238,7 +238,9 @@ async function startCapture(params: Record<string, unknown>): Promise<unknown> {
} }
const url = requireUrl(params); const url = requireUrl(params);
const opened = await openUrl(browserSettings, url, report); const opened = await openUrl(browserSettings, url, report, {
forceXWayland: watcher.snapshotSettings.fullscreen.enabled,
});
const wait = Number(params.readyDelayMs ?? browserSettings.readyDelayMs); const wait = Number(params.readyDelayMs ?? browserSettings.readyDelayMs);
report('info', `Attente de ${Math.round(wait / 1000)} s avant le plein écran`); report('info', `Attente de ${Math.round(wait / 1000)} s avant le plein écran`);
@@ -275,7 +277,9 @@ async function stopCapture(): Promise<unknown> {
const action = const action =
browserSettings.onStop === 'close' browserSettings.onStop === 'close'
? closeWindow(watcher.snapshotSettings.fullscreen.windowMatch, report) ? closeWindow(watcher.snapshotSettings.fullscreen.windowMatch, report)
: blankPage(browserSettings, report); : blankPage(browserSettings, report, {
forceXWayland: watcher.snapshotSettings.fullscreen.enabled,
});
closed = await action.catch((err: Error) => { closed = await action.catch((err: Error) => {
report('warn', `Libération de la fenêtre impossible : ${err.message}`, 'command.failed'); report('warn', `Libération de la fenêtre impossible : ${err.message}`, 'command.failed');
return `échec : ${err.message}`; return `échec : ${err.message}`;
@@ -297,7 +301,9 @@ async function runAction(action: AgentAction, params: Record<string, unknown>):
case 'hotkey.fullscreen': case 'hotkey.fullscreen':
return watcher.restoreFullscreen(); return watcher.restoreFullscreen();
case 'browser.open': case 'browser.open':
return openUrl(browserSettings, requireUrl(params), report); return openUrl(browserSettings, requireUrl(params), report, {
forceXWayland: watcher.snapshotSettings.fullscreen.enabled,
});
case 'browser.close': case 'browser.close':
return closeWindow(watcher.snapshotSettings.fullscreen.windowMatch, report); return closeWindow(watcher.snapshotSettings.fullscreen.windowMatch, report);
case 'capture.start': case 'capture.start':

View File

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